discovery.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import { NodeFileSystem, NodePath } from "@effect/platform-node"
  2. import { Effect, FileSystem, Layer, Path, Schema, ServiceMap } from "effect"
  3. import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
  4. import { withTransientReadRetry } from "@/util/effect-http-client"
  5. import { Global } from "../global"
  6. import { Log } from "../util/log"
  7. export namespace Discovery {
  8. const skillConcurrency = 4
  9. const fileConcurrency = 8
  10. class IndexSkill extends Schema.Class<IndexSkill>("IndexSkill")({
  11. name: Schema.String,
  12. files: Schema.Array(Schema.String),
  13. }) {}
  14. class Index extends Schema.Class<Index>("Index")({
  15. skills: Schema.Array(IndexSkill),
  16. }) {}
  17. export interface Interface {
  18. readonly pull: (url: string) => Effect.Effect<string[]>
  19. }
  20. export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SkillDiscovery") {}
  21. export const layer: Layer.Layer<Service, never, FileSystem.FileSystem | Path.Path | HttpClient.HttpClient> =
  22. Layer.effect(
  23. Service,
  24. Effect.gen(function* () {
  25. const log = Log.create({ service: "skill-discovery" })
  26. const fs = yield* FileSystem.FileSystem
  27. const path = yield* Path.Path
  28. const http = HttpClient.filterStatusOk(withTransientReadRetry(yield* HttpClient.HttpClient))
  29. const cache = path.join(Global.Path.cache, "skills")
  30. const download = Effect.fn("Discovery.download")(function* (url: string, dest: string) {
  31. if (yield* fs.exists(dest).pipe(Effect.orDie)) return true
  32. return yield* HttpClientRequest.get(url).pipe(
  33. http.execute,
  34. Effect.flatMap((res) => res.arrayBuffer),
  35. Effect.flatMap((body) =>
  36. fs
  37. .makeDirectory(path.dirname(dest), { recursive: true })
  38. .pipe(Effect.flatMap(() => fs.writeFile(dest, new Uint8Array(body)))),
  39. ),
  40. Effect.as(true),
  41. Effect.catch((err) =>
  42. Effect.sync(() => {
  43. log.error("failed to download", { url, err })
  44. return false
  45. }),
  46. ),
  47. )
  48. })
  49. const pull = Effect.fn("Discovery.pull")(function* (url: string) {
  50. const base = url.endsWith("/") ? url : `${url}/`
  51. const index = new URL("index.json", base).href
  52. const host = base.slice(0, -1)
  53. log.info("fetching index", { url: index })
  54. const data = yield* HttpClientRequest.get(index).pipe(
  55. HttpClientRequest.acceptJson,
  56. http.execute,
  57. Effect.flatMap(HttpClientResponse.schemaBodyJson(Index)),
  58. Effect.catch((err) =>
  59. Effect.sync(() => {
  60. log.error("failed to fetch index", { url: index, err })
  61. return null
  62. }),
  63. ),
  64. )
  65. if (!data) return []
  66. const list = data.skills.filter((skill) => {
  67. if (!skill.files.includes("SKILL.md")) {
  68. log.warn("skill entry missing SKILL.md", { url: index, skill: skill.name })
  69. return false
  70. }
  71. return true
  72. })
  73. const dirs = yield* Effect.forEach(
  74. list,
  75. (skill) =>
  76. Effect.gen(function* () {
  77. const root = path.join(cache, skill.name)
  78. yield* Effect.forEach(
  79. skill.files,
  80. (file) => download(new URL(file, `${host}/${skill.name}/`).href, path.join(root, file)),
  81. {
  82. concurrency: fileConcurrency,
  83. },
  84. )
  85. const md = path.join(root, "SKILL.md")
  86. return (yield* fs.exists(md).pipe(Effect.orDie)) ? root : null
  87. }),
  88. { concurrency: skillConcurrency },
  89. )
  90. return dirs.filter((dir): dir is string => dir !== null)
  91. })
  92. return Service.of({ pull })
  93. }),
  94. )
  95. export const defaultLayer: Layer.Layer<Service> = layer.pipe(
  96. Layer.provide(FetchHttpClient.layer),
  97. Layer.provide(NodeFileSystem.layer),
  98. Layer.provide(NodePath.layer),
  99. )
  100. }