diff --git a/.claude/skills/refresh-favicon/SKILL.md b/.claude/skills/refresh-favicon/SKILL.md deleted file mode 100644 index 47c89dd5..00000000 --- a/.claude/skills/refresh-favicon/SKILL.md +++ /dev/null @@ -1,76 +0,0 @@ -# Refresh Agent Favicons - -Re-download all agent icon/favicon files into `assets/agents/` and keep the manifest `icon` fields in sync. - -## When to use - -Run this when: -- An agent's logo changes (new branding, new org avatar) -- An icon URL breaks (404 or stale redirect) -- A new agent is added to the manifest without a local icon -- You suspect an icon file is corrupt or outdated - -## Arguments - -- `--agent ` — Refresh only the specified agent (e.g. `--agent openclaw`). Omit to refresh all. -- `--dry-run` — Print what would be downloaded without writing files. - -## Procedure - -### Step 1: Read the manifest - -```bash -python3 -c "import json; d=json.load(open('manifest.json')); [print(k, v.get('icon','')) for k,v in d['agents'].items()]" -``` - -### Step 2: Determine source URLs - -For each agent, the canonical icon source URL is tracked in `assets/agents/.sources.json`. -If it doesn't exist, fall back to the current `icon` field in `manifest.json`. - -### Step 3: Download each icon - -For each agent (or the specified `--agent`): - -1. Fetch the source URL with `curl -fsSL -o assets/agents/{agent}.{ext}` -2. Detect the file extension from the `Content-Type` header: - - `image/svg+xml` → `.svg` - - `image/png` → `.png` - - `image/jpeg` → `.jpg` - - `image/x-icon` or `image/vnd.microsoft.icon` → `.ico` -3. If the HTTP response is not 200, print a warning and skip that agent -4. If `--dry-run`, print what would be downloaded without writing - -### Step 4: Update assets/.sources.json - -Write (or update) `assets/agents/.sources.json` with the mapping of each agent to its source URL and detected extension: - -```json -{ - "claude": { "url": "https://...", "ext": "png" }, - "openclaw": { "url": "https://...", "ext": "png" }, - ... -} -``` - -### Step 5: Update manifest.json icon fields - -For each refreshed agent, set `icon` in `manifest.json` to the raw GitHub URL: - -``` -https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/assets/agents/{agent}.{ext} -``` - -### Step 6: Verify - -```bash -ls -lh assets/agents/ -python3 -c "import json; d=json.load(open('manifest.json')); [print(k, d['agents'][k].get('icon','MISSING')) for k in d['agents']]" -``` - -### Step 7: Summary - -Print a summary: -- Agents refreshed (with old → new byte sizes) -- Agents skipped (errors or dry-run) -- Any icon URL changes detected diff --git a/.claude/skills/update-metadata/SKILL.md b/.claude/skills/update-metadata/SKILL.md new file mode 100644 index 00000000..1820973a --- /dev/null +++ b/.claude/skills/update-metadata/SKILL.md @@ -0,0 +1,30 @@ +# Update Agent Metadata + +Refresh agent icons (favicons) and all metadata/stats in `manifest.json` by fetching live data from GitHub and agent websites. + +## When to use + +Run this when: +- An agent's logo changes (new branding, new org avatar) +- An icon URL breaks (404 or stale redirect) +- A new agent is added to the manifest without metadata +- GitHub star counts need refreshing (run periodically) +- Agent repo info changed (license, language, description) +- You want a full metadata audit across all agents + +## Arguments + +- `--agent ` — Update only the specified agent (e.g. `--agent openclaw`). Omit to update all. +- `--dry-run` — Print what would change without writing files. +- `--icons-only` — Only refresh icons, skip GitHub metadata. +- `--stats-only` — Only refresh GitHub stats, skip icon downloads. + +## Procedure + +Run the update script, passing through any arguments: + +```bash +bun run .claude/skills/update-metadata/update.ts [arguments] +``` + +Review the output, then commit the changed files (`manifest.json`, `assets/agents/.sources.json`, and any updated icon files in `assets/agents/`). diff --git a/.claude/skills/update-metadata/update.ts b/.claude/skills/update-metadata/update.ts new file mode 100644 index 00000000..f68ee25a --- /dev/null +++ b/.claude/skills/update-metadata/update.ts @@ -0,0 +1,222 @@ +import { readFileSync, writeFileSync, existsSync } from "fs"; +import { resolve } from "path"; + +// ── Types ─────────────────────────────────────────────────────────── + +interface AgentEntry { + icon?: string; + repo?: string; + github_stars?: number; + stars_updated?: string; + license?: string; + language?: string; + creator?: string; + created?: string; + added?: string; + runtime?: string; + category?: string; + tagline?: string; + tags?: string[]; + [key: string]: unknown; +} + +interface SourceEntry { + url: string; + ext: string; +} + +// ── Paths ─────────────────────────────────────────────────────────── + +const ROOT = resolve(import.meta.dir, "../../.."); +const MANIFEST_PATH = resolve(ROOT, "manifest.json"); +const SOURCES_PATH = resolve(ROOT, "assets/agents/.sources.json"); + +// ── Parse args ────────────────────────────────────────────────────── + +const args = process.argv.slice(2); +const dryRun = args.includes("--dry-run"); +const iconsOnly = args.includes("--icons-only"); +const statsOnly = args.includes("--stats-only"); +const agentIdx = args.indexOf("--agent"); +const onlyAgent = agentIdx !== -1 ? args[agentIdx + 1] : null; + +// ── Load data ─────────────────────────────────────────────────────── + +const manifest = JSON.parse(readFileSync(MANIFEST_PATH, "utf-8")); +const agents: Record = manifest.agents; +const sources: Record = existsSync(SOURCES_PATH) + ? JSON.parse(readFileSync(SOURCES_PATH, "utf-8")) + : {}; + +const agentIds = onlyAgent ? [onlyAgent] : Object.keys(agents); +const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD + +const EXT_MAP: Record = { + "image/svg+xml": "svg", + "image/png": "png", + "image/jpeg": "jpg", + "image/x-icon": "ico", + "image/vnd.microsoft.icon": "ico", +}; + +const METADATA_FIELDS = [ + "creator", + "repo", + "license", + "created", + "added", + "github_stars", + "stars_updated", + "language", + "runtime", + "category", + "tagline", + "tags", +]; + +// ── Icon refresh ──────────────────────────────────────────────────── + +async function refreshIcons() { + console.log("── Refreshing icons ──"); + for (const id of agentIds) { + const src = sources[id]; + if (!src) { + console.log(` ⚠ ${id}: no entry in .sources.json, skipping icon`); + continue; + } + try { + const res = await fetch(src.url); + if (!res.ok) { + console.log(` ⚠ ${id}: icon fetch failed (HTTP ${res.status})`); + continue; + } + const contentType = + res.headers.get("content-type")?.split(";")[0] ?? ""; + const ext = EXT_MAP[contentType] ?? src.ext; + const outPath = resolve(ROOT, `assets/agents/${id}.${ext}`); + const rawUrl = `https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/assets/agents/${id}.${ext}`; + + if (dryRun) { + console.log( + ` [dry-run] ${id}: would download ${src.url} → ${outPath}` + ); + } else { + const buf = Buffer.from(await res.arrayBuffer()); + writeFileSync(outPath, buf); + agents[id].icon = rawUrl; + sources[id].ext = ext; + console.log( + ` ✓ ${id}: icon refreshed (${buf.length} bytes, .${ext})` + ); + } + } catch (err) { + console.log(` ⚠ ${id}: icon fetch error: ${err}`); + } + } +} + +// ── GitHub metadata refresh ───────────────────────────────────────── + +async function refreshStats() { + console.log("── Refreshing GitHub stats ──"); + for (const id of agentIds) { + const agent = agents[id]; + if (!agent.repo) { + console.log(` ⚠ ${id}: no repo field, skipping GitHub metadata`); + continue; + } + try { + const proc = Bun.spawn( + [ + "gh", + "api", + `repos/${agent.repo}`, + "--jq", + "{stargazers_count, license: .license.spdx_id, language}", + ], + { stdout: "pipe", stderr: "pipe" } + ); + const out = await new Response(proc.stdout).text(); + const exitCode = await proc.exited; + if (exitCode !== 0) { + const errText = await new Response(proc.stderr).text(); + console.log(` ⚠ ${id}: gh api failed: ${errText.trim()}`); + continue; + } + const data = JSON.parse(out); + const oldStars = agent.github_stars; + + if (dryRun) { + console.log( + ` [dry-run] ${id}: stars ${oldStars ?? "?"} → ${data.stargazers_count}` + ); + if (data.license && data.license !== agent.license) + console.log( + ` [dry-run] ${id}: license ${agent.license ?? "?"} → ${data.license}` + ); + if (data.language && data.language !== agent.language) + console.log( + ` [dry-run] ${id}: language ${agent.language ?? "?"} → ${data.language}` + ); + } else { + agent.github_stars = data.stargazers_count; + agent.stars_updated = today; + if (data.license) agent.license = data.license; + if (data.language) agent.language = data.language; + const delta = + oldStars != null + ? ` (${data.stargazers_count - oldStars >= 0 ? "+" : ""}${data.stargazers_count - oldStars})` + : ""; + console.log(` ✓ ${id}: ${data.stargazers_count} stars${delta}`); + } + } catch (err) { + console.log(` ⚠ ${id}: GitHub metadata error: ${err}`); + } + } +} + +// ── Metadata completeness check ───────────────────────────────────── + +function validateMetadata() { + console.log("── Metadata completeness ──"); + for (const id of agentIds) { + const agent = agents[id]; + const missing = METADATA_FIELDS.filter((f) => agent[f] == null); + if (missing.length > 0) { + console.log(` ⚠ ${id}: missing ${missing.join(", ")}`); + } else { + console.log(` ✓ ${id}: all metadata fields present`); + } + } +} + +// ── Main ──────────────────────────────────────────────────────────── + +async function main() { + console.log( + `Updating metadata for ${agentIds.length} agent(s)${dryRun ? " [dry-run]" : ""}...\n` + ); + + if (!statsOnly) await refreshIcons(); + if (!iconsOnly) await refreshStats(); + validateMetadata(); + + if (!dryRun) { + writeFileSync( + MANIFEST_PATH, + JSON.stringify(manifest, null, 2) + "\n", + "utf-8" + ); + writeFileSync( + SOURCES_PATH, + JSON.stringify(sources, null, 2) + "\n", + "utf-8" + ); + console.log("\n✓ manifest.json and .sources.json updated"); + } +} + +main().catch((err) => { + console.error("Fatal:", err); + process.exit(1); +});