host.ts 16 KB

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