discovery.ts 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. import path from "path"
  2. import { mkdir } from "fs/promises"
  3. import { Log } from "../util/log"
  4. import { Global } from "../global"
  5. export namespace Discovery {
  6. const log = Log.create({ service: "skill-discovery" })
  7. type Index = {
  8. skills: Array<{
  9. name: string
  10. description: string
  11. files: string[]
  12. }>
  13. }
  14. export function dir() {
  15. return path.join(Global.Path.cache, "skills")
  16. }
  17. async function get(url: string, dest: string): Promise<boolean> {
  18. if (await Bun.file(dest).exists()) return true
  19. return fetch(url)
  20. .then(async (response) => {
  21. if (!response.ok) {
  22. log.error("failed to download", { url, status: response.status })
  23. return false
  24. }
  25. await Bun.write(dest, await response.text())
  26. return true
  27. })
  28. .catch((err) => {
  29. log.error("failed to download", { url, err })
  30. return false
  31. })
  32. }
  33. export async function pull(url: string): Promise<string[]> {
  34. const result: string[] = []
  35. const base = url.endsWith("/") ? url : `${url}/`
  36. const index = new URL("index.json", base).href
  37. const cache = dir()
  38. const host = base.slice(0, -1)
  39. log.info("fetching index", { url: index })
  40. const data = await fetch(index)
  41. .then(async (response) => {
  42. if (!response.ok) {
  43. log.error("failed to fetch index", { url: index, status: response.status })
  44. return undefined
  45. }
  46. return response
  47. .json()
  48. .then((json) => json as Index)
  49. .catch((err) => {
  50. log.error("failed to parse index", { url: index, err })
  51. return undefined
  52. })
  53. })
  54. .catch((err) => {
  55. log.error("failed to fetch index", { url: index, err })
  56. return undefined
  57. })
  58. if (!data?.skills || !Array.isArray(data.skills)) {
  59. log.warn("invalid index format", { url: index })
  60. return result
  61. }
  62. const list = data.skills.filter((skill) => {
  63. if (!skill?.name || !Array.isArray(skill.files)) {
  64. log.warn("invalid skill entry", { url: index, skill })
  65. return false
  66. }
  67. return true
  68. })
  69. await Promise.all(
  70. list.map(async (skill) => {
  71. const root = path.join(cache, skill.name)
  72. await Promise.all(
  73. skill.files.map(async (file) => {
  74. const link = new URL(file, `${host}/${skill.name}/`).href
  75. const dest = path.join(root, file)
  76. await mkdir(path.dirname(dest), { recursive: true })
  77. await get(link, dest)
  78. }),
  79. )
  80. const md = path.join(root, "SKILL.md")
  81. if (await Bun.file(md).exists()) result.push(root)
  82. }),
  83. )
  84. return result
  85. }
  86. }