Skill: calecorbett/discord-hub-builder (version 1.0.0)

--- name: discord-hub-builder description: "Build a complete Discord AI command center server from scratch using the Discord REST API. Use when a user wants to set up a professional Discord server for AI agent management, including categories, channels, roles, permissions, and pinned workflow cards. Executes entirely via API — no manual Discord UI steps required. Requires a Discord bot token with Manage Channels, Manage Roles, and Send Messages permissions, plus the target guild/server ID." --- # Discord Hub Builder Builds a complete AI command center Discord server via API. No manual steps. ## What Gets Built **Categories & Channels:** - COMMAND CENTER → #daily-brief, #task-queue - AGENT CHANNELS → #agent-openclaw, #agent-perplexity, #agent-manus - RESEARCH & OUTPUTS → #financial-briefs, #content-drafts, #research-archive - OPERATIONS → #agent-logs, #ops-notes, #personal **Roles:** Agent (blue), Reviewer (green) **Workflow Cards:** Pinned in each agent channel with TOOL / TRIGGER / INPUT / OUTPUT / FREQUENCY ## Prerequisites Before running, confirm the user has: 1. **A Discord bot** — created at https://discord.com/developers/applications 2. **Bot permissions:** `Manage Channels`, `Manage Roles`, `Send Messages`, `Manage Messages` (for pinning) 3. **Bot invited to the server** — use OAuth2 URL with `bot` scope + above permissions 4. **Guild ID** — right-click server name → Copy Server ID (Developer Mode must be on) 5. **Bot token** — from the Bot tab in developer portal If the user doesn't have these, walk through setup before running. ## Execution ### Dry run first (always) ```bash python3 scripts/build_hub.py --token BOT_TOKEN --guild GUILD_ID --dry-run ``` Show the user the preview. Confirm before live run. ### Live run ```bash python3 scripts/build_hub.py --token BOT_TOKEN --guild GUILD_ID ``` The script: - Creates roles (skips if already exist by name) - Creates all categories and channels - Posts and pins workflow cards in agent channels - Prints channel IDs on completion ## After Build Tell the user: 1. Set channel-specific permissions manually for `#task-queue` (Owner-only send) and readonly channels — Discord's permission API requires role IDs which vary per server 2. To add more agent channels: copy an existing agent channel block in the script and re-run 3. Workflow cards can be edited by finding the pinned message in each agent channel ## Error Handling If the script fails mid-run: - Re-running is safe — roles skip if they exist by name - Channels don't have dedup logic; re-run will create duplicates — delete extras manually or use the dry-run to check state first - Rate limit errors (429): add `time.sleep(1)` between calls or wait and retry

path: scripts/build_hub.py
content: #!/usr/bin/env python3
"""
Discord AI Hub Builder
Builds a full AI command center server structure via Discord REST API.
Usage: python3 build_hub.py --token BOT_TOKEN --guild GUILD_ID [--dry-run]
"""

import argparse
import json
import time
import sys
import urllib.request
import urllib.error

BASE = "https://discord.com/api/v10"


def api(method, path, token, data=None):
    url = f"{BASE}{path}"
    body = json.dumps(data).encode() if data else None
    req = urllib.request.Request(
        url,
        data=body,
        method=method,
        headers={
            "Authorization": f"Bot {token}",
            "Content-Type": "application/json",
            "User-Agent": "OpenClaw-Discord-Hub-Builder/1.0",
        },
    )
    try:
        with urllib.request.urlopen(req) as resp:
            result = json.loads(resp.read())
            time.sleep(0.5)  # basic rate limit respect
            return result
    except urllib.error.HTTPError as e:
        body = e.read().decode()
        print(f"  ERROR {e.code} on {method} {path}: {body}", file=sys.stderr)
        return None


def build_hub(token, guild_id, dry_run=False):
    print(f"\n{'[DRY RUN] ' if dry_run else ''}Building AI Hub for guild {guild_id}\n")

    # ── Roles ──────────────────────────────────────────────────────────────
    print("── Creating roles ──")
    roles = {
        "Agent": {"color": 0x5865F2, "hoist": True},
        "Reviewer": {"color": 0x57F287, "hoist": True},
    }
    role_ids = {}
    existing_roles = api("GET", f"/guilds/{guild_id}/roles", token) or []
    existing_names = {r["name"]: r["id"] for r in existing_roles}

    for name, opts in roles.items():
        if name in existing_names:
            role_ids[name] = existing_names[name]
            print(f"  ✓ {name} (exists)")
        elif not dry_run:
            r = api("POST", f"/guilds/{guild_id}/roles", token, {"name": name, **opts})
            if r:
                role_ids[name] = r["id"]
                print(f"  + {name} created (id={r['id']})")
        else:
            print(f"  [dry] Would create role: {name}")

    # ── Categories & Channels ──────────────────────────────────────────────
    print("\n── Creating categories & channels ──")

    STRUCTURE = [
        {
            "category": "COMMAND CENTER",
            "channels": [
                {"name": "daily-brief", "topic": "Daily summaries and briefings from agents.", "readonly_humans": False},
                {"name": "task-queue", "topic": "Owner-only task intake. Drop requests here.", "owner_only_send": True},
            ],
        },
        {
            "category": "AGENT CHANNELS",
            "channels": [
                {"name": "agent-openclaw", "topic": "OpenClaw agent outputs and status.", "workflow_card": True, "tool": "OpenClaw", "trigger": "Heartbeat + direct task", "input": "Plain English task or cron", "output": "Summaries, files, confirmations", "frequency": "On-demand + 2-4x/day heartbeat"},
                {"name": "agent-perplexity", "topic": "Perplexity research outputs.", "workflow_card": True, "tool": "Perplexity", "trigger": "Research request", "input": "Question or topic", "output": "Research brief with sources", "frequency": "On-demand"},
                {"name": "agent-manus", "topic": "Manus agent outputs.", "workflow_card": True, "tool": "Manus", "trigger": "Complex multi-step task", "input": "Detailed task description", "output": "Completed task artifacts", "frequency": "On-demand"},
            ],
        },
        {
            "category": "RESEARCH & OUTPUTS",
            "channels": [
                {"name": "financial-briefs", "topic": "Financial research and analysis outputs.", "readonly_humans": True},
                {"name": "content-drafts", "topic": "Draft content from agents for review.", "readonly_humans": True},
                {"name": "research-archive", "topic": "Completed research drops.", "readonly_humans": True},
            ],
        },
        {
            "category": "OPERATIONS",
            "channels": [
                {"name": "agent-logs", "topic": "Agent activity logs.", "readonly_humans": True},
                {"name": "ops-notes", "topic": "Operational notes and config changes."},
                {"name": "personal", "topic": "Personal channel — Owner only."},
            ],
        },
    ]

    created = []

    for block in STRUCTURE:
        cat_name = block["category"]
        if not dry_run:
            cat = api("POST", f"/guilds/{guild_id}/channels", token, {
                "name": cat_name,
                "type": 4,  # GUILD_CATEGORY
            })
            cat_id = cat["id"] if cat else None
            print(f"\n  [Category] {cat_name} (id={cat_id})")
        else:
            cat_id = "DRY_RUN_ID"
            print(f"\n  [dry] Category: {cat_name}")

        for ch in block["channels"]:
            ch_data = {
                "name": ch["name"],
                "type": 0,  # GUILD_TEXT
                "parent_id": cat_id,
                "topic": ch.get("topic", ""),
            }

            if not dry_run:
                channel = api("POST", f"/guilds/{guild_id}/channels", token, ch_data)
                if channel:
                    ch_id = channel["id"]
                    print(f"    + #{ch['name']} (id={ch_id})")
                    created.append({"name": ch["name"], "id": ch_id, "data": ch})
                else:
                    print(f"    ! Failed to create #{ch['name']}")
            else:
                print(f"    [dry] #{ch['name']}")

    # ── Workflow Cards ─────────────────────────────────────────────────────
    if not dry_run and created:
        print("\n── Pinning workflow cards ──")
        for ch in created:
            if not ch["data"].get("workflow_card"):
                continue
            d = ch["data"]
            card = (
                f"```\n"
                f"TOOL      : {d.get('tool','—')}\n"
                f"TRIGGER   : {d.get('trigger','—')}\n"
                f"INPUT     : {d.get('input','—')}\n"
                f"OUTPUT    : {d.get('output','—')}\n"
                f"FREQUENCY : {d.get('frequency','—')}\n"
                f"```"
            )
            msg = api("POST", f"/channels/{ch['id']}/messages", token, {"content": card})
            if msg:
                api("PUT", f"/channels/{ch['id']}/pins/{msg['id']}", token)
                print(f"  ✓ Pinned card in #{ch['name']}")

    print("\n── Done ──")
    print(f"Created {len(created)} channels across {len(STRUCTURE)} categories.")
    if created:
        print("\nChannel IDs:")
        for ch in created:
            print(f"  #{ch['name']}: {ch['id']}")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Build Discord AI Hub")
    parser.add_argument("--token", required=True, help="Discord bot token")
    parser.add_argument("--guild", required=True, help="Guild (server) ID")
    parser.add_argument("--dry-run", action="store_true", help="Preview without creating")
    args = parser.parse_args()
    build_hub(args.token, args.guild, args.dry_run)


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)