From 064812d73038c36fc16ca4ed515ff62825ac6bff Mon Sep 17 00:00:00 2001 From: Horus AI Date: Tue, 24 Mar 2026 16:54:12 +0100 Subject: [PATCH] feat: Multi-agent ClawHub management - Agent selector (Horus, Amun, Cleo) - Install/delete skills per agent - Status bar showing installed skills per agent - Popular refresh button - Skills synced to agents-skills repo --- app/api/clawhub/route.ts | 145 +++++++-------- app/mission-control/clawhub/page.tsx | 266 +++++++++++++++------------ 2 files changed, 220 insertions(+), 191 deletions(-) diff --git a/app/api/clawhub/route.ts b/app/api/clawhub/route.ts index 3db9120..1dba815 100644 --- a/app/api/clawhub/route.ts +++ b/app/api/clawhub/route.ts @@ -1,14 +1,14 @@ import { NextRequest, NextResponse } from "next/server"; import { exec } from "child_process"; import { promisify } from "util"; -import { readFile, writeFile, mkdir } from "fs/promises"; +import { readFile, writeFile, mkdir, rm, readdir } from "fs/promises"; import { existsSync } from "fs"; import path from "path"; const execAsync = promisify(exec); const CLAWHUB_BIN = "/usr/bin/clawhub"; -const SKILLS_DIR = "/root/.openclaw/workspace/skills"; +const SKILLS_BASE = "/root/.openclaw/workspace/agents-skills"; export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url); @@ -34,8 +34,6 @@ export async function GET(req: NextRequest) { } if (action === "popular") { - // Get featured/popular - show top skills sorted by score - // Use a common query that returns high-quality skills const { stdout } = await execAsync(`${CLAWHUB_BIN} search "productivity automation" 2>&1`); const lines = stdout.trim().split("\n"); const skills = lines @@ -49,19 +47,41 @@ export async function GET(req: NextRequest) { }) .filter(Boolean) .sort((a: any, b: any) => b.score - a.score) - .slice(0, 20); + .slice(0, 30); return NextResponse.json({ skills }); } + if (action === "list") { + // List installed skills per agent + 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 entries = await readdir(agentPath, { withFileTypes: true }); + installed[agent] = entries + .filter((d) => d.isDirectory()) + .map((d) => d.name); + } catch { + installed[agent] = []; + } + } else { + installed[agent] = []; + } + } + return NextResponse.json({ installed }); + } + if (action === "info") { - // Get skill info using inspect 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: unknown) { - const error = e as Error & { stdout?: string }; + const error = e as Error & { stdout?: string; message?: string }; return NextResponse.json({ error: error.message || "Failed", details: error.stdout || "", @@ -72,87 +92,56 @@ export async function GET(req: NextRequest) { export async function POST(req: NextRequest) { try { const body = await req.json(); - const { action, slug, name } = body; + const { action, slug, agents = ["horus"], name } = body; if (action === "install") { - const { stdout, stderr } = await execAsync( - `cd /root/.openclaw/workspace && ${CLAWHUB_BIN} install ${slug} 2>&1` - ); - return NextResponse.json({ output: stdout || stderr || "Installed successfully" }); - } - - if (action === "analyze") { - // Download and analyze skill code - const skillPath = path.join(SKILLS_DIR, slug); + const results: string[] = []; - // Install to temp location for analysis - await execAsync(`cd /tmp && rm -rf ${slug} 2>/dev/null; ${CLAWHUB_BIN} install ${slug} --dir /tmp/${slug} 2>&1`); - - // Read the SKILL.md - const skillMdPath = path.join("/tmp", slug, "SKILL.md"); - let analysis = ""; - - if (existsSync(skillMdPath)) { - const content = await readFile(skillMdPath, "utf-8"); - analysis = `## Skill Analysis: ${slug}\n\n`; - analysis += `### Purpose\n${content.split("---")[2] || "Analysis unavailable"}\n\n`; - analysis += `### Triggers\nWhat activates this skill?\n\n`; - analysis += `### Actions\nWhat does it do?\n\n`; - analysis += `### Recommendations for Custom Version\n`; - analysis += `- Keep the core trigger logic\n`; - analysis += `- Customize for Haitham's workflow\n`; - analysis += `- Add personal touch\n`; - } else { - analysis = `Could not analyze ${slug} - skill structure not found`; - } - - return NextResponse.json({ output: analysis }); - } - - if (action === "clone") { - // Create a custom version of the skill - const skillPath = path.join(SKILLS_DIR, slug); - const newSlug = slug.replace(/[^a-z0-9-]/gi, "-") + "-custom"; - const newPath = path.join(SKILLS_DIR, newSlug); - - // Install original - await execAsync(`cd /tmp && rm -rf ${slug} 2>/dev/null; ${CLAWHUB_BIN} install ${slug} --dir /tmp/${slug} 2>&1`); - - // Create new version - await mkdir(newPath, { recursive: true }); - - const skillMdPath = path.join("/tmp", slug, "SKILL.md"); - const skillMd = existsSync(skillMdPath) - ? await readFile(skillMdPath, "utf-8") - : `# ${name || slug} Custom\n\nCustom version for Haitham.\n`; - - // Create customized SKILL.md - const customSkillMd = skillMd.replace( - /name:[^\n]+\n/, - `name: ${name || slug} Custom\n` - ).replace( - /description:[^\n]+\n/, - `description: Custom version built for Haitham's workflow\n` - ); - - await writeFile(path.join(newPath, "SKILL.md"), customSkillMd); - - // Copy other files if they exist - const srcFiles = ["/tmp/script.sh", "/tmp/prompts.md"]; - for (const f of srcFiles) { - if (existsSync(f)) { - await writeFile(path.join(newPath, path.basename(f)), await readFile(f, "utf-8")); + for (const agent of agents) { + const agentPath = path.join(SKILLS_BASE, agent, slug); + const agentDir = path.join(SKILLS_BASE, agent); + + // Ensure agent dir exists + if (!existsSync(agentDir)) { + await mkdir(agentDir, { recursive: true }); + } + + // Install to temp first + const tempPath = `/tmp/clawhub-install-${slug}`; + await execAsync(`rm -rf ${tempPath} 2>/dev/null; ${CLAWHUB_BIN} install ${slug} --dir ${tempPath} 2>&1`); + + // Move to agent folder + if (existsSync(tempPath)) { + await execAsync(`mv ${tempPath} ${agentPath}`); + results.push(`✓ ${agent}: installed ${slug}`); + } else { + results.push(`✗ ${agent}: failed to install ${slug}`); } } - return NextResponse.json({ - output: `Created custom skill at: ${newPath}\nSlug: ${newSlug}\n\nEdit the SKILL.md to customize further!` - }); + 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 rm(agentPath, { recursive: true }); + 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: unknown) { - const error = e as Error & { stdout?: string; stderr?: string }; + const error = e as Error & { stdout?: string; stderr?: string; message?: string }; return NextResponse.json({ error: error.message || "Failed", details: error.stdout || error.stderr || "", diff --git a/app/mission-control/clawhub/page.tsx b/app/mission-control/clawhub/page.tsx index c2b0925..1b0f28d 100644 --- a/app/mission-control/clawhub/page.tsx +++ b/app/mission-control/clawhub/page.tsx @@ -8,35 +8,36 @@ interface Skill { name: string; score: number; description?: string; - version?: string; - author?: string; - downloads?: number; } +const AGENTS = ["horus", "amun", "cleo"] as const; +const AGENT_LABELS: Record = { + horus: "🦅 Horus (Local)", + amun: "🤖 Amun", + cleo: "👑 Cleo", +}; + export default function ClawHubMarketplace() { const [skills, setSkills] = useState([]); const [search, setSearch] = useState(""); const [loading, setLoading] = useState(false); const [selectedSkill, setSelectedSkill] = useState(null); - const [action, setAction] = useState<"install" | "analyze" | "clone" | null>(null); + const [selectedAgents, setSelectedAgents] = useState(["horus"]); + const [action, setAction] = useState<"install" | "delete" | null>(null); const [output, setOutput] = useState(""); - const [installing, setInstalling] = useState(false); + const [installedSkills, setInstalledSkills] = useState>({}); + + const fetchInstalled = useCallback(async () => { + try { + const res = await fetch("/api/clawhub?action=list"); + const data = await res.json(); + setInstalledSkills(data.installed || {}); + } catch (e) { + console.error(e); + } + }, []); const searchSkills = useCallback(async (query: string) => { - if (!query.trim()) { - // Load popular skills - setLoading(true); - try { - const res = await fetch(`/api/clawhub?action=popular`); - const data = await res.json(); - setSkills(data.skills || []); - } catch (e) { - console.error(e); - } - setLoading(false); - return; - } - setLoading(true); try { const res = await fetch(`/api/clawhub?action=search&q=${encodeURIComponent(query)}`); @@ -48,76 +49,81 @@ export default function ClawHubMarketplace() { setLoading(false); }, []); - useEffect(() => { - searchSkills(""); - }, [searchSkills]); - - const getSkillDetails = async (slug: string) => { + const refreshPopular = useCallback(async () => { setLoading(true); try { - const res = await fetch(`/api/clawhub?action=info&slug=${encodeURIComponent(slug)}`); + const res = await fetch("/api/clawhub?action=popular"); const data = await res.json(); - setSelectedSkill({ ...data, slug }); + setSkills(data.skills || []); } catch (e) { console.error(e); } setLoading(false); + }, []); + + useEffect(() => { + refreshPopular(); + fetchInstalled(); + }, [refreshPopular, fetchInstalled]); + + const toggleAgent = (agent: string) => { + setSelectedAgents((prev) => + prev.includes(agent) ? prev.filter((a) => a !== agent) : [...prev, agent] + ); }; + const selectAll = () => setSelectedAgents([...AGENTS]); + const handleInstall = async () => { - if (!selectedSkill) return; - setInstalling(true); + if (!selectedSkill || selectedAgents.length === 0) return; + setAction("install"); setOutput("Installing...\n"); try { - const res = await fetch(`/api/clawhub`, { + const res = await fetch("/api/clawhub", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ action: "install", slug: selectedSkill.slug }), + body: JSON.stringify({ + action: "install", + slug: selectedSkill.slug, + agents: selectedAgents, + }), }); const data = await res.json(); setOutput(data.output || data.error || "Done"); - } catch (e) { - setOutput("Error: " + String(e)); - } - setInstalling(false); - }; - - const handleAnalyze = async () => { - if (!selectedSkill) return; - setAction("analyze"); - setOutput("Analyzing skill code...\n"); - try { - const res = await fetch(`/api/clawhub`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ action: "analyze", slug: selectedSkill.slug }), - }); - const data = await res.json(); - setOutput(data.output || data.error || "Analysis complete"); + fetchInstalled(); } catch (e) { setOutput("Error: " + String(e)); } setAction(null); }; - const handleClone = async () => { - if (!selectedSkill) return; - setAction("clone"); - setOutput("Creating custom version...\n"); + const handleDelete = async () => { + if (!selectedSkill || selectedAgents.length === 0) return; + setAction("delete"); + setOutput("Removing...\n"); try { - const res = await fetch(`/api/clawhub`, { + const res = await fetch("/api/clawhub", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ action: "clone", slug: selectedSkill.slug, name: selectedSkill.name }), + body: JSON.stringify({ + action: "delete", + slug: selectedSkill.slug, + agents: selectedAgents, + }), }); const data = await res.json(); - setOutput(data.output || data.error || "Clone complete"); + setOutput(data.output || data.error || "Done"); + fetchInstalled(); } catch (e) { setOutput("Error: " + String(e)); } setAction(null); }; + const isInstalled = (slug: string) => { + return AGENTS.some((agent) => installedSkills[agent]?.includes(slug)); + }; + return (
@@ -125,9 +131,37 @@ export default function ClawHubMarketplace() {

🛒 ClawHub Marketplace

-

Search, install, analyze, and customize skills from ClawHub

+

Search, install, and manage skills across all agents

+ {/* Agent Status Bar */} +
+
+

Agent Skills Status

+ +
+
+ {AGENTS.map((agent) => ( +
+
{AGENT_LABELS[agent]}
+
+ {installedSkills[agent]?.length || 0} skills installed +
+
+ {installedSkills[agent]?.slice(0, 3).join(", ") || "None"} + {installedSkills[agent]?.length > 3 && "..."} +
+
+ ))} +
+
+ + {/* Search */}
@@ -158,29 +192,41 @@ export default function ClawHubMarketplace() {

Available Skills

{skills.length} skills found

-
+
{loading ? (
Loading...
) : skills.length === 0 ? (
No skills found
) : ( - skills.map((skill) => ( - - )) + skills.map((skill) => { + const installed = isInstalled(skill.slug); + return ( + + ); + }) )}
@@ -202,53 +248,47 @@ export default function ClawHubMarketplace() {
) : (
- {selectedSkill.description && ( -
-

Description

-

{selectedSkill.description}

+ {/* Agent Selector */} +
+

Install On:

+
+ {AGENTS.map((agent) => ( + + ))}
- )} - -
- {selectedSkill.version && ( - - v{selectedSkill.version} - - )} - {selectedSkill.author && ( - - by {selectedSkill.author} - - )} - {selectedSkill.downloads !== undefined && ( - - {selectedSkill.downloads} downloads - - )} +
{/* Action Buttons */}
-
@@ -256,7 +296,7 @@ export default function ClawHubMarketplace() { {output && (

Output

-
+                    
                       {output}