summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/config/config.ts4
-rw-r--r--packages/opencode/src/skill/discovery.ts80
-rw-r--r--packages/opencode/src/skill/skill.ts17
3 files changed, 101 insertions, 0 deletions
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<typeof Skills>
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<boolean> {
+ 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<string[]> {
+ 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),