feat: upgrade refresh-favicon skill to update-metadata (#1502)

Replace the icon-only refresh-favicon skill with a comprehensive
update-metadata skill using TypeScript + Bun. The script fetches
live GitHub stats (stars, license, language) and refreshes icons,
with metadata completeness validation.

- update.ts: runnable script (bun run .claude/skills/update-metadata/update.ts)
- Supports --agent, --dry-run, --icons-only, --stats-only flags
- Uses gh api for GitHub data, native fetch for icon downloads
- Validates all 12 metadata fields per agent

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
A 2026-02-19 21:27:31 -08:00 committed by GitHub
parent 6ae650b5e8
commit 2a4e7ff983
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 252 additions and 76 deletions

View file

@ -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 <id>` — 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

View file

@ -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 <id>` — 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/`).

View file

@ -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<string, AgentEntry> = manifest.agents;
const sources: Record<string, SourceEntry> = 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<string, string> = {
"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);
});