// Generate (or verify) packages/cli/src/utils/agentDirs.generated.ts — the // home-relative GLOBAL skills directory for every agent the upstream // vercel-labs/skills CLI knows about, plus a marker dir that means "this agent // is installed on this machine". // // bun run --cwd packages/cli gen:agent-dirs # write/update (fetches upstream) // bun run --cwd packages/cli gen:agent-dirs --check # verify only (CI / pre-commit) // bun packages/cli/scripts/sync-agent-dirs.ts --src # offline // // Why generated, not hand-maintained: `skills add --global` installs into the // per-agent dirs encoded in upstream's `src/agents.ts` (~70 agents). We mirror // the canonical store into those same dirs, so the list must track upstream. The // `skills` npm package exports nothing importable (CLI-only, bundled dist), so // we parse the source at a PINNED tag and commit the result — deterministic at // runtime, no network at install time. Bump SKILLS_REF and re-run on upgrade. import { readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; // Pin to the upstream release whose dir layout we install against. Bump this // (and re-run) when the bundled `skills` version moves. const SKILLS_REPO = "vercel-labs/skills"; const SKILLS_REF = "v1.5.13"; const AGENTS_TS_URL = `https://raw.githubusercontent.com/${SKILLS_REPO}/${SKILLS_REF}/src/agents.ts`; const here = dirname(fileURLToPath(import.meta.url)); const outPath = join(here, "..", "src", "utils", "agentDirs.generated.ts"); const isCheck = process.argv.includes("--check"); const srcFlag = process.argv.indexOf("--src"); const srcPath = srcFlag !== -1 ? process.argv[srcFlag + 1] : undefined; // The base directories agents.ts builds globalSkillsDir from. Each is an env // override with a documented default — the mirror resolves them at runtime so a // machine with XDG_CONFIG_HOME / CODEX_HOME / CLAUDE_CONFIG_DIR set lands in the // same place the agent actually reads. We store the base NAME + suffix here // rather than a frozen path so that resolution stays faithful to upstream. const BASE_VARS = [ "home", "configHome", "codexHome", "claudeHome", "vibeHome", "hermesHome", "autohandHome", ] as const; type BaseVar = (typeof BASE_VARS)[number]; interface AgentGlobalDir { agent: string; base: BaseVar; sub: string; } function posixJoin(parts: string[]): string { return parts .flatMap((p) => p.split("/")) .filter((s) => s && s !== ".") .join("/"); } /** Resolve a globalSkillsDir expression to { base, sub }, or null if none. */ function resolveGlobalExpr(expr: string): { base: BaseVar; sub: string } | null { const e = expr.trim(); if (e === "undefined") return null; // agent defines no global skills dir // openclaw's helper falls back to ~/.openclaw/skills when no variant is present. if (e.startsWith("getOpenClawGlobalSkillsDir")) return { base: "home", sub: ".openclaw/skills" }; const m = e.match(/^join\(([\s\S]+)\)$/); if (!m) throw new Error(`Unparseable globalSkillsDir: ${expr}`); const args = m[1]!.split(",").map((a) => a.trim()); const first = args[0]!; if (!(BASE_VARS as readonly string[]).includes(first)) { throw new Error(`globalSkillsDir does not start with a known base var: ${expr}`); } const segs: string[] = []; for (const raw of args.slice(1)) { const lit = raw.match(/^['"]([^'"]*)['"]$/); if (!lit) throw new Error(`Non-literal segment "${raw}" in globalSkillsDir: ${expr}`); segs.push(lit[1]!); } return { base: first as BaseVar, sub: posixJoin(segs) }; } function parseAgents(source: string): AgentGlobalDir[] { const re = /^ {2}(?:"([a-z0-9-]+)"|'([a-z0-9-]+)'|([a-z0-9-]+)):\s*\{/gm; const blocks: { key: string; pos: number }[] = []; let m: RegExpExecArray | null; while ((m = re.exec(source))) blocks.push({ key: m[1] || m[2] || m[3]!, pos: m.index }); if (blocks.length < 60) { throw new Error(`Parsed only ${blocks.length} agent blocks — upstream layout likely changed`); } const out: AgentGlobalDir[] = []; for (let i = 0; i < blocks.length; i++) { const seg = source.slice(blocks[i]!.pos, blocks[i + 1] ? blocks[i + 1]!.pos : source.length); const gd = seg.match(/globalSkillsDir:\s*([\s\S]+?),\n/); const expr = gd ? gd[1]!.trim().replace(/\s+/g, " ") : "undefined"; const resolved = resolveGlobalExpr(expr); if (resolved === null) continue; // no global dir (e.g. eve, promptscript) out.push({ agent: blocks[i]!.key, base: resolved.base, sub: resolved.sub }); } return out; } function render(rows: AgentGlobalDir[]): string { const lines = rows.map( (r) => ` { agent: ${JSON.stringify(r.agent)}, base: ${JSON.stringify(r.base)}, sub: ${JSON.stringify(r.sub)} },`, ); return `// @generated by packages/cli/scripts/sync-agent-dirs.ts — DO NOT EDIT. // Source: ${SKILLS_REPO}@${SKILLS_REF} (src/agents.ts). Regenerate with: // bun run --cwd packages/cli gen:agent-dirs // // Each entry is one agent the upstream \`skills\` CLI installs to. The agent's // GLOBAL skills directory is \`join(, )\`, where \`base\` is one of the // env-overridable home dirs below (resolved at runtime by skillsMirror.ts, so // XDG_CONFIG_HOME / CODEX_HOME / CLAUDE_CONFIG_DIR are honored). Agents with no // global skills dir upstream (eve, promptscript) are omitted. /** Env-overridable base dirs, matching upstream agents.ts. */ export type AgentDirBase = ${BASE_VARS.map((b) => ` | ${JSON.stringify(b)}`).join("\n")}; export interface AgentGlobalDir { /** Upstream agent key. */ agent: string; /** Base directory the global skills dir is rooted at. */ base: AgentDirBase; /** POSIX suffix joined onto the resolved base. */ sub: string; } export const AGENT_GLOBAL_DIRS: readonly AgentGlobalDir[] = [ ${lines.join("\n")} ]; `; } async function loadAgentsSource(): Promise { if (srcPath) return readFileSync(srcPath, "utf8"); const res = await fetch(AGENTS_TS_URL); if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${AGENTS_TS_URL}`); return res.text(); } const source = await loadAgentsSource(); const rows = parseAgents(source); const next = render(rows); if (isCheck) { let current = ""; try { current = readFileSync(outPath, "utf8"); } catch { /* missing → drift */ } if (current !== next) { console.error( `agentDirs.generated.ts is out of date (parsed ${rows.length} agents from ${SKILLS_REPO}@${SKILLS_REF}).\n` + `Run: bun run --cwd packages/cli gen:agent-dirs`, ); process.exit(1); } console.log(`agentDirs.generated.ts is up to date (${rows.length} agents).`); } else { writeFileSync(outPath, next, "utf8"); console.log(`Wrote ${rows.length} agents → ${outPath}`); }