host.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. import type { AISDKHooks, PluginHost } from "@opencode-ai/plugin/v2/effect"
  2. import { AgentV2 } from "@opencode-ai/core/agent"
  3. import { Catalog } from "@opencode-ai/core/catalog"
  4. import { Integration } from "@opencode-ai/core/integration"
  5. import { ModelV2 } from "@opencode-ai/core/model"
  6. import { PluginV2 } from "@opencode-ai/core/plugin"
  7. import { ProviderV2 } from "@opencode-ai/core/provider"
  8. import type { IntegrationEnvMethod, IntegrationKeyMethod, IntegrationOAuthMethod } from "@opencode-ai/sdk/v2/types"
  9. import { Effect, Stream } from "effect"
  10. export function host(overrides: Partial<PluginHost> = {}): PluginHost {
  11. return {
  12. aisdk: {
  13. hook: () => Effect.die("unused aisdk.hook"),
  14. },
  15. agent: {
  16. get: () => Effect.die("unused agent.get"),
  17. default: () => Effect.die("unused agent.default"),
  18. list: () => Effect.die("unused agent.list"),
  19. rebuild: () => Effect.die("unused agent.rebuild"),
  20. transform: () => Effect.die("unused agent.transform"),
  21. },
  22. catalog: {
  23. provider: {
  24. get: () => Effect.die("unused catalog.provider.get"),
  25. list: () => Effect.die("unused catalog.provider.list"),
  26. available: () => Effect.die("unused catalog.provider.available"),
  27. },
  28. model: {
  29. get: () => Effect.die("unused catalog.model.get"),
  30. list: () => Effect.die("unused catalog.model.list"),
  31. available: () => Effect.die("unused catalog.model.available"),
  32. default: () => Effect.die("unused catalog.model.default"),
  33. small: () => Effect.die("unused catalog.model.small"),
  34. },
  35. rebuild: () => Effect.die("unused catalog.rebuild"),
  36. transform: () => Effect.die("unused catalog.transform"),
  37. },
  38. command: {
  39. get: () => Effect.die("unused command.get"),
  40. list: () => Effect.die("unused command.list"),
  41. rebuild: () => Effect.die("unused command.rebuild"),
  42. transform: () => Effect.die("unused command.transform"),
  43. },
  44. event: {
  45. subscribe: () => Stream.die("unused event.subscribe"),
  46. },
  47. filesystem: {
  48. read: () => Effect.die("unused filesystem.read"),
  49. list: () => Effect.die("unused filesystem.list"),
  50. find: () => Effect.die("unused filesystem.find"),
  51. glob: () => Effect.die("unused filesystem.glob"),
  52. },
  53. integration: {
  54. get: () => Effect.die("unused integration.get"),
  55. list: () => Effect.die("unused integration.list"),
  56. rebuild: () => Effect.die("unused integration.rebuild"),
  57. transform: () => Effect.die("unused integration.transform"),
  58. },
  59. location: {
  60. directory: "/unused/location",
  61. project: { directory: "/unused/project" },
  62. },
  63. npm: {
  64. add: () => Effect.die("unused npm.add"),
  65. },
  66. path: {
  67. home: "/unused/home",
  68. data: "/unused/data",
  69. cache: "/unused/cache",
  70. config: "/unused/config",
  71. state: "/unused/state",
  72. temp: "/unused/temp",
  73. },
  74. reference: {
  75. list: () => Effect.die("unused reference.list"),
  76. rebuild: () => Effect.die("unused reference.rebuild"),
  77. transform: () => Effect.die("unused reference.transform"),
  78. },
  79. skill: {
  80. sources: () => Effect.die("unused skill.sources"),
  81. list: () => Effect.die("unused skill.list"),
  82. rebuild: () => Effect.die("unused skill.rebuild"),
  83. transform: () => Effect.die("unused skill.transform"),
  84. },
  85. ...overrides,
  86. }
  87. }
  88. export function aisdkHost(plugin: PluginV2.Interface): PluginHost["aisdk"] {
  89. return {
  90. hook: (name, callback) => {
  91. if (name === "sdk") {
  92. const run = callback as AISDKHooks["sdk"]
  93. return plugin.hook("aisdk.sdk", (event) => {
  94. const output = { ...event }
  95. const result = run(output)
  96. return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
  97. Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
  98. )
  99. })
  100. }
  101. const run = callback as AISDKHooks["language"]
  102. return plugin.hook("aisdk.language", (event) => {
  103. const output = { ...event }
  104. const result = run(output)
  105. return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
  106. Effect.tap(() => Effect.sync(() => (event.language = output.language))),
  107. )
  108. })
  109. },
  110. }
  111. }
  112. export function agentHost(agent: AgentV2.Interface): PluginHost["agent"] {
  113. return {
  114. ...host().agent,
  115. transform: (callback) =>
  116. agent.transform((draft) =>
  117. callback({
  118. list: () => draft.list().map(agentInfo),
  119. get: (id) => {
  120. const value = draft.get(AgentV2.ID.make(id))
  121. return value && agentInfo(value)
  122. },
  123. default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)),
  124. update: (id, update) =>
  125. draft.update(AgentV2.ID.make(id), (value) => {
  126. const current = agentInfo(value)
  127. update(current)
  128. Object.assign(value, current, { id: AgentV2.ID.make(current.id) })
  129. }),
  130. remove: (id) => draft.remove(AgentV2.ID.make(id)),
  131. }),
  132. ),
  133. }
  134. }
  135. export function catalogHost(catalog: Catalog.Interface): PluginHost["catalog"] {
  136. return {
  137. ...host().catalog,
  138. rebuild: catalog.rebuild,
  139. transform: (callback) =>
  140. catalog.transform((draft) =>
  141. callback({
  142. provider: {
  143. list: () =>
  144. draft.provider.list().map((value) => ({
  145. provider: providerInfo(value.provider),
  146. models: new Map(Array.from(value.models, ([id, model]) => [id, modelInfo(model)])),
  147. })),
  148. get: (id) => {
  149. const value = draft.provider.get(ProviderV2.ID.make(id))
  150. return (
  151. value && {
  152. provider: providerInfo(value.provider),
  153. models: new Map(Array.from(value.models, ([id, model]) => [id, modelInfo(model)])),
  154. }
  155. )
  156. },
  157. update: (id, update) =>
  158. draft.provider.update(ProviderV2.ID.make(id), (value) => {
  159. const current = providerInfo(value)
  160. update(current)
  161. Object.assign(value, current, { id: ProviderV2.ID.make(current.id) })
  162. }),
  163. remove: (id) => draft.provider.remove(ProviderV2.ID.make(id)),
  164. },
  165. model: {
  166. get: (providerID, modelID) => {
  167. const value = draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID))
  168. return value && modelInfo(value)
  169. },
  170. update: (providerID, modelID, update) =>
  171. draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), (value) => {
  172. const current = modelInfo(value)
  173. update(current)
  174. Object.assign(value, current, {
  175. id: ModelV2.ID.make(current.id),
  176. providerID: ProviderV2.ID.make(current.providerID),
  177. family: current.family === undefined ? undefined : ModelV2.Family.make(current.family),
  178. variants: current.variants.map((variant) => ({
  179. ...variant,
  180. id: ModelV2.VariantID.make(variant.id),
  181. })),
  182. })
  183. }),
  184. remove: (providerID, modelID) =>
  185. draft.model.remove(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
  186. default: {
  187. get: () => {
  188. const value = draft.model.default.get()
  189. return value && { providerID: value.providerID, modelID: value.modelID }
  190. },
  191. set: (providerID, modelID) =>
  192. draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
  193. },
  194. },
  195. }),
  196. ),
  197. }
  198. }
  199. export function integrationHost(integration: Integration.Interface): PluginHost["integration"] {
  200. const info = (value: Integration.Info) => ({
  201. id: value.id,
  202. name: value.name,
  203. methods: value.methods.map(method),
  204. connections: value.connections.map((item) => ({ ...item })),
  205. })
  206. return {
  207. get: (id) => integration.get(Integration.ID.make(id)).pipe(Effect.map((value) => value && info(value))),
  208. list: () => integration.list().pipe(Effect.map((items) => items.map(info))),
  209. rebuild: integration.rebuild,
  210. transform: (callback) =>
  211. integration.transform((draft) =>
  212. callback({
  213. list: () => draft.list().map((value) => ({ id: value.id, name: value.name })),
  214. get: (id) => {
  215. const value = draft.get(Integration.ID.make(id))
  216. return value && { id: value.id, name: value.name }
  217. },
  218. update: (id, update) => draft.update(Integration.ID.make(id), update),
  219. remove: (id) => draft.remove(Integration.ID.make(id)),
  220. method: {
  221. list: (id) => draft.method.list(Integration.ID.make(id)).map(method),
  222. update: (input) =>
  223. input.method.type === "env"
  224. ? draft.method.update({
  225. integrationID: Integration.ID.make(input.integrationID),
  226. method: { ...input.method, names: [...input.method.names] },
  227. })
  228. : draft.method.update({
  229. integrationID: Integration.ID.make(input.integrationID),
  230. method: input.method,
  231. }),
  232. remove: (id, item) => draft.method.remove(Integration.ID.make(id), internalMethod(item)),
  233. },
  234. }),
  235. ),
  236. }
  237. }
  238. function method(value: Integration.Method) {
  239. if (value.type === "env") return { type: value.type, names: [...value.names] }
  240. if (value.type === "key") return { type: value.type, label: value.label }
  241. return {
  242. type: value.type,
  243. id: value.id,
  244. label: value.label,
  245. prompts: value.prompts?.map((prompt) => {
  246. if (prompt.type === "text") return { ...prompt }
  247. return { ...prompt, options: prompt.options.map((option) => ({ ...option })) }
  248. }),
  249. }
  250. }
  251. function internalMethod(
  252. value: IntegrationOAuthMethod | IntegrationKeyMethod | IntegrationEnvMethod,
  253. ): Integration.Method {
  254. if (value.type === "env") return value
  255. if (value.type === "key") return value
  256. return {
  257. ...value,
  258. id: Integration.MethodID.make(value.id),
  259. }
  260. }
  261. function agentInfo(value: AgentV2.Info) {
  262. return {
  263. ...value,
  264. model: value.model && { ...value.model },
  265. request: { headers: { ...value.request.headers }, body: { ...value.request.body } },
  266. permissions: value.permissions.map((permission) => ({ ...permission })),
  267. }
  268. }
  269. function providerInfo(value: ProviderV2.MutableInfo) {
  270. return {
  271. ...value,
  272. api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
  273. request: { headers: { ...value.request.headers }, body: { ...value.request.body } },
  274. }
  275. }
  276. function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) {
  277. return {
  278. ...value,
  279. api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
  280. capabilities: {
  281. ...value.capabilities,
  282. input: [...value.capabilities.input],
  283. output: [...value.capabilities.output],
  284. },
  285. request: {
  286. ...value.request,
  287. headers: { ...value.request.headers },
  288. body: { ...value.request.body },
  289. generation: value.request.generation && {
  290. ...value.request.generation,
  291. stop: value.request.generation.stop && [...value.request.generation.stop],
  292. },
  293. options: value.request.options && { ...value.request.options },
  294. },
  295. variants: value.variants.map((variant) => ({
  296. ...variant,
  297. headers: { ...variant.headers },
  298. body: { ...variant.body },
  299. generation: variant.generation && {
  300. ...variant.generation,
  301. stop: variant.generation.stop && [...variant.generation.stop],
  302. },
  303. options: variant.options && { ...variant.options },
  304. })),
  305. time: { ...value.time },
  306. cost: value.cost.map((cost) => ({ ...cost, tier: cost.tier && { ...cost.tier }, cache: { ...cost.cache } })),
  307. limit: { ...value.limit },
  308. }
  309. }