host.ts 14 KB

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