host.ts 12 KB

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