sdkClient.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. /**
  2. * OpenCode SDK client instance
  3. * Configured to connect to the OpenCode server at the default location.
  4. *
  5. * When `window.__OPENCODE_SERVER_URL__` is set (e.g. injected by the VS Code
  6. * gui-only plugin), all API requests target that absolute URL. When absent,
  7. * relative URLs are used — identical to the original behaviour.
  8. */
  9. import { createOpencodeClient, type Provider } from "@opencode-ai/sdk/client"
  10. import { ideBridge } from "../ideBridge"
  11. export const serverBase: string =
  12. ((globalThis as any).__OPENCODE_SERVER_URL__ as string | undefined)?.replace(/\/$/, "") || ""
  13. const baseClient = createOpencodeClient({ baseUrl: serverBase || "/" })
  14. interface ProvidersResponse {
  15. providers: Provider[]
  16. default: Record<string, string>
  17. }
  18. interface ModelEntry {
  19. providerID: string
  20. modelID: string
  21. }
  22. interface ModelPreferences {
  23. recent: ModelEntry[]
  24. favorite: ModelEntry[]
  25. variant?: Record<string, string>
  26. }
  27. interface PathResponse {
  28. state: string
  29. config: string
  30. worktree: string
  31. directory: string
  32. }
  33. /**
  34. * Extended SDK client with state management methods
  35. * TODO: Remove once SDK is regenerated with Stainless
  36. */
  37. export const sdk = {
  38. ...baseClient,
  39. session: Object.assign(baseClient.session, {
  40. retry: async (options: { path: { sessionID: string } }) => {
  41. try {
  42. const response = await fetch(`${serverBase}/app/api/session/${options.path.sessionID}/retry`, {
  43. method: "POST",
  44. headers: { "Content-Type": "application/json" },
  45. })
  46. if (!response.ok) {
  47. return { error: { message: "Failed to retry session" }, data: null }
  48. }
  49. const data = await response.json()
  50. return { data, error: null }
  51. } catch (error) {
  52. return {
  53. error: { message: error instanceof Error ? error.message : "Unknown error" },
  54. data: null,
  55. }
  56. }
  57. },
  58. }) as (typeof baseClient.session) & {
  59. retry: (options: { path: { sessionID: string } }) => Promise<any>
  60. },
  61. config: {
  62. get: baseClient.config.get.bind(baseClient.config),
  63. update: baseClient.config.update.bind(baseClient.config),
  64. providers: baseClient.config.providers.bind(baseClient.config),
  65. allProviders: async () => {
  66. try {
  67. const response = await fetch(`${serverBase}/app/api/config/providers`, {
  68. method: "GET",
  69. headers: { "Content-Type": "application/json" },
  70. })
  71. if (!response.ok) {
  72. return { error: { message: "Failed to load providers" }, data: null as ProvidersResponse | null }
  73. }
  74. const data = (await response.json()) as ProvidersResponse
  75. return { data, error: null as { message: string } | null }
  76. } catch (error) {
  77. return {
  78. error: { message: error instanceof Error ? error.message : "Unknown error" },
  79. data: null as ProvidersResponse | null,
  80. }
  81. }
  82. },
  83. },
  84. path: {
  85. get: async () => {
  86. try {
  87. const response = await fetch(`${serverBase}/path`, {
  88. method: "GET",
  89. headers: { "Content-Type": "application/json" },
  90. })
  91. if (!response.ok) {
  92. return {
  93. error: { message: "Failed to fetch path" },
  94. data: null as PathResponse | null,
  95. }
  96. }
  97. const data = (await response.json()) as PathResponse
  98. return { data, error: null as { message: string } | null }
  99. } catch (error) {
  100. return {
  101. error: { message: error instanceof Error ? error.message : "Unknown error" },
  102. data: null as PathResponse | null,
  103. }
  104. }
  105. },
  106. },
  107. auth: {
  108. set: async (provider: string, value: any) => {
  109. const res = await fetch(`${serverBase}/app/api/auth/set`, {
  110. method: "POST",
  111. headers: { "Content-Type": "application/json" },
  112. body: JSON.stringify({ provider, value }),
  113. })
  114. if (!res.ok) throw new Error(await res.text())
  115. },
  116. list: async () => {
  117. const res = await fetch(`${serverBase}/app/api/auth/list`)
  118. return res.json() as Promise<Record<string, any>>
  119. },
  120. remove: async (provider: string) => {
  121. await fetch(`${serverBase}/app/api/auth/remove`, {
  122. method: "POST",
  123. headers: { "Content-Type": "application/json" },
  124. body: JSON.stringify({ provider }),
  125. })
  126. },
  127. methods: async (provider: string) => {
  128. const res = await fetch(`${serverBase}/app/api/auth/methods?provider=${provider}`)
  129. return res.json() as Promise<
  130. Array<{
  131. label: string
  132. type: "oauth" | "api"
  133. prompts?: any[]
  134. }>
  135. >
  136. },
  137. start: async (provider: string, methodIndex: number, inputs: any) => {
  138. const res = await fetch(`${serverBase}/app/api/auth/login/start`, {
  139. method: "POST",
  140. headers: { "Content-Type": "application/json" },
  141. body: JSON.stringify({ provider, methodIndex, inputs }),
  142. })
  143. if (!res.ok) throw new Error(await res.text())
  144. return res.json() as Promise<{ id: string; url?: string; method: "auto" | "code"; instructions?: string }>
  145. },
  146. submit: async (id: string, code: string) => {
  147. const res = await fetch(`${serverBase}/app/api/auth/login/submit`, {
  148. method: "POST",
  149. headers: { "Content-Type": "application/json" },
  150. body: JSON.stringify({ id, code }),
  151. })
  152. if (!res.ok) throw new Error(await res.text())
  153. return res.json() as Promise<boolean>
  154. },
  155. status: async (id: string) => {
  156. const res = await fetch(`${serverBase}/app/api/auth/login/status/${id}`)
  157. return res.json() as Promise<{ status: "pending" | "success" | "failed"; result?: any }>
  158. },
  159. },
  160. permissions: {
  161. respond: async (options: {
  162. path: { requestID: string }
  163. body: { reply: "once" | "always" | "reject"; message?: string }
  164. }) => {
  165. const response = await fetch(`${serverBase}/permission/${options.path.requestID}/reply`, {
  166. method: "POST",
  167. headers: { "Content-Type": "application/json" },
  168. body: JSON.stringify(options.body),
  169. })
  170. if (!response.ok) {
  171. return { error: { message: "Failed to respond to permission" }, data: null }
  172. }
  173. const data = await response.json()
  174. return { data, error: null }
  175. },
  176. },
  177. question: {
  178. reply: async (options: { requestID: string; answers: Array<Array<string>> }) => {
  179. const response = await fetch(`${serverBase}/question/${options.requestID}/reply`, {
  180. method: "POST",
  181. headers: { "Content-Type": "application/json" },
  182. body: JSON.stringify({ answers: options.answers }),
  183. })
  184. if (!response.ok) {
  185. return { error: { message: "Failed to reply to question" }, data: null }
  186. }
  187. const data = await response.json()
  188. return { data, error: null }
  189. },
  190. reject: async (options: { requestID: string }) => {
  191. const response = await fetch(`${serverBase}/question/${options.requestID}/reject`, {
  192. method: "POST",
  193. headers: { "Content-Type": "application/json" },
  194. })
  195. if (!response.ok) {
  196. return { error: { message: "Failed to reject question" }, data: null }
  197. }
  198. const data = await response.json()
  199. return { data, error: null }
  200. },
  201. },
  202. model: {
  203. get: async () => {
  204. if (!ideBridge.isInstalled()) return { data: { recent: [], favorite: [], variant: {} } as ModelPreferences, error: null as { message: string } | null }
  205. try {
  206. const res = await ideBridge.request("model.get")
  207. return { data: (res.payload ?? { recent: [], favorite: [], variant: {} }) as ModelPreferences, error: null as { message: string } | null }
  208. } catch (error) {
  209. return { error: { message: error instanceof Error ? error.message : "Unknown error" }, data: null as ModelPreferences | null }
  210. }
  211. },
  212. update: async (options: { body: Partial<ModelPreferences> }) => {
  213. if (!ideBridge.isInstalled()) return { data: null as ModelPreferences | null, error: { message: "IdeBridge not available" } }
  214. try {
  215. const res = await ideBridge.request("model.update", options.body)
  216. return { data: (res.payload ?? { recent: [], favorite: [], variant: {} }) as ModelPreferences, error: null as { message: string } | null }
  217. } catch (error) {
  218. return { error: { message: error instanceof Error ? error.message : "Unknown error" }, data: null as ModelPreferences | null }
  219. }
  220. },
  221. },
  222. kv: {
  223. get: async () => {
  224. if (!ideBridge.isInstalled()) return { data: {} as Record<string, any>, error: null as { message: string } | null }
  225. try {
  226. const res = await ideBridge.request("kv.get")
  227. return { data: (res.payload ?? {}) as Record<string, any>, error: null as { message: string } | null }
  228. } catch (error) {
  229. return { error: { message: error instanceof Error ? error.message : "Unknown error" }, data: null as Record<string, any> | null }
  230. }
  231. },
  232. update: async (options: { body: Record<string, any> }) => {
  233. if (!ideBridge.isInstalled()) return { data: null as Record<string, any> | null, error: { message: "IdeBridge not available" } }
  234. try {
  235. const res = await ideBridge.request("kv.update", options.body)
  236. return { data: (res.payload ?? {}) as Record<string, any>, error: null as { message: string } | null }
  237. } catch (error) {
  238. return { error: { message: error instanceof Error ? error.message : "Unknown error" }, data: null as Record<string, any> | null }
  239. }
  240. },
  241. },
  242. }