From 266de27a0b56c29a3bdb81a5adb211f93214f5a8 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 6 Feb 2026 00:29:43 -0500 Subject: [PATCH] feat(skill): add skill discovery from URLs via well-known RFC Implement the Agent Skills Discovery RFC to allow fetching skills from URLs: - Add 'urls' field to config.skills for specifying skill registry URLs - Create Discovery namespace in skill/discovery.ts with pull() function - Download skills from /.well-known/skills/index.json endpoints - Cache downloaded skills to ~/.cache/opencode/skills/ - Skip re-downloading existing files for efficiency Users can now configure: { "skills": { "urls": ["https://example.com/.well-known/skills/"] } } Implements: https://github.com/cloudflare/agent-skills-discovery-rfc --- packages/opencode/src/config/config.ts | 4 ++ packages/opencode/src/skill/discovery.ts | 80 ++++++++++++++++++++++++ packages/opencode/src/skill/skill.ts | 17 +++++ 3 files changed, 101 insertions(+) create mode 100644 packages/opencode/src/skill/discovery.ts diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index c56bd6c78..6dd0592d5 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -660,6 +660,10 @@ export namespace Config { export const Skills = z.object({ paths: z.array(z.string()).optional().describe("Additional paths to skill folders"), + urls: z + .array(z.string()) + .optional() + .describe("URLs to fetch skills from (e.g., https://example.com/.well-known/skills/)"), }) export type Skills = z.infer diff --git a/packages/opencode/src/skill/discovery.ts b/packages/opencode/src/skill/discovery.ts new file mode 100644 index 000000000..12b3cb3f7 --- /dev/null +++ b/packages/opencode/src/skill/discovery.ts @@ -0,0 +1,80 @@ +import path from "path" +import { mkdir } from "fs/promises" +import { Log } from "../util/log" +import { Global } from "@/global" + +export namespace Discovery { + const log = Log.create({ service: "skill-discovery" }) + + type Index = { + skills: Array<{ + name: string + description: string + files: string[] + }> + } + + export function dir() { + return path.join(Global.Path.cache, "skills") + } + + async function get(url: string, dest: string): Promise { + if (await Bun.file(dest).exists()) return true + try { + const response = await fetch(url) + if (!response.ok) { + log.error("failed to download", { url, status: response.status }) + return false + } + const content = await response.text() + await Bun.write(dest, content) + return true + } catch (err) { + log.error("failed to download", { url, err }) + return false + } + } + + export async function pull(url: string): Promise { + const result: string[] = [] + const indexUrl = new URL("index.json", url.endsWith("/") ? url : `${url}/`).href + const cacheDir = dir() + + try { + log.info("fetching index", { url: indexUrl }) + const response = await fetch(indexUrl) + if (!response.ok) { + log.error("failed to fetch index", { url: indexUrl, status: response.status }) + return result + } + + const index = (await response.json()) as Index + if (!index.skills || !Array.isArray(index.skills)) { + log.warn("invalid index format", { url: indexUrl }) + return result + } + + for (const skill of index.skills) { + if (!skill.name || !skill.files || !Array.isArray(skill.files)) { + log.warn("invalid skill entry", { url: indexUrl, skill }) + continue + } + + const skillDir = path.join(cacheDir, skill.name) + for (const file of skill.files) { + const fileUrl = new URL(file, `${url.replace(/\/$/, "")}/${skill.name}/`).href + const localPath = path.join(skillDir, file) + await mkdir(path.dirname(localPath), { recursive: true }) + await get(fileUrl, localPath) + } + + const skillMd = path.join(skillDir, "SKILL.md") + if (await Bun.file(skillMd).exists()) result.push(skillDir) + } + } catch (err) { + log.error("failed to fetch from URL", { url, err }) + } + + return result + } +} diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index b4f4acd52..b8eb64250 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -11,6 +11,7 @@ import { Filesystem } from "@/util/filesystem" import { Flag } from "@/flag/flag" import { Bus } from "@/bus" import { Session } from "@/session" +import { Discovery } from "./discovery" export namespace Skill { const log = Log.create({ service: "skill" }) @@ -151,6 +152,22 @@ export namespace Skill { } } + // Download and load skills from URLs + for (const skillUrl of config.skills?.urls ?? []) { + const downloadedDirs = await Discovery.pull(skillUrl) + for (const dir of downloadedDirs) { + dirs.add(dir) + for await (const match of SKILL_GLOB.scan({ + cwd: dir, + absolute: true, + onlyFiles: true, + followSymlinks: true, + })) { + await addSkill(match) + } + } + } + return { skills, dirs: Array.from(dirs),