host.ts 14 KB

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