mirror of
https://github.com/hanzoai/bot.git
synced 2026-10-30 17:24:26 +00:00
Rename all openclaw/claw identifiers, file paths, docs, CI configs, shell scripts, systemd units, Docker helpers, test fixtures, and translation files. Remove stale clawdbot/clawhub paths. Fix pre-commit hook for bash 3 compatibility (macOS). Zero openclaw or claw brand references remain in the working tree.
554 lines
29 KiB
TypeScript
554 lines
29 KiB
TypeScript
// Build Skills Catalog
|
||
//
|
||
// Parses all skills/<slug>/SKILL.md files and outputs:
|
||
// 1. ui/public/skills-catalog.json — JSON catalog for the list page
|
||
// 2. ui/public/skills/{slug}.html — Individual SEO-optimized detail pages
|
||
|
||
import MarkdownIt from "markdown-it";
|
||
import * as fs from "node:fs";
|
||
import * as path from "node:path";
|
||
|
||
const ROOT = path.resolve(import.meta.dirname, "..");
|
||
const SKILLS_DIR = path.join(ROOT, "skills");
|
||
const PUBLIC_DIR = path.join(ROOT, "ui/public");
|
||
const OUTPUT_JSON = path.join(PUBLIC_DIR, "skills-catalog.json");
|
||
const OUTPUT_HTML_DIR = path.join(PUBLIC_DIR, "skills");
|
||
|
||
const md = new MarkdownIt({ html: true, linkify: true, typographer: true });
|
||
|
||
/* ─── Category mapping ──────────────────────────────────────────────── */
|
||
|
||
const CATEGORY_MAP: Record<string, string> = {
|
||
github: "Development",
|
||
"coding-agent": "Development",
|
||
"skill-creator": "Development",
|
||
sag: "Development",
|
||
canvas: "Development",
|
||
bothub: "Development",
|
||
discord: "Communication",
|
||
slack: "Communication",
|
||
imsg: "Communication",
|
||
bluebubbles: "Communication",
|
||
"voice-call": "Communication",
|
||
wacli: "Communication",
|
||
notion: "Productivity",
|
||
obsidian: "Productivity",
|
||
"apple-notes": "Productivity",
|
||
"apple-reminders": "Productivity",
|
||
"bear-notes": "Productivity",
|
||
"things-mac": "Productivity",
|
||
trello: "Productivity",
|
||
himalaya: "Productivity",
|
||
gog: "Productivity",
|
||
summarize: "Content & Media",
|
||
"openai-whisper": "Content & Media",
|
||
"openai-whisper-api": "Content & Media",
|
||
"openai-image-gen": "Content & Media",
|
||
"nano-banana-pro": "Content & Media",
|
||
"video-frames": "Content & Media",
|
||
"sherpa-onnx-tts": "Content & Media",
|
||
"nano-pdf": "Content & Media",
|
||
gifgrep: "Content & Media",
|
||
peekaboo: "Content & Media",
|
||
camsnap: "Content & Media",
|
||
songsee: "Content & Media",
|
||
"local-places": "Search & Info",
|
||
goplaces: "Search & Info",
|
||
weather: "Search & Info",
|
||
blogwatcher: "Search & Info",
|
||
};
|
||
|
||
/* ─── Types ─────────────────────────────────────────────────────────── */
|
||
|
||
interface SkillData {
|
||
slug: string;
|
||
name: string;
|
||
description: string;
|
||
emoji: string;
|
||
homepage?: string;
|
||
category: string;
|
||
requires: { bins?: string[]; anyBins?: string[]; config?: string[]; env?: string[] };
|
||
install: Array<{
|
||
id: string;
|
||
kind: string;
|
||
label: string;
|
||
formula?: string;
|
||
package?: string;
|
||
module?: string;
|
||
}>;
|
||
readmeBody: string;
|
||
fileSize: number;
|
||
githubPath: string;
|
||
}
|
||
|
||
/* ─── Parsing ───────────────────────────────────────────────────────── */
|
||
|
||
function parseFrontmatter(content: string): { frontmatter: string; body: string } {
|
||
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
||
if (!match) {
|
||
return { frontmatter: "", body: content };
|
||
}
|
||
return { frontmatter: match[1], body: match[2] };
|
||
}
|
||
|
||
function escapeHtml(str: string): string {
|
||
return str
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
function parseSkillMd(slug: string, filePath: string): SkillData | null {
|
||
let raw: string;
|
||
try {
|
||
raw = fs.readFileSync(filePath, "utf-8");
|
||
} catch {
|
||
console.warn(` ⚠ Could not read ${filePath}`);
|
||
return null;
|
||
}
|
||
|
||
const { frontmatter, body } = parseFrontmatter(raw);
|
||
if (!frontmatter) {
|
||
console.warn(` ⚠ No frontmatter in ${slug}`);
|
||
return null;
|
||
}
|
||
|
||
// Extract simple key-value fields
|
||
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
||
const name = nameMatch ? nameMatch[1].replace(/^["']|["']$/g, "").trim() : slug;
|
||
|
||
const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
||
const description = descMatch ? descMatch[1].replace(/^["']|["']$/g, "").trim() : "";
|
||
|
||
const homeMatch = frontmatter.match(/^homepage:\s*(.+)$/m);
|
||
const homepage = homeMatch ? homeMatch[1].replace(/^["']|["']$/g, "").trim() : undefined;
|
||
|
||
// Extract metadata JSON block
|
||
let emoji = "🔧";
|
||
let requires: SkillData["requires"] = {};
|
||
let install: SkillData["install"] = [];
|
||
|
||
const metaIdx = frontmatter.indexOf("metadata:");
|
||
if (metaIdx >= 0) {
|
||
let metaStr = frontmatter.slice(metaIdx + "metadata:".length).trim();
|
||
// Strip trailing commas before } or ]
|
||
metaStr = metaStr.replace(/,(\s*[}\]])/g, "$1");
|
||
try {
|
||
const meta = JSON.parse(metaStr);
|
||
// Support both "bot" and "hanzo-bot" keys
|
||
const bot = meta.bot || meta["hanzo-bot"] || {};
|
||
emoji = bot.emoji || "🔧";
|
||
requires = bot.requires || {};
|
||
install = (bot.install || []).map((i: Record<string, unknown>) => ({
|
||
id: i.id as string,
|
||
kind: i.kind as string,
|
||
label: i.label as string,
|
||
formula: i.formula as string | undefined,
|
||
package: i.package as string | undefined,
|
||
module: i.module as string | undefined,
|
||
}));
|
||
} catch (e) {
|
||
console.warn(` ⚠ Failed to parse metadata for ${slug}: ${(e as Error).message}`);
|
||
}
|
||
}
|
||
|
||
const category = CATEGORY_MAP[slug] || "Utilities";
|
||
const fileSize = fs.statSync(filePath).size;
|
||
|
||
return {
|
||
slug,
|
||
name,
|
||
description,
|
||
emoji,
|
||
homepage,
|
||
category,
|
||
requires,
|
||
install,
|
||
readmeBody: body.trim(),
|
||
fileSize,
|
||
githubPath: `skills/${slug}/SKILL.md`,
|
||
};
|
||
}
|
||
|
||
/* ─── Shared CSS ────────────────────────────────────────────────────── */
|
||
|
||
function sharedCss(): string {
|
||
return `*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
||
:root{--brand:#fd4444;--brand-secondary:#ff6b6b;--bg:#000;--bg-card:rgba(255,255,255,0.04);--bg-hover:#1a1a1a;--border:rgba(255,255,255,0.08);--border-hover:rgba(255,255,255,0.16);--text:#fff;--muted:#888;--subtle:#555;--max-w:1200px;--ease:cubic-bezier(0.4,0,0.2,1);--ease-bounce:cubic-bezier(0.34,1.56,0.64,1);--font:'Inter',-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;--font-mono:'SF Mono',SFMono-Regular,Menlo,Consolas,monospace}
|
||
html{scroll-behavior:smooth}
|
||
body{font-family:var(--font);background:var(--bg);color:var(--text);line-height:1.6;-webkit-font-smoothing:antialiased;position:relative}
|
||
body::before{content:'';position:fixed;inset:0;background-image:linear-gradient(rgba(255,255,255,0.03) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,0.03) 1px,transparent 1px);background-size:64px 64px;pointer-events:none;z-index:0}
|
||
body::after{content:'';position:fixed;inset:0;opacity:0.035;pointer-events:none;z-index:0;background:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")}
|
||
body>*{position:relative;z-index:1}
|
||
a{color:inherit;text-decoration:none}
|
||
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
|
||
.header{position:fixed;top:0;left:0;right:0;z-index:100;background:rgba(0,0,0,0.85);backdrop-filter:blur(12px);border-bottom:1px solid var(--border)}
|
||
.header-inner{max-width:var(--max-w);margin:0 auto;padding:0 24px;height:56px;display:flex;align-items:center;justify-content:space-between}
|
||
.header-left{display:flex;align-items:center;gap:32px}
|
||
.logo{display:flex;align-items:center;gap:8px}
|
||
.logo-mark{width:24px;height:24px}
|
||
.logo-mark svg{width:100%;height:100%;display:block}
|
||
.logo-text{font-size:15px;font-weight:600;letter-spacing:-0.03em}
|
||
.nav-links{display:flex;gap:4px}
|
||
.nav-link{position:relative;padding:8px 12px;font-size:13px;font-weight:500;color:var(--muted);border-radius:6px;transition:color 0.15s,background 0.15s}
|
||
.nav-link:hover{color:var(--text);background:var(--bg-hover)}
|
||
.nav-link.active{color:var(--brand)}
|
||
.header-right{display:flex;align-items:center;gap:8px}
|
||
.btn-ghost{padding:8px 16px;font-size:13px;font-weight:500;color:var(--muted);border-radius:8px;transition:color 0.15s,background 0.15s}
|
||
.btn-ghost:hover{color:var(--text);background:var(--bg-hover)}
|
||
.btn-primary{display:inline-flex;align-items:center;justify-content:center;padding:8px 20px;font-size:13px;font-weight:600;color:#fff;background:var(--brand);border-radius:9999px;transition:opacity 0.15s,box-shadow 0.15s}
|
||
.btn-primary:hover{opacity:0.9;box-shadow:0 0 24px rgba(253,68,68,0.3)}
|
||
.btn-outline{display:inline-flex;align-items:center;justify-content:center;padding:8px 20px;font-size:13px;font-weight:500;color:var(--text);border:1px solid var(--border);border-radius:9999px;transition:border-color 0.15s,background 0.15s,box-shadow 0.15s}
|
||
.btn-outline:hover{border-color:rgba(253,68,68,0.3);background:rgba(253,68,68,0.06)}
|
||
.btn-lg{padding:14px 32px;font-size:15px;border-radius:9999px}
|
||
.footer{border-top:1px solid var(--border);padding:64px 24px 32px}
|
||
.footer-inner{max-width:var(--max-w);margin:0 auto}
|
||
.footer-grid{display:grid;grid-template-columns:240px repeat(4,1fr);gap:40px;margin-bottom:48px}
|
||
.footer-brand .footer-logo{display:flex;align-items:center;gap:8px;margin-bottom:16px}
|
||
.footer-brand .footer-logo svg{width:20px;height:20px}
|
||
.footer-brand .footer-logo span{font-size:14px;font-weight:600}
|
||
.footer-brand p{font-size:13px;color:var(--muted);line-height:1.6;margin-bottom:16px}
|
||
.footer-social{display:flex;gap:12px}
|
||
.footer-social a{width:32px;height:32px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:8px;color:var(--muted);transition:color 0.15s,border-color 0.15s}
|
||
.footer-social a:hover{color:var(--text);border-color:var(--border-hover)}
|
||
.footer-social svg{width:14px;height:14px}
|
||
.footer-col h4{font-size:12px;font-weight:500;letter-spacing:0.05em;text-transform:uppercase;color:var(--subtle);margin-bottom:16px}
|
||
.footer-col a{display:block;font-size:13px;color:var(--muted);padding:3px 0;transition:color 0.15s}
|
||
.footer-col a:hover{color:var(--text)}
|
||
.footer-bottom{display:flex;justify-content:space-between;align-items:center;padding-top:24px;border-top:1px solid var(--border);font-size:12px;color:var(--subtle)}
|
||
@media(max-width:1024px){.footer-grid{grid-template-columns:repeat(3,1fr)}.footer-brand{grid-column:1/-1}.nav-links{display:none}}
|
||
@media(max-width:768px){.footer-grid{grid-template-columns:repeat(2,1fr)}}
|
||
@media(max-width:480px){.footer-grid{grid-template-columns:1fr}}`;
|
||
}
|
||
|
||
/* ─── Shared HTML fragments ─────────────────────────────────────────── */
|
||
|
||
const HANZO_LOGO_SVG = `<svg viewBox="0 0 67 67" xmlns="http://www.w3.org/2000/svg"><path d="M22.21 67V44.6369H0V67H22.21Z" fill="#fff"/><path d="M0 44.6369L22.21 46.8285V44.6369H0Z" fill="#fff"/><path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#fff"/><path d="M22.21 0H0V22.3184H22.21V0Z" fill="#fff"/><path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#fff"/><path d="M66.6753 22.3185L44.5098 20.0822V22.3185H66.6753Z" fill="#fff"/><path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#fff"/></svg>`;
|
||
|
||
function headerHtml(activeNav?: string): string {
|
||
return `<header class="header">
|
||
<div class="header-inner">
|
||
<div class="header-left">
|
||
<a href="/" class="logo">
|
||
<div class="logo-mark">${HANZO_LOGO_SVG}</div>
|
||
<span class="logo-text">Bot</span>
|
||
</a>
|
||
<nav class="nav-links">
|
||
<a href="https://hanzo.ai" class="nav-link">AI</a>
|
||
<a href="/" class="nav-link">Home</a>
|
||
<a href="/skills" class="nav-link${activeNav === "skills" ? " active" : ""}">Skills</a>
|
||
<a href="/book" class="nav-link">Book</a>
|
||
<a href="https://docs.hanzo.bot" class="nav-link">Docs</a>
|
||
<a href="https://github.com/hanzoai/bot" class="nav-link">GitHub</a>
|
||
</nav>
|
||
</div>
|
||
<div class="header-right">
|
||
<a href="https://hanzo.ai/contact" class="btn-ghost">Contact Sales</a>
|
||
<a href="/chat" class="btn-primary">Sign In</a>
|
||
</div>
|
||
</div>
|
||
</header>`;
|
||
}
|
||
|
||
function footerHtml(): string {
|
||
return `<footer class="footer">
|
||
<div class="footer-inner">
|
||
<div class="footer-grid">
|
||
<div class="footer-brand">
|
||
<div class="footer-logo">${HANZO_LOGO_SVG}<span>Hanzo</span></div>
|
||
<p>Build, deploy, and manage autonomous AI agents across every platform.</p>
|
||
<div class="footer-social">
|
||
<a href="https://x.com/hanaborteam" aria-label="X"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg></a>
|
||
<a href="https://linkedin.com/company/hanzo-ai" aria-label="LinkedIn"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg></a>
|
||
<a href="https://discord.gg/XthHQQj" aria-label="Discord"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.37a19.791 19.791 0 00-4.885-1.515.074.074 0 00-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 00-5.487 0 12.64 12.64 0 00-.617-1.25.077.077 0 00-.079-.037A19.736 19.736 0 003.677 4.37a.07.07 0 00-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 00.031.057 19.9 19.9 0 005.993 3.03.078.078 0 00.084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 00-.041-.106 13.107 13.107 0 01-1.872-.892.077.077 0 01-.008-.128 10.2 10.2 0 00.372-.292.074.074 0 01.077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 01.078.01c.12.098.246.198.373.292a.077.077 0 01-.006.127 12.299 12.299 0 01-1.873.892.077.077 0 00-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 00.084.028 19.839 19.839 0 006.002-3.03.077.077 0 00.032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 00-.031-.03z"/></svg></a>
|
||
</div>
|
||
</div>
|
||
<div class="footer-col"><h4>Products</h4><a href="https://hanzo.ai">Hanzo AI</a><a href="https://hanzo.app">Hanzo App</a><a href="https://hanzo.bot">Hanzo Bot</a><a href="https://hanzo.ai/dev">Hanzo Dev</a><a href="https://hanzo.ai/cloud">Hanzo Cloud</a><a href="https://hanzo.app/download">Desktop App</a><a href="/book">The Hanzo Book</a></div>
|
||
<div class="footer-col"><h4>Developers</h4><a href="https://docs.hanzo.bot">Documentation</a><a href="https://github.com/hanzoai/bot">GitHub</a><a href="https://docs.hanzo.bot/api">API Reference</a><a href="/skills">Skills Hub</a><a href="https://docs.hanzo.bot/cli">CLI Reference</a></div>
|
||
<div class="footer-col"><h4>Solutions</h4><a href="#">Customer Support</a><a href="#">Developer Operations</a><a href="#">Marketing</a><a href="#">Finance</a><a href="#">Security</a><a href="#">Enterprise</a></div>
|
||
<div class="footer-col"><h4>Company</h4><a href="https://hanzo.ai/about">About</a><a href="https://hanzo.ai/blog">Blog</a><a href="https://hanzo.ai/careers">Careers</a><a href="https://hanzo.ai/contact">Contact</a><a href="https://hanzo.ai/privacy">Privacy</a><a href="https://hanzo.ai/terms">Terms</a><a href="https://status.hanzo.ai">Status</a></div>
|
||
</div>
|
||
<div class="footer-bottom">
|
||
<span>© 2016–2026 Hanzo Industries. All rights reserved.</span>
|
||
<span>hanzo.ai</span>
|
||
</div>
|
||
</div>
|
||
</footer>`;
|
||
}
|
||
|
||
/* ─── Detail page generator ─────────────────────────────────────────── */
|
||
|
||
function requiresList(r: SkillData["requires"]): string {
|
||
const items: string[] = [];
|
||
if (r.bins?.length) {
|
||
items.push(...r.bins.map((b) => `<code>${escapeHtml(b)}</code>`));
|
||
}
|
||
if (r.anyBins?.length) {
|
||
items.push(`any of: ${r.anyBins.map((b) => `<code>${escapeHtml(b)}</code>`).join(", ")}`);
|
||
}
|
||
if (r.config?.length) {
|
||
items.push(...r.config.map((c) => `<code>${escapeHtml(c)}</code>`));
|
||
}
|
||
if (r.env?.length) {
|
||
items.push(...r.env.map((e) => `<code>${escapeHtml(e)}</code>`));
|
||
}
|
||
return items.length ? items.join(", ") : "None";
|
||
}
|
||
|
||
function installHtml(installs: SkillData["install"]): string {
|
||
if (!installs.length) {
|
||
return "";
|
||
}
|
||
const rows = installs
|
||
.map((i) => {
|
||
let cmd = "";
|
||
if (i.kind === "brew" && i.formula) {
|
||
cmd = `brew install ${i.formula}`;
|
||
} else if (i.kind === "apt" && i.package) {
|
||
cmd = `sudo apt install ${i.package}`;
|
||
} else if (i.kind === "node" && i.package) {
|
||
cmd = `npm install -g ${i.package}`;
|
||
} else if (i.kind === "go" && i.module) {
|
||
cmd = `go install ${i.module}`;
|
||
} else if (i.label) {
|
||
cmd = i.label;
|
||
}
|
||
return `<div class="install-row"><span class="install-label">${escapeHtml(i.label || i.kind)}</span>${cmd ? `<code class="install-cmd">${escapeHtml(cmd)}</code>` : ""}</div>`;
|
||
})
|
||
.join("\n");
|
||
return `<div class="sidebar-card"><h3>Install</h3>${rows}</div>`;
|
||
}
|
||
|
||
function generateDetailPage(skill: SkillData): string {
|
||
const renderedReadme = md.render(skill.readmeBody);
|
||
const safeDesc = escapeHtml(skill.description);
|
||
const safeName = escapeHtml(skill.name);
|
||
const ghUrl = `https://github.com/hanzoai/bot/blob/main/${skill.githubPath}`;
|
||
|
||
return `<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||
<title>${safeName} — Hanzo Bot Skills</title>
|
||
<meta name="description" content="${safeDesc}">
|
||
<meta name="keywords" content="${safeName}, Hanzo Bot, AI skill, ${escapeHtml(skill.category)}, autonomous agent">
|
||
<link rel="canonical" href="https://hanzo.bot/skills/${skill.slug}">
|
||
<!-- Open Graph -->
|
||
<meta property="og:title" content="${safeName} — Hanzo Bot Skills">
|
||
<meta property="og:description" content="${safeDesc}">
|
||
<meta property="og:type" content="article">
|
||
<meta property="og:url" content="https://hanzo.bot/skills/${skill.slug}">
|
||
<meta property="og:site_name" content="Hanzo Bot">
|
||
<meta property="og:image" content="https://hanzo.bot/og-skills.png">
|
||
<!-- Twitter Card -->
|
||
<meta name="twitter:card" content="summary">
|
||
<meta name="twitter:site" content="@hanaborteam">
|
||
<meta name="twitter:title" content="${safeName} — Hanzo Bot Skills">
|
||
<meta name="twitter:description" content="${safeDesc}">
|
||
<!-- JSON-LD -->
|
||
<script type="application/ld+json">
|
||
${JSON.stringify(
|
||
{
|
||
"@context": "https://schema.org",
|
||
"@type": "SoftwareApplication",
|
||
name: `${skill.name} — Hanzo Bot Skill`,
|
||
description: skill.description,
|
||
applicationCategory: "DeveloperApplication",
|
||
operatingSystem: "Cross-platform",
|
||
offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
|
||
author: { "@type": "Organization", name: "Hanzo AI", url: "https://hanzo.ai" },
|
||
},
|
||
null,
|
||
2,
|
||
)}
|
||
</script>
|
||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🥷</text></svg>">
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
${sharedCss()}
|
||
|
||
/* ── Detail layout ── */
|
||
.detail-wrap{max-width:var(--max-w);margin:0 auto;padding:80px 24px 60px}
|
||
.back-link{display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--muted);margin-bottom:24px;transition:color 0.15s}
|
||
.back-link:hover{color:var(--text)}
|
||
.skill-hero{display:flex;align-items:center;gap:16px;margin-bottom:32px}
|
||
.skill-emoji{font-size:48px;line-height:1}
|
||
.skill-hero h1{font-size:clamp(28px,4vw,40px);font-weight:700;letter-spacing:-0.03em}
|
||
.skill-desc{font-size:15px;color:var(--muted);line-height:1.7;margin-bottom:40px;max-width:720px}
|
||
.detail-layout{display:grid;grid-template-columns:1fr 320px;gap:48px;align-items:start}
|
||
.detail-main{min-width:0}
|
||
.detail-sidebar{display:flex;flex-direction:column;gap:16px;position:sticky;top:80px}
|
||
.sidebar-card{background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:20px;backdrop-filter:blur(16px)}
|
||
.sidebar-card h3{font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:var(--subtle);margin-bottom:16px}
|
||
.info-row{display:flex;justify-content:space-between;align-items:baseline;padding:6px 0;font-size:13px;border-bottom:1px solid var(--border)}
|
||
.info-row:last-child{border-bottom:none}
|
||
.info-row .label{color:var(--muted)}
|
||
.info-row .value{color:var(--text);text-align:right}
|
||
.info-row .value code{font-family:var(--font-mono);font-size:11px;background:rgba(255,255,255,0.06);padding:2px 6px;border-radius:4px}
|
||
.info-row .value a{color:var(--brand);transition:opacity 0.15s}
|
||
.info-row .value a:hover{opacity:0.8}
|
||
.install-row{padding:8px 0;border-bottom:1px solid var(--border)}
|
||
.install-row:last-child{border-bottom:none}
|
||
.install-label{display:block;font-size:12px;color:var(--muted);margin-bottom:4px}
|
||
.install-cmd{display:block;font-family:var(--font-mono);font-size:12px;color:var(--brand-secondary);background:rgba(255,255,255,0.04);padding:6px 10px;border-radius:6px;word-break:break-all}
|
||
.sidebar-actions{display:flex;flex-direction:column;gap:8px}
|
||
.sidebar-actions a{text-align:center}
|
||
.web3-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;background:rgba(99,102,241,0.1);border:1px solid rgba(99,102,241,0.2);border-radius:6px;font-size:11px;font-weight:500;color:#818cf8;margin-bottom:4px}
|
||
.web3-features{display:flex;flex-direction:column;gap:6px;margin-top:12px}
|
||
.web3-feat{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)}
|
||
.web3-feat::before{content:'';width:6px;height:6px;border-radius:50%;background:#818cf8;flex-shrink:0}
|
||
.category-badge{display:inline-block;padding:3px 10px;border-radius:100px;font-size:11px;font-weight:500;background:rgba(253,68,68,0.08);color:var(--brand-secondary);border:1px solid rgba(253,68,68,0.15)}
|
||
/* ── README styles ── */
|
||
.skill-readme{line-height:1.7;color:rgba(255,255,255,0.85)}
|
||
.skill-readme h1{font-size:28px;font-weight:700;margin:32px 0 16px;letter-spacing:-0.02em}
|
||
.skill-readme h2{font-size:22px;font-weight:600;margin:28px 0 12px;letter-spacing:-0.01em;color:var(--text)}
|
||
.skill-readme h3{font-size:17px;font-weight:600;margin:24px 0 8px}
|
||
.skill-readme h4{font-size:14px;font-weight:600;margin:20px 0 8px}
|
||
.skill-readme p{margin:0 0 16px}
|
||
.skill-readme ul,.skill-readme ol{margin:0 0 16px;padding-left:24px}
|
||
.skill-readme li{margin-bottom:6px}
|
||
.skill-readme code{font-family:var(--font-mono);font-size:0.88em;background:rgba(255,255,255,0.06);padding:2px 6px;border-radius:4px}
|
||
.skill-readme pre{margin:0 0 16px;border-radius:8px;overflow-x:auto;background:rgba(255,255,255,0.04);border:1px solid var(--border)}
|
||
.skill-readme pre code{display:block;padding:16px;background:none;font-size:13px;line-height:1.5}
|
||
.skill-readme table{width:100%;border-collapse:collapse;margin:0 0 16px;font-size:13px}
|
||
.skill-readme th,.skill-readme td{padding:8px 12px;border:1px solid var(--border);text-align:left}
|
||
.skill-readme th{background:rgba(255,255,255,0.04);font-weight:600}
|
||
.skill-readme blockquote{border-left:3px solid var(--brand);padding:8px 16px;margin:0 0 16px;color:var(--muted);background:rgba(255,255,255,0.02);border-radius:0 8px 8px 0}
|
||
.skill-readme a{color:var(--brand-secondary);text-decoration:underline;text-underline-offset:2px}
|
||
.skill-readme a:hover{color:var(--brand)}
|
||
.skill-readme hr{border:none;border-top:1px solid var(--border);margin:24px 0}
|
||
.skill-readme strong{color:var(--text)}
|
||
@media(max-width:1024px){.detail-layout{grid-template-columns:1fr}.detail-sidebar{position:static;order:2}}
|
||
@media(max-width:768px){.detail-wrap{padding:72px 16px 40px}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
${headerHtml("skills")}
|
||
<main class="detail-wrap">
|
||
<a href="/skills" class="back-link">← Back to all skills</a>
|
||
<div class="skill-hero">
|
||
<span class="skill-emoji">${skill.emoji}</span>
|
||
<div>
|
||
<h1>${safeName}</h1>
|
||
</div>
|
||
</div>
|
||
<p class="skill-desc">${safeDesc}</p>
|
||
<div class="detail-layout">
|
||
<div class="detail-main">
|
||
<div class="skill-readme">${renderedReadme}</div>
|
||
</div>
|
||
<aside class="detail-sidebar">
|
||
<div class="sidebar-card">
|
||
<h3>Info</h3>
|
||
<div class="info-row"><span class="label">Category</span><span class="value"><span class="category-badge">${escapeHtml(skill.category)}</span></span></div>
|
||
<div class="info-row"><span class="label">Requires</span><span class="value">${requiresList(skill.requires)}</span></div>
|
||
<div class="info-row"><span class="label">Size</span><span class="value">${(skill.fileSize / 1024).toFixed(1)} KB</span></div>
|
||
${skill.homepage ? `<div class="info-row"><span class="label">Homepage</span><span class="value"><a href="${escapeHtml(skill.homepage)}" target="_blank" rel="noopener">${escapeHtml(new URL(skill.homepage).hostname)}</a></span></div>` : ""}
|
||
<div class="info-row"><span class="label">Source</span><span class="value"><a href="${escapeHtml(ghUrl)}" target="_blank" rel="noopener">GitHub</a></span></div>
|
||
<div class="info-row" id="lastUpdated"><span class="label">Updated</span><span class="value">—</span></div>
|
||
</div>
|
||
${installHtml(skill.install)}
|
||
<div class="sidebar-card sidebar-actions">
|
||
<a href="${escapeHtml(ghUrl)}" class="btn-outline" target="_blank" rel="noopener">View on GitHub</a>
|
||
<a href="/chat" class="btn-primary">Open in Hanzo Bot</a>
|
||
</div>
|
||
<div class="sidebar-card">
|
||
<h3>Web3 Native</h3>
|
||
<div class="web3-badge">◆ On-Chain Ready</div>
|
||
<div class="web3-features">
|
||
<div class="web3-feat">DID identity for every bot</div>
|
||
<div class="web3-feat">Integrated wallet & payments</div>
|
||
<div class="web3-feat">Prefund balance, draw on credits</div>
|
||
<div class="web3-feat">On-chain & fiat payment rails</div>
|
||
<div class="web3-feat">Send funds to bots directly</div>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</main>
|
||
${footerHtml()}
|
||
<script>
|
||
(function(){
|
||
var slug='${skill.slug}';
|
||
var el=document.getElementById('lastUpdated');
|
||
if(!el)return;
|
||
var key='gh-commits-'+slug;
|
||
var cached=sessionStorage.getItem(key);
|
||
function show(data){
|
||
if(!Array.isArray(data)||!data[0]||!data[0].commit)return;
|
||
var d=new Date(data[0].commit.author.date);
|
||
var days=Math.floor((Date.now()-d.getTime())/86400000);
|
||
var txt=days===0?'today':days===1?'yesterday':days+' days ago';
|
||
el.querySelector('.value').textContent=txt;
|
||
}
|
||
if(cached){try{show(JSON.parse(cached))}catch(e){}}
|
||
else{
|
||
fetch('https://api.github.com/repos/hanzoai/bot/commits?path=skills/'+slug+'/SKILL.md&per_page=1')
|
||
.then(function(r){return r.json()})
|
||
.then(function(d){try{sessionStorage.setItem(key,JSON.stringify(d))}catch(e){}show(d)})
|
||
.catch(function(){});
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
|
||
/* ─── Main ──────────────────────────────────────────────────────────── */
|
||
|
||
function main(): void {
|
||
console.log("🔧 Building skills catalog...\n");
|
||
|
||
// Collect all skill directories
|
||
const dirs = fs
|
||
.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
||
.filter((d) => d.isDirectory())
|
||
.map((d) => d.name)
|
||
.toSorted();
|
||
|
||
console.log(` Found ${dirs.length} skill directories\n`);
|
||
|
||
const skills: SkillData[] = [];
|
||
|
||
for (const slug of dirs) {
|
||
const skillMdPath = path.join(SKILLS_DIR, slug, "SKILL.md");
|
||
if (!fs.existsSync(skillMdPath)) {
|
||
console.warn(` ⚠ No SKILL.md in ${slug}, skipping`);
|
||
continue;
|
||
}
|
||
const skill = parseSkillMd(slug, skillMdPath);
|
||
if (skill) {
|
||
skills.push(skill);
|
||
console.log(` ✓ ${skill.emoji} ${skill.name} (${skill.category})`);
|
||
}
|
||
}
|
||
|
||
console.log(`\n Parsed ${skills.length} skills\n`);
|
||
|
||
// Write JSON catalog (without readmeBody to keep it small for the list page)
|
||
const catalog = skills.map(({ readmeBody: _readmeBody, ...rest }) => rest);
|
||
fs.mkdirSync(path.dirname(OUTPUT_JSON), { recursive: true });
|
||
fs.writeFileSync(OUTPUT_JSON, JSON.stringify(catalog, null, 2));
|
||
console.log(` ✓ Wrote ${OUTPUT_JSON}`);
|
||
|
||
// Generate individual detail pages
|
||
fs.mkdirSync(OUTPUT_HTML_DIR, { recursive: true });
|
||
let pageCount = 0;
|
||
for (const skill of skills) {
|
||
const html = generateDetailPage(skill);
|
||
const outPath = path.join(OUTPUT_HTML_DIR, `${skill.slug}.html`);
|
||
fs.writeFileSync(outPath, html);
|
||
pageCount++;
|
||
}
|
||
console.log(` ✓ Generated ${pageCount} detail pages in ${OUTPUT_HTML_DIR}/`);
|
||
|
||
console.log("\n🎉 Done!\n");
|
||
}
|
||
|
||
main();
|