host.ts 12 KB

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