host.ts 16 KB

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