mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-07 09:10:55 +00:00
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:
parent
6ae650b5e8
commit
2a4e7ff983
3 changed files with 252 additions and 76 deletions
|
|
@ -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
|
||||
30
.claude/skills/update-metadata/SKILL.md
Normal file
30
.claude/skills/update-metadata/SKILL.md
Normal 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/`).
|
||||
222
.claude/skills/update-metadata/update.ts
Normal file
222
.claude/skills/update-metadata/update.ts
Normal 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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue