diff --git a/app/api/clawhub/route.ts b/app/api/clawhub/route.ts new file mode 100644 index 0000000..39946de --- /dev/null +++ b/app/api/clawhub/route.ts @@ -0,0 +1,163 @@ +import { NextRequest, NextResponse } from "next/server"; +import { exec } from "child_process"; +import { promisify } from "util"; +import { readFile, writeFile, mkdir } from "fs/promises"; +import { existsSync } from "fs"; +import path from "path"; + +const execAsync = promisify(exec); + +const SKILLS_DIR = "/root/.openclaw/workspace/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 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+([^\s]+)\s+\(([0-9.]+)\)$/); + if (match) { + return { slug: match[1], name: match[2], score: parseFloat(match[3]) }; + } + const parts = line.trim().split(/\s+/); + return { + slug: parts[0] || "", + name: parts[1] || parts[0] || "", + score: parseFloat(parts[parts.length - 1]?.replace(/[()]/g, "") || "0") || 0, + }; + }); + return NextResponse.json({ skills }); + } + + if (action === "popular") { + // Get featured/popular - show top skills sorted by score + const { stdout } = await execAsync(`clawhub search "" 2>&1`); + const lines = stdout.trim().split("\n"); + const skills = lines + .filter((line) => line.includes("(")) + .map((line) => { + const parts = line.trim().split(/\s+/); + return { + slug: parts[0] || "", + name: parts[1] || parts[0] || "", + score: parseFloat(parts[parts.length - 1]?.replace(/[()]/g, "") || "0") || 0, + }; + }) + .sort((a, b) => b.score - a.score) + .slice(0, 20); + return NextResponse.json({ skills }); + } + + if (action === "info") { + // Get skill info using inspect + const { stdout } = await execAsync(`clawhub 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 }; + return NextResponse.json({ + error: error.message || "Failed", + details: error.stdout || "", + }, { status: 500 }); + } +} + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { action, slug, name } = body; + + if (action === "install") { + const { stdout, stderr } = await execAsync( + `cd /root/.openclaw/workspace && clawhub 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); + + // Install to temp location for analysis + await execAsync(`cd /tmp && rm -rf ${slug} 2>/dev/null; clawhub 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 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")); + } + } + + return NextResponse.json({ + output: `Created custom skill at: ${newPath}\nSlug: ${newSlug}\n\nEdit the SKILL.md to customize further!` + }); + } + + return NextResponse.json({ error: "Unknown action" }, { status: 400 }); + } catch (e: unknown) { + const error = e as Error & { stdout?: string; stderr?: string }; + return NextResponse.json({ + error: error.message || "Failed", + details: error.stdout || error.stderr || "", + }, { status: 500 }); + } +} diff --git a/app/mission-control/clawhub/page.tsx b/app/mission-control/clawhub/page.tsx new file mode 100644 index 0000000..c2b0925 --- /dev/null +++ b/app/mission-control/clawhub/page.tsx @@ -0,0 +1,271 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import BackToMC from "@/components/mission-control/BackToMC"; + +interface Skill { + slug: string; + name: string; + score: number; + description?: string; + version?: string; + author?: string; + downloads?: number; +} + +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 [output, setOutput] = useState(""); + const [installing, setInstalling] = useState(false); + + 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)}`); + const data = await res.json(); + setSkills(data.skills || []); + } catch (e) { + console.error(e); + } + setLoading(false); + }, []); + + useEffect(() => { + searchSkills(""); + }, [searchSkills]); + + const getSkillDetails = async (slug: string) => { + setLoading(true); + try { + const res = await fetch(`/api/clawhub?action=info&slug=${encodeURIComponent(slug)}`); + const data = await res.json(); + setSelectedSkill({ ...data, slug }); + } catch (e) { + console.error(e); + } + setLoading(false); + }; + + const handleInstall = async () => { + if (!selectedSkill) return; + setInstalling(true); + setOutput("Installing...\n"); + try { + const res = await fetch(`/api/clawhub`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "install", slug: selectedSkill.slug }), + }); + 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"); + } 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, name: selectedSkill.name }), + }); + const data = await res.json(); + setOutput(data.output || data.error || "Clone complete"); + } catch (e) { + setOutput("Error: " + String(e)); + } + setAction(null); + }; + + return ( +
+ + +
+
+

🛒 ClawHub Marketplace

+

Search, install, analyze, and customize skills from ClawHub

+
+ +
+ setSearch(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && searchSkills(search)} + placeholder="Search skills..." + className="flex-1 bg-slate-800 border border-slate-700 rounded-lg px-4 py-2 text-white placeholder-slate-400 focus:outline-none focus:border-blue-500" + /> + + +
+ +
+ {/* Skills List */} +
+
+

Available Skills

+

{skills.length} skills found

+
+
+ {loading ? ( +
Loading...
+ ) : skills.length === 0 ? ( +
No skills found
+ ) : ( + skills.map((skill) => ( + + )) + )} +
+
+ + {/* Detail Panel */} +
+
+

+ {selectedSkill ? selectedSkill.name : "Select a Skill"} +

+ {selectedSkill && ( +

@{selectedSkill.slug}

+ )} +
+ + {!selectedSkill ? ( +
+ Click a skill to see details +
+ ) : ( +
+ {selectedSkill.description && ( +
+

Description

+

{selectedSkill.description}

+
+ )} + +
+ {selectedSkill.version && ( + + v{selectedSkill.version} + + )} + {selectedSkill.author && ( + + by {selectedSkill.author} + + )} + {selectedSkill.downloads !== undefined && ( + + {selectedSkill.downloads} downloads + + )} +
+ + {/* Action Buttons */} +
+ + + +
+ + {/* Output */} + {output && ( +
+

Output

+
+                      {output}
+                    
+
+ )} +
+ )} +
+
+
+
+ ); +} diff --git a/app/mission-control/council/page.tsx b/app/mission-control/council/page.tsx index fd4e1cc..2db5cc9 100644 --- a/app/mission-control/council/page.tsx +++ b/app/mission-control/council/page.tsx @@ -4,16 +4,16 @@ import BackToMC from "@/components/mission-control/BackToMC"; export default function CouncilPage() { return ( -
+
-
-
-

🏛️ Council Chat

-

Loading...

-
-
-

Loading...

-
+
+

🏛️ Council

+