chad-code 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. package/AGENTS.md +27 -0
  2. package/Dockerfile +18 -0
  3. package/README.md +15 -0
  4. package/README.npm.md +64 -0
  5. package/bin/chad-code +84 -0
  6. package/bunfig.toml +7 -0
  7. package/eslint.config.js +29 -0
  8. package/package.json +107 -0
  9. package/parsers-config.ts +253 -0
  10. package/script/build.ts +167 -0
  11. package/script/postinstall.mjs +122 -0
  12. package/script/publish-registries.ts +187 -0
  13. package/script/publish.ts +93 -0
  14. package/script/schema.ts +47 -0
  15. package/src/acp/README.md +164 -0
  16. package/src/acp/agent.ts +1086 -0
  17. package/src/acp/session.ts +101 -0
  18. package/src/acp/types.ts +22 -0
  19. package/src/agent/agent.ts +253 -0
  20. package/src/agent/generate.txt +75 -0
  21. package/src/agent/prompt/compaction.txt +12 -0
  22. package/src/agent/prompt/explore.txt +18 -0
  23. package/src/agent/prompt/summary.txt +11 -0
  24. package/src/agent/prompt/title.txt +36 -0
  25. package/src/auth/index.ts +70 -0
  26. package/src/bun/index.ts +130 -0
  27. package/src/bus/bus-event.ts +43 -0
  28. package/src/bus/global.ts +10 -0
  29. package/src/bus/index.ts +105 -0
  30. package/src/cli/bootstrap.ts +17 -0
  31. package/src/cli/cmd/acp.ts +69 -0
  32. package/src/cli/cmd/agent.ts +257 -0
  33. package/src/cli/cmd/auth.ts +132 -0
  34. package/src/cli/cmd/cmd.ts +7 -0
  35. package/src/cli/cmd/debug/agent.ts +28 -0
  36. package/src/cli/cmd/debug/config.ts +15 -0
  37. package/src/cli/cmd/debug/file.ts +91 -0
  38. package/src/cli/cmd/debug/index.ts +45 -0
  39. package/src/cli/cmd/debug/lsp.ts +48 -0
  40. package/src/cli/cmd/debug/ripgrep.ts +83 -0
  41. package/src/cli/cmd/debug/scrap.ts +15 -0
  42. package/src/cli/cmd/debug/skill.ts +15 -0
  43. package/src/cli/cmd/debug/snapshot.ts +48 -0
  44. package/src/cli/cmd/export.ts +88 -0
  45. package/src/cli/cmd/generate.ts +38 -0
  46. package/src/cli/cmd/github.ts +32 -0
  47. package/src/cli/cmd/import.ts +98 -0
  48. package/src/cli/cmd/mcp.ts +670 -0
  49. package/src/cli/cmd/models.ts +42 -0
  50. package/src/cli/cmd/pr.ts +112 -0
  51. package/src/cli/cmd/run.ts +374 -0
  52. package/src/cli/cmd/serve.ts +16 -0
  53. package/src/cli/cmd/session.ts +135 -0
  54. package/src/cli/cmd/stats.ts +402 -0
  55. package/src/cli/cmd/tui/app.tsx +705 -0
  56. package/src/cli/cmd/tui/attach.ts +32 -0
  57. package/src/cli/cmd/tui/component/border.tsx +21 -0
  58. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  59. package/src/cli/cmd/tui/component/dialog-command.tsx +124 -0
  60. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  61. package/src/cli/cmd/tui/component/dialog-model.tsx +232 -0
  62. package/src/cli/cmd/tui/component/dialog-provider.tsx +228 -0
  63. package/src/cli/cmd/tui/component/dialog-session-list.tsx +115 -0
  64. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  65. package/src/cli/cmd/tui/component/dialog-stash.tsx +86 -0
  66. package/src/cli/cmd/tui/component/dialog-status.tsx +162 -0
  67. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  68. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  69. package/src/cli/cmd/tui/component/did-you-know.tsx +85 -0
  70. package/src/cli/cmd/tui/component/logo.tsx +43 -0
  71. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +654 -0
  72. package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
  73. package/src/cli/cmd/tui/component/prompt/index.tsx +1078 -0
  74. package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
  75. package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
  76. package/src/cli/cmd/tui/component/tips.ts +92 -0
  77. package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
  78. package/src/cli/cmd/tui/context/args.tsx +14 -0
  79. package/src/cli/cmd/tui/context/directory.ts +13 -0
  80. package/src/cli/cmd/tui/context/exit.tsx +23 -0
  81. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  82. package/src/cli/cmd/tui/context/keybind.tsx +101 -0
  83. package/src/cli/cmd/tui/context/kv.tsx +49 -0
  84. package/src/cli/cmd/tui/context/local.tsx +392 -0
  85. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  86. package/src/cli/cmd/tui/context/route.tsx +46 -0
  87. package/src/cli/cmd/tui/context/sdk.tsx +75 -0
  88. package/src/cli/cmd/tui/context/sync.tsx +384 -0
  89. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  90. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  91. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
  92. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  93. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  94. package/src/cli/cmd/tui/context/theme/chad.json +245 -0
  95. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  96. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  97. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  98. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  99. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  100. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  101. package/src/cli/cmd/tui/context/theme/gruvbox.json +95 -0
  102. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  103. package/src/cli/cmd/tui/context/theme/lucent-orng.json +227 -0
  104. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  105. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  106. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  107. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  108. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  109. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  110. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  111. package/src/cli/cmd/tui/context/theme/orng.json +245 -0
  112. package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
  113. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  114. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  115. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  116. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  117. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  118. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  119. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  120. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  121. package/src/cli/cmd/tui/context/theme.tsx +1137 -0
  122. package/src/cli/cmd/tui/event.ts +46 -0
  123. package/src/cli/cmd/tui/routes/home.tsx +138 -0
  124. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
  125. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
  126. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
  127. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  128. package/src/cli/cmd/tui/routes/session/footer.tsx +88 -0
  129. package/src/cli/cmd/tui/routes/session/header.tsx +125 -0
  130. package/src/cli/cmd/tui/routes/session/index.tsx +1814 -0
  131. package/src/cli/cmd/tui/routes/session/permission.tsx +416 -0
  132. package/src/cli/cmd/tui/routes/session/sidebar.tsx +318 -0
  133. package/src/cli/cmd/tui/spawn.ts +48 -0
  134. package/src/cli/cmd/tui/thread.ts +111 -0
  135. package/src/cli/cmd/tui/ui/dialog-alert.tsx +57 -0
  136. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +83 -0
  137. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +204 -0
  138. package/src/cli/cmd/tui/ui/dialog-help.tsx +38 -0
  139. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +77 -0
  140. package/src/cli/cmd/tui/ui/dialog-select.tsx +345 -0
  141. package/src/cli/cmd/tui/ui/dialog.tsx +171 -0
  142. package/src/cli/cmd/tui/ui/link.tsx +28 -0
  143. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  144. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  145. package/src/cli/cmd/tui/util/clipboard.ts +127 -0
  146. package/src/cli/cmd/tui/util/editor.ts +32 -0
  147. package/src/cli/cmd/tui/util/signal.ts +7 -0
  148. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  149. package/src/cli/cmd/tui/util/transcript.ts +98 -0
  150. package/src/cli/cmd/tui/worker.ts +68 -0
  151. package/src/cli/cmd/uninstall.ts +344 -0
  152. package/src/cli/cmd/upgrade.ts +67 -0
  153. package/src/cli/cmd/web.ts +73 -0
  154. package/src/cli/error.ts +56 -0
  155. package/src/cli/network.ts +53 -0
  156. package/src/cli/ui.ts +87 -0
  157. package/src/cli/upgrade.ts +25 -0
  158. package/src/command/index.ts +131 -0
  159. package/src/command/template/initialize.txt +10 -0
  160. package/src/command/template/review.txt +97 -0
  161. package/src/config/config.ts +1124 -0
  162. package/src/config/markdown.ts +41 -0
  163. package/src/env/index.ts +26 -0
  164. package/src/file/ignore.ts +83 -0
  165. package/src/file/index.ts +411 -0
  166. package/src/file/ripgrep.ts +402 -0
  167. package/src/file/time.ts +64 -0
  168. package/src/file/watcher.ts +117 -0
  169. package/src/flag/flag.ts +52 -0
  170. package/src/format/formatter.ts +359 -0
  171. package/src/format/index.ts +137 -0
  172. package/src/global/index.ts +55 -0
  173. package/src/id/id.ts +73 -0
  174. package/src/ide/index.ts +77 -0
  175. package/src/index.ts +159 -0
  176. package/src/installation/index.ts +198 -0
  177. package/src/lsp/client.ts +252 -0
  178. package/src/lsp/index.ts +485 -0
  179. package/src/lsp/language.ts +119 -0
  180. package/src/lsp/server.ts +2023 -0
  181. package/src/mcp/auth.ts +135 -0
  182. package/src/mcp/index.ts +874 -0
  183. package/src/mcp/oauth-callback.ts +200 -0
  184. package/src/mcp/oauth-provider.ts +154 -0
  185. package/src/patch/index.ts +622 -0
  186. package/src/permission/arity.ts +163 -0
  187. package/src/permission/index.ts +210 -0
  188. package/src/permission/next.ts +268 -0
  189. package/src/plugin/index.ts +106 -0
  190. package/src/project/bootstrap.ts +31 -0
  191. package/src/project/instance.ts +78 -0
  192. package/src/project/project.ts +263 -0
  193. package/src/project/state.ts +65 -0
  194. package/src/project/vcs.ts +76 -0
  195. package/src/provider/auth.ts +143 -0
  196. package/src/provider/models-macro.ts +4 -0
  197. package/src/provider/models.ts +77 -0
  198. package/src/provider/provider.ts +516 -0
  199. package/src/provider/transform.ts +114 -0
  200. package/src/pty/index.ts +212 -0
  201. package/src/server/error.ts +36 -0
  202. package/src/server/mdns.ts +57 -0
  203. package/src/server/project.ts +79 -0
  204. package/src/server/server.ts +2866 -0
  205. package/src/server/tui.ts +71 -0
  206. package/src/session/compaction.ts +225 -0
  207. package/src/session/index.ts +469 -0
  208. package/src/session/llm.ts +213 -0
  209. package/src/session/message-v2.ts +742 -0
  210. package/src/session/message.ts +189 -0
  211. package/src/session/processor.ts +402 -0
  212. package/src/session/prompt/anthropic-20250930.txt +166 -0
  213. package/src/session/prompt/anthropic.txt +105 -0
  214. package/src/session/prompt/anthropic_spoof.txt +1 -0
  215. package/src/session/prompt/beast.txt +147 -0
  216. package/src/session/prompt/build-switch.txt +5 -0
  217. package/src/session/prompt/codex.txt +318 -0
  218. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  219. package/src/session/prompt/gemini.txt +155 -0
  220. package/src/session/prompt/max-steps.txt +16 -0
  221. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  222. package/src/session/prompt/plan.txt +26 -0
  223. package/src/session/prompt/qwen.txt +109 -0
  224. package/src/session/prompt.ts +1621 -0
  225. package/src/session/retry.ts +90 -0
  226. package/src/session/revert.ts +108 -0
  227. package/src/session/status.ts +76 -0
  228. package/src/session/summary.ts +194 -0
  229. package/src/session/system.ts +108 -0
  230. package/src/session/todo.ts +37 -0
  231. package/src/share/share-next.ts +194 -0
  232. package/src/share/share.ts +23 -0
  233. package/src/shell/shell.ts +67 -0
  234. package/src/skill/index.ts +1 -0
  235. package/src/skill/skill.ts +124 -0
  236. package/src/snapshot/index.ts +197 -0
  237. package/src/storage/storage.ts +226 -0
  238. package/src/tool/bash.ts +262 -0
  239. package/src/tool/bash.txt +116 -0
  240. package/src/tool/batch.ts +175 -0
  241. package/src/tool/batch.txt +24 -0
  242. package/src/tool/codesearch.ts +132 -0
  243. package/src/tool/codesearch.txt +12 -0
  244. package/src/tool/edit.ts +655 -0
  245. package/src/tool/edit.txt +10 -0
  246. package/src/tool/glob.ts +75 -0
  247. package/src/tool/glob.txt +6 -0
  248. package/src/tool/grep.ts +132 -0
  249. package/src/tool/grep.txt +8 -0
  250. package/src/tool/invalid.ts +17 -0
  251. package/src/tool/ls.ts +119 -0
  252. package/src/tool/ls.txt +1 -0
  253. package/src/tool/lsp.ts +94 -0
  254. package/src/tool/lsp.txt +19 -0
  255. package/src/tool/multiedit.ts +46 -0
  256. package/src/tool/multiedit.txt +41 -0
  257. package/src/tool/patch.ts +210 -0
  258. package/src/tool/patch.txt +1 -0
  259. package/src/tool/read.ts +191 -0
  260. package/src/tool/read.txt +12 -0
  261. package/src/tool/registry.ts +137 -0
  262. package/src/tool/skill.ts +77 -0
  263. package/src/tool/task.ts +167 -0
  264. package/src/tool/task.txt +60 -0
  265. package/src/tool/todo.ts +53 -0
  266. package/src/tool/todoread.txt +14 -0
  267. package/src/tool/todowrite.txt +167 -0
  268. package/src/tool/tool.ts +73 -0
  269. package/src/tool/webfetch.ts +182 -0
  270. package/src/tool/webfetch.txt +13 -0
  271. package/src/tool/websearch.ts +144 -0
  272. package/src/tool/websearch.txt +11 -0
  273. package/src/tool/write.ts +84 -0
  274. package/src/tool/write.txt +8 -0
  275. package/src/util/archive.ts +16 -0
  276. package/src/util/color.ts +19 -0
  277. package/src/util/context.ts +25 -0
  278. package/src/util/defer.ts +12 -0
  279. package/src/util/eventloop.ts +20 -0
  280. package/src/util/filesystem.ts +83 -0
  281. package/src/util/fn.ts +11 -0
  282. package/src/util/iife.ts +3 -0
  283. package/src/util/keybind.ts +102 -0
  284. package/src/util/lazy.ts +18 -0
  285. package/src/util/locale.ts +81 -0
  286. package/src/util/lock.ts +98 -0
  287. package/src/util/log.ts +180 -0
  288. package/src/util/queue.ts +32 -0
  289. package/src/util/rpc.ts +42 -0
  290. package/src/util/scrap.ts +10 -0
  291. package/src/util/signal.ts +12 -0
  292. package/src/util/timeout.ts +14 -0
  293. package/src/util/token.ts +7 -0
  294. package/src/util/wildcard.ts +54 -0
  295. package/src/worktree/index.ts +217 -0
  296. package/sst-env.d.ts +9 -0
  297. package/test/agent/agent.test.ts +448 -0
  298. package/test/bun.test.ts +53 -0
  299. package/test/cli/github-action.test.ts +129 -0
  300. package/test/cli/github-remote.test.ts +80 -0
  301. package/test/cli/tui/transcript.test.ts +297 -0
  302. package/test/config/agent-color.test.ts +66 -0
  303. package/test/config/config.test.ts +870 -0
  304. package/test/config/markdown.test.ts +89 -0
  305. package/test/file/ignore.test.ts +10 -0
  306. package/test/file/path-traversal.test.ts +115 -0
  307. package/test/fixture/fixture.ts +45 -0
  308. package/test/fixture/lsp/fake-lsp-server.js +77 -0
  309. package/test/ide/ide.test.ts +82 -0
  310. package/test/keybind.test.ts +421 -0
  311. package/test/lsp/client.test.ts +95 -0
  312. package/test/mcp/headers.test.ts +153 -0
  313. package/test/patch/patch.test.ts +348 -0
  314. package/test/permission/arity.test.ts +33 -0
  315. package/test/permission/next.test.ts +652 -0
  316. package/test/preload.ts +63 -0
  317. package/test/project/project.test.ts +120 -0
  318. package/test/provider/amazon-bedrock.test.ts +236 -0
  319. package/test/provider/provider.test.ts +2127 -0
  320. package/test/provider/transform.test.ts +980 -0
  321. package/test/server/session-select.test.ts +78 -0
  322. package/test/session/compaction.test.ts +251 -0
  323. package/test/session/message-v2.test.ts +570 -0
  324. package/test/session/retry.test.ts +131 -0
  325. package/test/session/revert-compact.test.ts +285 -0
  326. package/test/session/session.test.ts +71 -0
  327. package/test/skill/skill.test.ts +185 -0
  328. package/test/snapshot/snapshot.test.ts +939 -0
  329. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  330. package/test/tool/bash.test.ts +232 -0
  331. package/test/tool/grep.test.ts +109 -0
  332. package/test/tool/patch.test.ts +261 -0
  333. package/test/tool/read.test.ts +167 -0
  334. package/test/util/iife.test.ts +36 -0
  335. package/test/util/lazy.test.ts +50 -0
  336. package/test/util/timeout.test.ts +21 -0
  337. package/test/util/wildcard.test.ts +55 -0
  338. package/tsconfig.json +16 -0
@@ -0,0 +1,2866 @@
1
+ import { BusEvent } from "@/bus/bus-event"
2
+ import { Bus } from "@/bus"
3
+ import { GlobalBus } from "@/bus/global"
4
+ import { Log } from "../util/log"
5
+ import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
6
+ import { Hono } from "hono"
7
+ import { cors } from "hono/cors"
8
+ import { stream, streamSSE } from "hono/streaming"
9
+ import { proxy } from "hono/proxy"
10
+ import { Session } from "../session"
11
+ import z from "zod"
12
+ import { Provider } from "../provider/provider"
13
+ import { filter, mapValues, sortBy, pipe } from "remeda"
14
+ import { NamedError } from "@opencode-ai/util/error"
15
+ import { ModelsDev } from "../provider/models"
16
+ import { Ripgrep } from "../file/ripgrep"
17
+ import { Config } from "../config/config"
18
+ import { File } from "../file"
19
+ import { LSP } from "../lsp"
20
+ import { Format } from "../format"
21
+ import { MessageV2 } from "../session/message-v2"
22
+ import { TuiRoute } from "./tui"
23
+ import { Instance } from "../project/instance"
24
+ import { Project } from "../project/project"
25
+ import { Vcs } from "../project/vcs"
26
+ import { Agent } from "../agent/agent"
27
+ import { Auth } from "../auth"
28
+ import { Command } from "../command"
29
+ import { ProviderAuth } from "../provider/auth"
30
+ import { Global } from "../global"
31
+ import { ProjectRoute } from "./project"
32
+ import { ToolRegistry } from "../tool/registry"
33
+ import { zodToJsonSchema } from "zod-to-json-schema"
34
+ import { SessionPrompt } from "../session/prompt"
35
+ import { SessionCompaction } from "../session/compaction"
36
+ import { SessionRevert } from "../session/revert"
37
+ import { lazy } from "../util/lazy"
38
+ import { Todo } from "../session/todo"
39
+ import { InstanceBootstrap } from "../project/bootstrap"
40
+ import { MCP } from "../mcp"
41
+ import { Storage } from "../storage/storage"
42
+ import type { ContentfulStatusCode } from "hono/utils/http-status"
43
+ import { TuiEvent } from "@/cli/cmd/tui/event"
44
+ import { Snapshot } from "@/snapshot"
45
+ import { SessionSummary } from "@/session/summary"
46
+ import { SessionStatus } from "@/session/status"
47
+ import { upgradeWebSocket, websocket } from "hono/bun"
48
+ import { errors } from "./error"
49
+ import { Pty } from "@/pty"
50
+ import { PermissionNext } from "@/permission/next"
51
+ import { Installation } from "@/installation"
52
+ import { MDNS } from "./mdns"
53
+ import { Worktree } from "../worktree"
54
+
55
+ // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
56
+ globalThis.AI_SDK_LOG_WARNINGS = false
57
+
58
+ export namespace Server {
59
+ const log = Log.create({ service: "server" })
60
+
61
+ let _url: URL | undefined
62
+ let _corsWhitelist: string[] = []
63
+
64
+ export function url(): URL {
65
+ return _url ?? new URL("http://localhost:4096")
66
+ }
67
+
68
+ export const Event = {
69
+ Connected: BusEvent.define("server.connected", z.object({})),
70
+ Disposed: BusEvent.define("global.disposed", z.object({})),
71
+ }
72
+
73
+ const app = new Hono()
74
+ export const App = lazy(() =>
75
+ app
76
+ .onError((err, c) => {
77
+ log.error("failed", {
78
+ error: err,
79
+ })
80
+ if (err instanceof NamedError) {
81
+ let status: ContentfulStatusCode
82
+ if (err instanceof Storage.NotFoundError) status = 404
83
+ else if (err instanceof Provider.ModelNotFoundError) status = 400
84
+ else if (err.name.startsWith("Worktree")) status = 400
85
+ else status = 500
86
+ return c.json(err.toObject(), { status })
87
+ }
88
+ const message = err instanceof Error && err.stack ? err.stack : err.toString()
89
+ return c.json(new NamedError.Unknown({ message }).toObject(), {
90
+ status: 500,
91
+ })
92
+ })
93
+ .use(async (c, next) => {
94
+ const skipLogging = c.req.path === "/log"
95
+ if (!skipLogging) {
96
+ log.info("request", {
97
+ method: c.req.method,
98
+ path: c.req.path,
99
+ })
100
+ }
101
+ const timer = log.time("request", {
102
+ method: c.req.method,
103
+ path: c.req.path,
104
+ })
105
+ await next()
106
+ if (!skipLogging) {
107
+ timer.stop()
108
+ }
109
+ })
110
+ .use(
111
+ cors({
112
+ origin(input) {
113
+ if (!input) return
114
+
115
+ if (input.startsWith("http://localhost:")) return input
116
+ if (input.startsWith("http://127.0.0.1:")) return input
117
+ if (input === "tauri://localhost" || input === "http://tauri.localhost") return input
118
+
119
+ // *.corethink.ai (https only)
120
+ if (/^https:\/\/([a-z0-9-]+\.)*corethink\.ai$/.test(input)) {
121
+ return input
122
+ }
123
+ if (_corsWhitelist.includes(input)) {
124
+ return input
125
+ }
126
+
127
+ return
128
+ },
129
+ }),
130
+ )
131
+ .get(
132
+ "/global/health",
133
+ describeRoute({
134
+ summary: "Get health",
135
+ description: "Get health information about the Chad Code server.",
136
+ operationId: "global.health",
137
+ responses: {
138
+ 200: {
139
+ description: "Health information",
140
+ content: {
141
+ "application/json": {
142
+ schema: resolver(z.object({ healthy: z.literal(true), version: z.string() })),
143
+ },
144
+ },
145
+ },
146
+ },
147
+ }),
148
+ async (c) => {
149
+ return c.json({ healthy: true, version: Installation.VERSION })
150
+ },
151
+ )
152
+ .get(
153
+ "/global/event",
154
+ describeRoute({
155
+ summary: "Get global events",
156
+ description: "Subscribe to global events from the Chad Code system using server-sent events.",
157
+ operationId: "global.event",
158
+ responses: {
159
+ 200: {
160
+ description: "Event stream",
161
+ content: {
162
+ "text/event-stream": {
163
+ schema: resolver(
164
+ z
165
+ .object({
166
+ directory: z.string(),
167
+ payload: BusEvent.payloads(),
168
+ })
169
+ .meta({
170
+ ref: "GlobalEvent",
171
+ }),
172
+ ),
173
+ },
174
+ },
175
+ },
176
+ },
177
+ }),
178
+ async (c) => {
179
+ log.info("global event connected")
180
+ return streamSSE(c, async (stream) => {
181
+ stream.writeSSE({
182
+ data: JSON.stringify({
183
+ payload: {
184
+ type: "server.connected",
185
+ properties: {},
186
+ },
187
+ }),
188
+ })
189
+ async function handler(event: any) {
190
+ await stream.writeSSE({
191
+ data: JSON.stringify(event),
192
+ })
193
+ }
194
+ GlobalBus.on("event", handler)
195
+
196
+ // Send heartbeat every 30s to prevent WKWebView timeout (60s default)
197
+ const heartbeat = setInterval(() => {
198
+ stream.writeSSE({
199
+ data: JSON.stringify({
200
+ payload: {
201
+ type: "server.heartbeat",
202
+ properties: {},
203
+ },
204
+ }),
205
+ })
206
+ }, 30000)
207
+
208
+ await new Promise<void>((resolve) => {
209
+ stream.onAbort(() => {
210
+ clearInterval(heartbeat)
211
+ GlobalBus.off("event", handler)
212
+ resolve()
213
+ log.info("global event disconnected")
214
+ })
215
+ })
216
+ })
217
+ },
218
+ )
219
+ .post(
220
+ "/global/dispose",
221
+ describeRoute({
222
+ summary: "Dispose instance",
223
+ description: "Clean up and dispose all Chad Code instances, releasing all resources.",
224
+ operationId: "global.dispose",
225
+ responses: {
226
+ 200: {
227
+ description: "Global disposed",
228
+ content: {
229
+ "application/json": {
230
+ schema: resolver(z.boolean()),
231
+ },
232
+ },
233
+ },
234
+ },
235
+ }),
236
+ async (c) => {
237
+ await Instance.disposeAll()
238
+ GlobalBus.emit("event", {
239
+ directory: "global",
240
+ payload: {
241
+ type: Event.Disposed.type,
242
+ properties: {},
243
+ },
244
+ })
245
+ return c.json(true)
246
+ },
247
+ )
248
+ .use(async (c, next) => {
249
+ const directory = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
250
+ return Instance.provide({
251
+ directory,
252
+ init: InstanceBootstrap,
253
+ async fn() {
254
+ return next()
255
+ },
256
+ })
257
+ })
258
+ .get(
259
+ "/doc",
260
+ openAPIRouteHandler(app, {
261
+ documentation: {
262
+ info: {
263
+ title: "opencode",
264
+ version: "0.0.3",
265
+ description: "opencode api",
266
+ },
267
+ openapi: "3.1.1",
268
+ },
269
+ }),
270
+ )
271
+ .use(validator("query", z.object({ directory: z.string().optional() })))
272
+
273
+ .route("/project", ProjectRoute)
274
+
275
+ .get(
276
+ "/pty",
277
+ describeRoute({
278
+ summary: "List PTY sessions",
279
+ description: "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.",
280
+ operationId: "pty.list",
281
+ responses: {
282
+ 200: {
283
+ description: "List of sessions",
284
+ content: {
285
+ "application/json": {
286
+ schema: resolver(Pty.Info.array()),
287
+ },
288
+ },
289
+ },
290
+ },
291
+ }),
292
+ async (c) => {
293
+ return c.json(Pty.list())
294
+ },
295
+ )
296
+ .post(
297
+ "/pty",
298
+ describeRoute({
299
+ summary: "Create PTY session",
300
+ description: "Create a new pseudo-terminal (PTY) session for running shell commands and processes.",
301
+ operationId: "pty.create",
302
+ responses: {
303
+ 200: {
304
+ description: "Created session",
305
+ content: {
306
+ "application/json": {
307
+ schema: resolver(Pty.Info),
308
+ },
309
+ },
310
+ },
311
+ ...errors(400),
312
+ },
313
+ }),
314
+ validator("json", Pty.CreateInput),
315
+ async (c) => {
316
+ const info = await Pty.create(c.req.valid("json"))
317
+ return c.json(info)
318
+ },
319
+ )
320
+ .get(
321
+ "/pty/:ptyID",
322
+ describeRoute({
323
+ summary: "Get PTY session",
324
+ description: "Retrieve detailed information about a specific pseudo-terminal (PTY) session.",
325
+ operationId: "pty.get",
326
+ responses: {
327
+ 200: {
328
+ description: "Session info",
329
+ content: {
330
+ "application/json": {
331
+ schema: resolver(Pty.Info),
332
+ },
333
+ },
334
+ },
335
+ ...errors(404),
336
+ },
337
+ }),
338
+ validator("param", z.object({ ptyID: z.string() })),
339
+ async (c) => {
340
+ const info = Pty.get(c.req.valid("param").ptyID)
341
+ if (!info) {
342
+ throw new Storage.NotFoundError({ message: "Session not found" })
343
+ }
344
+ return c.json(info)
345
+ },
346
+ )
347
+ .put(
348
+ "/pty/:ptyID",
349
+ describeRoute({
350
+ summary: "Update PTY session",
351
+ description: "Update properties of an existing pseudo-terminal (PTY) session.",
352
+ operationId: "pty.update",
353
+ responses: {
354
+ 200: {
355
+ description: "Updated session",
356
+ content: {
357
+ "application/json": {
358
+ schema: resolver(Pty.Info),
359
+ },
360
+ },
361
+ },
362
+ ...errors(400),
363
+ },
364
+ }),
365
+ validator("param", z.object({ ptyID: z.string() })),
366
+ validator("json", Pty.UpdateInput),
367
+ async (c) => {
368
+ const info = await Pty.update(c.req.valid("param").ptyID, c.req.valid("json"))
369
+ return c.json(info)
370
+ },
371
+ )
372
+ .delete(
373
+ "/pty/:ptyID",
374
+ describeRoute({
375
+ summary: "Remove PTY session",
376
+ description: "Remove and terminate a specific pseudo-terminal (PTY) session.",
377
+ operationId: "pty.remove",
378
+ responses: {
379
+ 200: {
380
+ description: "Session removed",
381
+ content: {
382
+ "application/json": {
383
+ schema: resolver(z.boolean()),
384
+ },
385
+ },
386
+ },
387
+ ...errors(404),
388
+ },
389
+ }),
390
+ validator("param", z.object({ ptyID: z.string() })),
391
+ async (c) => {
392
+ await Pty.remove(c.req.valid("param").ptyID)
393
+ return c.json(true)
394
+ },
395
+ )
396
+ .get(
397
+ "/pty/:ptyID/connect",
398
+ describeRoute({
399
+ summary: "Connect to PTY session",
400
+ description:
401
+ "Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.",
402
+ operationId: "pty.connect",
403
+ responses: {
404
+ 200: {
405
+ description: "Connected session",
406
+ content: {
407
+ "application/json": {
408
+ schema: resolver(z.boolean()),
409
+ },
410
+ },
411
+ },
412
+ ...errors(404),
413
+ },
414
+ }),
415
+ validator("param", z.object({ ptyID: z.string() })),
416
+ upgradeWebSocket((c) => {
417
+ const id = c.req.param("ptyID")
418
+ let handler: ReturnType<typeof Pty.connect>
419
+ if (!Pty.get(id)) throw new Error("Session not found")
420
+ return {
421
+ onOpen(_event, ws) {
422
+ handler = Pty.connect(id, ws)
423
+ },
424
+ onMessage(event) {
425
+ handler?.onMessage(String(event.data))
426
+ },
427
+ onClose() {
428
+ handler?.onClose()
429
+ },
430
+ }
431
+ }),
432
+ )
433
+
434
+ .get(
435
+ "/config",
436
+ describeRoute({
437
+ summary: "Get configuration",
438
+ description: "Retrieve the current OpenCode configuration settings and preferences.",
439
+ operationId: "config.get",
440
+ responses: {
441
+ 200: {
442
+ description: "Get config info",
443
+ content: {
444
+ "application/json": {
445
+ schema: resolver(Config.Info),
446
+ },
447
+ },
448
+ },
449
+ },
450
+ }),
451
+ async (c) => {
452
+ return c.json(await Config.get())
453
+ },
454
+ )
455
+
456
+ .patch(
457
+ "/config",
458
+ describeRoute({
459
+ summary: "Update configuration",
460
+ description: "Update OpenCode configuration settings and preferences.",
461
+ operationId: "config.update",
462
+ responses: {
463
+ 200: {
464
+ description: "Successfully updated config",
465
+ content: {
466
+ "application/json": {
467
+ schema: resolver(Config.Info),
468
+ },
469
+ },
470
+ },
471
+ ...errors(400),
472
+ },
473
+ }),
474
+ validator("json", Config.Info),
475
+ async (c) => {
476
+ const config = c.req.valid("json")
477
+ await Config.update(config)
478
+ return c.json(config)
479
+ },
480
+ )
481
+ .get(
482
+ "/experimental/tool/ids",
483
+ describeRoute({
484
+ summary: "List tool IDs",
485
+ description:
486
+ "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.",
487
+ operationId: "tool.ids",
488
+ responses: {
489
+ 200: {
490
+ description: "Tool IDs",
491
+ content: {
492
+ "application/json": {
493
+ schema: resolver(z.array(z.string()).meta({ ref: "ToolIDs" })),
494
+ },
495
+ },
496
+ },
497
+ ...errors(400),
498
+ },
499
+ }),
500
+ async (c) => {
501
+ return c.json(await ToolRegistry.ids())
502
+ },
503
+ )
504
+ .get(
505
+ "/experimental/tool",
506
+ describeRoute({
507
+ summary: "List tools",
508
+ description:
509
+ "Get a list of available tools with their JSON schema parameters for a specific provider and model combination.",
510
+ operationId: "tool.list",
511
+ responses: {
512
+ 200: {
513
+ description: "Tools",
514
+ content: {
515
+ "application/json": {
516
+ schema: resolver(
517
+ z
518
+ .array(
519
+ z
520
+ .object({
521
+ id: z.string(),
522
+ description: z.string(),
523
+ parameters: z.any(),
524
+ })
525
+ .meta({ ref: "ToolListItem" }),
526
+ )
527
+ .meta({ ref: "ToolList" }),
528
+ ),
529
+ },
530
+ },
531
+ },
532
+ ...errors(400),
533
+ },
534
+ }),
535
+ validator(
536
+ "query",
537
+ z.object({
538
+ provider: z.string(),
539
+ model: z.string(),
540
+ }),
541
+ ),
542
+ async (c) => {
543
+ const { provider } = c.req.valid("query")
544
+ const tools = await ToolRegistry.tools(provider)
545
+ return c.json(
546
+ tools.map((t) => ({
547
+ id: t.id,
548
+ description: t.description,
549
+ // Handle both Zod schemas and plain JSON schemas
550
+ parameters: (t.parameters as any)?._def ? zodToJsonSchema(t.parameters as any) : t.parameters,
551
+ })),
552
+ )
553
+ },
554
+ )
555
+ .post(
556
+ "/instance/dispose",
557
+ describeRoute({
558
+ summary: "Dispose instance",
559
+ description: "Clean up and dispose the current OpenCode instance, releasing all resources.",
560
+ operationId: "instance.dispose",
561
+ responses: {
562
+ 200: {
563
+ description: "Instance disposed",
564
+ content: {
565
+ "application/json": {
566
+ schema: resolver(z.boolean()),
567
+ },
568
+ },
569
+ },
570
+ },
571
+ }),
572
+ async (c) => {
573
+ await Instance.dispose()
574
+ return c.json(true)
575
+ },
576
+ )
577
+ .get(
578
+ "/path",
579
+ describeRoute({
580
+ summary: "Get paths",
581
+ description: "Retrieve the current working directory and related path information for the OpenCode instance.",
582
+ operationId: "path.get",
583
+ responses: {
584
+ 200: {
585
+ description: "Path",
586
+ content: {
587
+ "application/json": {
588
+ schema: resolver(
589
+ z
590
+ .object({
591
+ home: z.string(),
592
+ state: z.string(),
593
+ config: z.string(),
594
+ worktree: z.string(),
595
+ directory: z.string(),
596
+ })
597
+ .meta({
598
+ ref: "Path",
599
+ }),
600
+ ),
601
+ },
602
+ },
603
+ },
604
+ },
605
+ }),
606
+ async (c) => {
607
+ return c.json({
608
+ home: Global.Path.home,
609
+ state: Global.Path.state,
610
+ config: Global.Path.config,
611
+ worktree: Instance.worktree,
612
+ directory: Instance.directory,
613
+ })
614
+ },
615
+ )
616
+ .post(
617
+ "/experimental/worktree",
618
+ describeRoute({
619
+ summary: "Create worktree",
620
+ description: "Create a new git worktree for the current project.",
621
+ operationId: "worktree.create",
622
+ responses: {
623
+ 200: {
624
+ description: "Worktree created",
625
+ content: {
626
+ "application/json": {
627
+ schema: resolver(Worktree.Info),
628
+ },
629
+ },
630
+ },
631
+ ...errors(400),
632
+ },
633
+ }),
634
+ validator("json", Worktree.create.schema),
635
+ async (c) => {
636
+ const body = c.req.valid("json")
637
+ const worktree = await Worktree.create(body)
638
+ return c.json(worktree)
639
+ },
640
+ )
641
+ .get(
642
+ "/experimental/worktree",
643
+ describeRoute({
644
+ summary: "List worktrees",
645
+ description: "List all sandbox worktrees for the current project.",
646
+ operationId: "worktree.list",
647
+ responses: {
648
+ 200: {
649
+ description: "List of worktree directories",
650
+ content: {
651
+ "application/json": {
652
+ schema: resolver(z.array(z.string())),
653
+ },
654
+ },
655
+ },
656
+ },
657
+ }),
658
+ async (c) => {
659
+ const sandboxes = await Project.sandboxes(Instance.project.id)
660
+ return c.json(sandboxes)
661
+ },
662
+ )
663
+ .get(
664
+ "/vcs",
665
+ describeRoute({
666
+ summary: "Get VCS info",
667
+ description: "Retrieve version control system (VCS) information for the current project, such as git branch.",
668
+ operationId: "vcs.get",
669
+ responses: {
670
+ 200: {
671
+ description: "VCS info",
672
+ content: {
673
+ "application/json": {
674
+ schema: resolver(Vcs.Info),
675
+ },
676
+ },
677
+ },
678
+ },
679
+ }),
680
+ async (c) => {
681
+ const branch = await Vcs.branch()
682
+ return c.json({
683
+ branch,
684
+ })
685
+ },
686
+ )
687
+ .get(
688
+ "/session",
689
+ describeRoute({
690
+ summary: "List sessions",
691
+ description: "Get a list of all OpenCode sessions, sorted by most recently updated.",
692
+ operationId: "session.list",
693
+ responses: {
694
+ 200: {
695
+ description: "List of sessions",
696
+ content: {
697
+ "application/json": {
698
+ schema: resolver(Session.Info.array()),
699
+ },
700
+ },
701
+ },
702
+ },
703
+ }),
704
+ validator(
705
+ "query",
706
+ z.object({
707
+ start: z.coerce
708
+ .number()
709
+ .optional()
710
+ .meta({ description: "Filter sessions updated on or after this timestamp (milliseconds since epoch)" }),
711
+ search: z.string().optional().meta({ description: "Filter sessions by title (case-insensitive)" }),
712
+ limit: z.coerce.number().optional().meta({ description: "Maximum number of sessions to return" }),
713
+ }),
714
+ ),
715
+ async (c) => {
716
+ const query = c.req.valid("query")
717
+ const term = query.search?.toLowerCase()
718
+ const sessions: Session.Info[] = []
719
+ for await (const session of Session.list()) {
720
+ if (query.start !== undefined && session.time.updated < query.start) continue
721
+ if (term !== undefined && !session.title.toLowerCase().includes(term)) continue
722
+ sessions.push(session)
723
+ if (query.limit !== undefined && sessions.length >= query.limit) break
724
+ }
725
+ return c.json(sessions)
726
+ },
727
+ )
728
+ .get(
729
+ "/session/status",
730
+ describeRoute({
731
+ summary: "Get session status",
732
+ description: "Retrieve the current status of all sessions, including active, idle, and completed states.",
733
+ operationId: "session.status",
734
+ responses: {
735
+ 200: {
736
+ description: "Get session status",
737
+ content: {
738
+ "application/json": {
739
+ schema: resolver(z.record(z.string(), SessionStatus.Info)),
740
+ },
741
+ },
742
+ },
743
+ ...errors(400),
744
+ },
745
+ }),
746
+ async (c) => {
747
+ const result = SessionStatus.list()
748
+ return c.json(result)
749
+ },
750
+ )
751
+ .get(
752
+ "/session/:sessionID",
753
+ describeRoute({
754
+ summary: "Get session",
755
+ description: "Retrieve detailed information about a specific OpenCode session.",
756
+ tags: ["Session"],
757
+ operationId: "session.get",
758
+ responses: {
759
+ 200: {
760
+ description: "Get session",
761
+ content: {
762
+ "application/json": {
763
+ schema: resolver(Session.Info),
764
+ },
765
+ },
766
+ },
767
+ ...errors(400, 404),
768
+ },
769
+ }),
770
+ validator(
771
+ "param",
772
+ z.object({
773
+ sessionID: Session.get.schema,
774
+ }),
775
+ ),
776
+ async (c) => {
777
+ const sessionID = c.req.valid("param").sessionID
778
+ log.info("SEARCH", { url: c.req.url })
779
+ const session = await Session.get(sessionID)
780
+ return c.json(session)
781
+ },
782
+ )
783
+ .get(
784
+ "/session/:sessionID/children",
785
+ describeRoute({
786
+ summary: "Get session children",
787
+ tags: ["Session"],
788
+ description: "Retrieve all child sessions that were forked from the specified parent session.",
789
+ operationId: "session.children",
790
+ responses: {
791
+ 200: {
792
+ description: "List of children",
793
+ content: {
794
+ "application/json": {
795
+ schema: resolver(Session.Info.array()),
796
+ },
797
+ },
798
+ },
799
+ ...errors(400, 404),
800
+ },
801
+ }),
802
+ validator(
803
+ "param",
804
+ z.object({
805
+ sessionID: Session.children.schema,
806
+ }),
807
+ ),
808
+ async (c) => {
809
+ const sessionID = c.req.valid("param").sessionID
810
+ const session = await Session.children(sessionID)
811
+ return c.json(session)
812
+ },
813
+ )
814
+ .get(
815
+ "/session/:sessionID/todo",
816
+ describeRoute({
817
+ summary: "Get session todos",
818
+ description: "Retrieve the todo list associated with a specific session, showing tasks and action items.",
819
+ operationId: "session.todo",
820
+ responses: {
821
+ 200: {
822
+ description: "Todo list",
823
+ content: {
824
+ "application/json": {
825
+ schema: resolver(Todo.Info.array()),
826
+ },
827
+ },
828
+ },
829
+ ...errors(400, 404),
830
+ },
831
+ }),
832
+ validator(
833
+ "param",
834
+ z.object({
835
+ sessionID: z.string().meta({ description: "Session ID" }),
836
+ }),
837
+ ),
838
+ async (c) => {
839
+ const sessionID = c.req.valid("param").sessionID
840
+ const todos = await Todo.get(sessionID)
841
+ return c.json(todos)
842
+ },
843
+ )
844
+ .post(
845
+ "/session",
846
+ describeRoute({
847
+ summary: "Create session",
848
+ description: "Create a new OpenCode session for interacting with AI assistants and managing conversations.",
849
+ operationId: "session.create",
850
+ responses: {
851
+ ...errors(400),
852
+ 200: {
853
+ description: "Successfully created session",
854
+ content: {
855
+ "application/json": {
856
+ schema: resolver(Session.Info),
857
+ },
858
+ },
859
+ },
860
+ },
861
+ }),
862
+ validator("json", Session.create.schema.optional()),
863
+ async (c) => {
864
+ const body = c.req.valid("json") ?? {}
865
+ const session = await Session.create(body)
866
+ return c.json(session)
867
+ },
868
+ )
869
+ .delete(
870
+ "/session/:sessionID",
871
+ describeRoute({
872
+ summary: "Delete session",
873
+ description: "Delete a session and permanently remove all associated data, including messages and history.",
874
+ operationId: "session.delete",
875
+ responses: {
876
+ 200: {
877
+ description: "Successfully deleted session",
878
+ content: {
879
+ "application/json": {
880
+ schema: resolver(z.boolean()),
881
+ },
882
+ },
883
+ },
884
+ ...errors(400, 404),
885
+ },
886
+ }),
887
+ validator(
888
+ "param",
889
+ z.object({
890
+ sessionID: Session.remove.schema,
891
+ }),
892
+ ),
893
+ async (c) => {
894
+ const sessionID = c.req.valid("param").sessionID
895
+ await Session.remove(sessionID)
896
+ return c.json(true)
897
+ },
898
+ )
899
+ .patch(
900
+ "/session/:sessionID",
901
+ describeRoute({
902
+ summary: "Update session",
903
+ description: "Update properties of an existing session, such as title or other metadata.",
904
+ operationId: "session.update",
905
+ responses: {
906
+ 200: {
907
+ description: "Successfully updated session",
908
+ content: {
909
+ "application/json": {
910
+ schema: resolver(Session.Info),
911
+ },
912
+ },
913
+ },
914
+ ...errors(400, 404),
915
+ },
916
+ }),
917
+ validator(
918
+ "param",
919
+ z.object({
920
+ sessionID: z.string(),
921
+ }),
922
+ ),
923
+ validator(
924
+ "json",
925
+ z.object({
926
+ title: z.string().optional(),
927
+ time: z
928
+ .object({
929
+ archived: z.number().optional(),
930
+ })
931
+ .optional(),
932
+ }),
933
+ ),
934
+ async (c) => {
935
+ const sessionID = c.req.valid("param").sessionID
936
+ const updates = c.req.valid("json")
937
+
938
+ const updatedSession = await Session.update(sessionID, (session) => {
939
+ if (updates.title !== undefined) {
940
+ session.title = updates.title
941
+ }
942
+ if (updates.time?.archived !== undefined) session.time.archived = updates.time.archived
943
+ })
944
+
945
+ return c.json(updatedSession)
946
+ },
947
+ )
948
+ .post(
949
+ "/session/:sessionID/init",
950
+ describeRoute({
951
+ summary: "Initialize session",
952
+ description:
953
+ "Analyze the current application and create an AGENTS.md file with project-specific agent configurations.",
954
+ operationId: "session.init",
955
+ responses: {
956
+ 200: {
957
+ description: "200",
958
+ content: {
959
+ "application/json": {
960
+ schema: resolver(z.boolean()),
961
+ },
962
+ },
963
+ },
964
+ ...errors(400, 404),
965
+ },
966
+ }),
967
+ validator(
968
+ "param",
969
+ z.object({
970
+ sessionID: z.string().meta({ description: "Session ID" }),
971
+ }),
972
+ ),
973
+ validator("json", Session.initialize.schema.omit({ sessionID: true })),
974
+ async (c) => {
975
+ const sessionID = c.req.valid("param").sessionID
976
+ const body = c.req.valid("json")
977
+ await Session.initialize({ ...body, sessionID })
978
+ return c.json(true)
979
+ },
980
+ )
981
+ .post(
982
+ "/session/:sessionID/fork",
983
+ describeRoute({
984
+ summary: "Fork session",
985
+ description: "Create a new session by forking an existing session at a specific message point.",
986
+ operationId: "session.fork",
987
+ responses: {
988
+ 200: {
989
+ description: "200",
990
+ content: {
991
+ "application/json": {
992
+ schema: resolver(Session.Info),
993
+ },
994
+ },
995
+ },
996
+ },
997
+ }),
998
+ validator(
999
+ "param",
1000
+ z.object({
1001
+ sessionID: Session.fork.schema.shape.sessionID,
1002
+ }),
1003
+ ),
1004
+ validator("json", Session.fork.schema.omit({ sessionID: true })),
1005
+ async (c) => {
1006
+ const sessionID = c.req.valid("param").sessionID
1007
+ const body = c.req.valid("json")
1008
+ const result = await Session.fork({ ...body, sessionID })
1009
+ return c.json(result)
1010
+ },
1011
+ )
1012
+ .post(
1013
+ "/session/:sessionID/abort",
1014
+ describeRoute({
1015
+ summary: "Abort session",
1016
+ description: "Abort an active session and stop any ongoing AI processing or command execution.",
1017
+ operationId: "session.abort",
1018
+ responses: {
1019
+ 200: {
1020
+ description: "Aborted session",
1021
+ content: {
1022
+ "application/json": {
1023
+ schema: resolver(z.boolean()),
1024
+ },
1025
+ },
1026
+ },
1027
+ ...errors(400, 404),
1028
+ },
1029
+ }),
1030
+ validator(
1031
+ "param",
1032
+ z.object({
1033
+ sessionID: z.string(),
1034
+ }),
1035
+ ),
1036
+ async (c) => {
1037
+ SessionPrompt.cancel(c.req.valid("param").sessionID)
1038
+ return c.json(true)
1039
+ },
1040
+ )
1041
+
1042
+ .post(
1043
+ "/session/:sessionID/share",
1044
+ describeRoute({
1045
+ summary: "Share session",
1046
+ description: "Create a shareable link for a session, allowing others to view the conversation.",
1047
+ operationId: "session.share",
1048
+ responses: {
1049
+ 200: {
1050
+ description: "Successfully shared session",
1051
+ content: {
1052
+ "application/json": {
1053
+ schema: resolver(Session.Info),
1054
+ },
1055
+ },
1056
+ },
1057
+ ...errors(400, 404),
1058
+ },
1059
+ }),
1060
+ validator(
1061
+ "param",
1062
+ z.object({
1063
+ sessionID: z.string(),
1064
+ }),
1065
+ ),
1066
+ async (c) => {
1067
+ const sessionID = c.req.valid("param").sessionID
1068
+ await Session.share(sessionID)
1069
+ const session = await Session.get(sessionID)
1070
+ return c.json(session)
1071
+ },
1072
+ )
1073
+ .get(
1074
+ "/session/:sessionID/diff",
1075
+ describeRoute({
1076
+ summary: "Get message diff",
1077
+ description: "Get the file changes (diff) that resulted from a specific user message in the session.",
1078
+ operationId: "session.diff",
1079
+ responses: {
1080
+ 200: {
1081
+ description: "Successfully retrieved diff",
1082
+ content: {
1083
+ "application/json": {
1084
+ schema: resolver(Snapshot.FileDiff.array()),
1085
+ },
1086
+ },
1087
+ },
1088
+ },
1089
+ }),
1090
+ validator(
1091
+ "param",
1092
+ z.object({
1093
+ sessionID: SessionSummary.diff.schema.shape.sessionID,
1094
+ }),
1095
+ ),
1096
+ validator(
1097
+ "query",
1098
+ z.object({
1099
+ messageID: SessionSummary.diff.schema.shape.messageID,
1100
+ }),
1101
+ ),
1102
+ async (c) => {
1103
+ const query = c.req.valid("query")
1104
+ const params = c.req.valid("param")
1105
+ const result = await SessionSummary.diff({
1106
+ sessionID: params.sessionID,
1107
+ messageID: query.messageID,
1108
+ })
1109
+ return c.json(result)
1110
+ },
1111
+ )
1112
+ .delete(
1113
+ "/session/:sessionID/share",
1114
+ describeRoute({
1115
+ summary: "Unshare session",
1116
+ description: "Remove the shareable link for a session, making it private again.",
1117
+ operationId: "session.unshare",
1118
+ responses: {
1119
+ 200: {
1120
+ description: "Successfully unshared session",
1121
+ content: {
1122
+ "application/json": {
1123
+ schema: resolver(Session.Info),
1124
+ },
1125
+ },
1126
+ },
1127
+ ...errors(400, 404),
1128
+ },
1129
+ }),
1130
+ validator(
1131
+ "param",
1132
+ z.object({
1133
+ sessionID: Session.unshare.schema,
1134
+ }),
1135
+ ),
1136
+ async (c) => {
1137
+ const sessionID = c.req.valid("param").sessionID
1138
+ await Session.unshare(sessionID)
1139
+ const session = await Session.get(sessionID)
1140
+ return c.json(session)
1141
+ },
1142
+ )
1143
+ .post(
1144
+ "/session/:sessionID/summarize",
1145
+ describeRoute({
1146
+ summary: "Summarize session",
1147
+ description: "Generate a concise summary of the session using AI compaction to preserve key information.",
1148
+ operationId: "session.summarize",
1149
+ responses: {
1150
+ 200: {
1151
+ description: "Summarized session",
1152
+ content: {
1153
+ "application/json": {
1154
+ schema: resolver(z.boolean()),
1155
+ },
1156
+ },
1157
+ },
1158
+ ...errors(400, 404),
1159
+ },
1160
+ }),
1161
+ validator(
1162
+ "param",
1163
+ z.object({
1164
+ sessionID: z.string().meta({ description: "Session ID" }),
1165
+ }),
1166
+ ),
1167
+ validator(
1168
+ "json",
1169
+ z.object({
1170
+ providerID: z.string(),
1171
+ modelID: z.string(),
1172
+ auto: z.boolean().optional().default(false),
1173
+ }),
1174
+ ),
1175
+ async (c) => {
1176
+ const sessionID = c.req.valid("param").sessionID
1177
+ const body = c.req.valid("json")
1178
+ const session = await Session.get(sessionID)
1179
+ await SessionRevert.cleanup(session)
1180
+ const msgs = await Session.messages({ sessionID })
1181
+ let currentAgent = await Agent.defaultAgent()
1182
+ for (let i = msgs.length - 1; i >= 0; i--) {
1183
+ const info = msgs[i].info
1184
+ if (info.role === "user") {
1185
+ currentAgent = info.agent || (await Agent.defaultAgent())
1186
+ break
1187
+ }
1188
+ }
1189
+ await SessionCompaction.create({
1190
+ sessionID,
1191
+ agent: currentAgent,
1192
+ model: {
1193
+ providerID: body.providerID,
1194
+ modelID: body.modelID,
1195
+ },
1196
+ auto: body.auto,
1197
+ })
1198
+ await SessionPrompt.loop(sessionID)
1199
+ return c.json(true)
1200
+ },
1201
+ )
1202
+ .get(
1203
+ "/session/:sessionID/message",
1204
+ describeRoute({
1205
+ summary: "Get session messages",
1206
+ description: "Retrieve all messages in a session, including user prompts and AI responses.",
1207
+ operationId: "session.messages",
1208
+ responses: {
1209
+ 200: {
1210
+ description: "List of messages",
1211
+ content: {
1212
+ "application/json": {
1213
+ schema: resolver(MessageV2.WithParts.array()),
1214
+ },
1215
+ },
1216
+ },
1217
+ ...errors(400, 404),
1218
+ },
1219
+ }),
1220
+ validator(
1221
+ "param",
1222
+ z.object({
1223
+ sessionID: z.string().meta({ description: "Session ID" }),
1224
+ }),
1225
+ ),
1226
+ validator(
1227
+ "query",
1228
+ z.object({
1229
+ limit: z.coerce.number().optional(),
1230
+ }),
1231
+ ),
1232
+ async (c) => {
1233
+ const query = c.req.valid("query")
1234
+ const messages = await Session.messages({
1235
+ sessionID: c.req.valid("param").sessionID,
1236
+ limit: query.limit,
1237
+ })
1238
+ return c.json(messages)
1239
+ },
1240
+ )
1241
+ .get(
1242
+ "/session/:sessionID/diff",
1243
+ describeRoute({
1244
+ summary: "Get session diff",
1245
+ description: "Get all file changes (diffs) made during this session.",
1246
+ operationId: "session.diff",
1247
+ responses: {
1248
+ 200: {
1249
+ description: "List of diffs",
1250
+ content: {
1251
+ "application/json": {
1252
+ schema: resolver(Snapshot.FileDiff.array()),
1253
+ },
1254
+ },
1255
+ },
1256
+ ...errors(400, 404),
1257
+ },
1258
+ }),
1259
+ validator(
1260
+ "param",
1261
+ z.object({
1262
+ sessionID: z.string().meta({ description: "Session ID" }),
1263
+ }),
1264
+ ),
1265
+ async (c) => {
1266
+ const diff = await Session.diff(c.req.valid("param").sessionID)
1267
+ return c.json(diff)
1268
+ },
1269
+ )
1270
+ .get(
1271
+ "/session/:sessionID/message/:messageID",
1272
+ describeRoute({
1273
+ summary: "Get message",
1274
+ description: "Retrieve a specific message from a session by its message ID.",
1275
+ operationId: "session.message",
1276
+ responses: {
1277
+ 200: {
1278
+ description: "Message",
1279
+ content: {
1280
+ "application/json": {
1281
+ schema: resolver(
1282
+ z.object({
1283
+ info: MessageV2.Info,
1284
+ parts: MessageV2.Part.array(),
1285
+ }),
1286
+ ),
1287
+ },
1288
+ },
1289
+ },
1290
+ ...errors(400, 404),
1291
+ },
1292
+ }),
1293
+ validator(
1294
+ "param",
1295
+ z.object({
1296
+ sessionID: z.string().meta({ description: "Session ID" }),
1297
+ messageID: z.string().meta({ description: "Message ID" }),
1298
+ }),
1299
+ ),
1300
+ async (c) => {
1301
+ const params = c.req.valid("param")
1302
+ const message = await MessageV2.get({
1303
+ sessionID: params.sessionID,
1304
+ messageID: params.messageID,
1305
+ })
1306
+ return c.json(message)
1307
+ },
1308
+ )
1309
+ .delete(
1310
+ "/session/:sessionID/message/:messageID/part/:partID",
1311
+ describeRoute({
1312
+ description: "Delete a part from a message",
1313
+ operationId: "part.delete",
1314
+ responses: {
1315
+ 200: {
1316
+ description: "Successfully deleted part",
1317
+ content: {
1318
+ "application/json": {
1319
+ schema: resolver(z.boolean()),
1320
+ },
1321
+ },
1322
+ },
1323
+ ...errors(400, 404),
1324
+ },
1325
+ }),
1326
+ validator(
1327
+ "param",
1328
+ z.object({
1329
+ sessionID: z.string().meta({ description: "Session ID" }),
1330
+ messageID: z.string().meta({ description: "Message ID" }),
1331
+ partID: z.string().meta({ description: "Part ID" }),
1332
+ }),
1333
+ ),
1334
+ async (c) => {
1335
+ const params = c.req.valid("param")
1336
+ await Session.removePart({
1337
+ sessionID: params.sessionID,
1338
+ messageID: params.messageID,
1339
+ partID: params.partID,
1340
+ })
1341
+ return c.json(true)
1342
+ },
1343
+ )
1344
+ .patch(
1345
+ "/session/:sessionID/message/:messageID/part/:partID",
1346
+ describeRoute({
1347
+ description: "Update a part in a message",
1348
+ operationId: "part.update",
1349
+ responses: {
1350
+ 200: {
1351
+ description: "Successfully updated part",
1352
+ content: {
1353
+ "application/json": {
1354
+ schema: resolver(MessageV2.Part),
1355
+ },
1356
+ },
1357
+ },
1358
+ ...errors(400, 404),
1359
+ },
1360
+ }),
1361
+ validator(
1362
+ "param",
1363
+ z.object({
1364
+ sessionID: z.string().meta({ description: "Session ID" }),
1365
+ messageID: z.string().meta({ description: "Message ID" }),
1366
+ partID: z.string().meta({ description: "Part ID" }),
1367
+ }),
1368
+ ),
1369
+ validator("json", MessageV2.Part),
1370
+ async (c) => {
1371
+ const params = c.req.valid("param")
1372
+ const body = c.req.valid("json")
1373
+ if (body.id !== params.partID || body.messageID !== params.messageID || body.sessionID !== params.sessionID) {
1374
+ throw new Error(
1375
+ `Part mismatch: body.id='${body.id}' vs partID='${params.partID}', body.messageID='${body.messageID}' vs messageID='${params.messageID}', body.sessionID='${body.sessionID}' vs sessionID='${params.sessionID}'`,
1376
+ )
1377
+ }
1378
+ const part = await Session.updatePart(body)
1379
+ return c.json(part)
1380
+ },
1381
+ )
1382
+ .post(
1383
+ "/session/:sessionID/message",
1384
+ describeRoute({
1385
+ summary: "Send message",
1386
+ description: "Create and send a new message to a session, streaming the AI response.",
1387
+ operationId: "session.prompt",
1388
+ responses: {
1389
+ 200: {
1390
+ description: "Created message",
1391
+ content: {
1392
+ "application/json": {
1393
+ schema: resolver(
1394
+ z.object({
1395
+ info: MessageV2.Assistant,
1396
+ parts: MessageV2.Part.array(),
1397
+ }),
1398
+ ),
1399
+ },
1400
+ },
1401
+ },
1402
+ ...errors(400, 404),
1403
+ },
1404
+ }),
1405
+ validator(
1406
+ "param",
1407
+ z.object({
1408
+ sessionID: z.string().meta({ description: "Session ID" }),
1409
+ }),
1410
+ ),
1411
+ validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
1412
+ async (c) => {
1413
+ c.status(200)
1414
+ c.header("Content-Type", "application/json")
1415
+ return stream(c, async (stream) => {
1416
+ const sessionID = c.req.valid("param").sessionID
1417
+ const body = c.req.valid("json")
1418
+ const msg = await SessionPrompt.prompt({ ...body, sessionID })
1419
+ stream.write(JSON.stringify(msg))
1420
+ })
1421
+ },
1422
+ )
1423
+ .post(
1424
+ "/session/:sessionID/prompt_async",
1425
+ describeRoute({
1426
+ summary: "Send async message",
1427
+ description:
1428
+ "Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.",
1429
+ operationId: "session.prompt_async",
1430
+ responses: {
1431
+ 204: {
1432
+ description: "Prompt accepted",
1433
+ },
1434
+ ...errors(400, 404),
1435
+ },
1436
+ }),
1437
+ validator(
1438
+ "param",
1439
+ z.object({
1440
+ sessionID: z.string().meta({ description: "Session ID" }),
1441
+ }),
1442
+ ),
1443
+ validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
1444
+ async (c) => {
1445
+ c.status(204)
1446
+ c.header("Content-Type", "application/json")
1447
+ return stream(c, async () => {
1448
+ const sessionID = c.req.valid("param").sessionID
1449
+ const body = c.req.valid("json")
1450
+ SessionPrompt.prompt({ ...body, sessionID })
1451
+ })
1452
+ },
1453
+ )
1454
+ .post(
1455
+ "/session/:sessionID/command",
1456
+ describeRoute({
1457
+ summary: "Send command",
1458
+ description: "Send a new command to a session for execution by the AI assistant.",
1459
+ operationId: "session.command",
1460
+ responses: {
1461
+ 200: {
1462
+ description: "Created message",
1463
+ content: {
1464
+ "application/json": {
1465
+ schema: resolver(
1466
+ z.object({
1467
+ info: MessageV2.Assistant,
1468
+ parts: MessageV2.Part.array(),
1469
+ }),
1470
+ ),
1471
+ },
1472
+ },
1473
+ },
1474
+ ...errors(400, 404),
1475
+ },
1476
+ }),
1477
+ validator(
1478
+ "param",
1479
+ z.object({
1480
+ sessionID: z.string().meta({ description: "Session ID" }),
1481
+ }),
1482
+ ),
1483
+ validator("json", SessionPrompt.CommandInput.omit({ sessionID: true })),
1484
+ async (c) => {
1485
+ const sessionID = c.req.valid("param").sessionID
1486
+ const body = c.req.valid("json")
1487
+ const msg = await SessionPrompt.command({ ...body, sessionID })
1488
+ return c.json(msg)
1489
+ },
1490
+ )
1491
+ .post(
1492
+ "/session/:sessionID/shell",
1493
+ describeRoute({
1494
+ summary: "Run shell command",
1495
+ description: "Execute a shell command within the session context and return the AI's response.",
1496
+ operationId: "session.shell",
1497
+ responses: {
1498
+ 200: {
1499
+ description: "Created message",
1500
+ content: {
1501
+ "application/json": {
1502
+ schema: resolver(MessageV2.Assistant),
1503
+ },
1504
+ },
1505
+ },
1506
+ ...errors(400, 404),
1507
+ },
1508
+ }),
1509
+ validator(
1510
+ "param",
1511
+ z.object({
1512
+ sessionID: z.string().meta({ description: "Session ID" }),
1513
+ }),
1514
+ ),
1515
+ validator("json", SessionPrompt.ShellInput.omit({ sessionID: true })),
1516
+ async (c) => {
1517
+ const sessionID = c.req.valid("param").sessionID
1518
+ const body = c.req.valid("json")
1519
+ const msg = await SessionPrompt.shell({ ...body, sessionID })
1520
+ return c.json(msg)
1521
+ },
1522
+ )
1523
+ .post(
1524
+ "/session/:sessionID/revert",
1525
+ describeRoute({
1526
+ summary: "Revert message",
1527
+ description: "Revert a specific message in a session, undoing its effects and restoring the previous state.",
1528
+ operationId: "session.revert",
1529
+ responses: {
1530
+ 200: {
1531
+ description: "Updated session",
1532
+ content: {
1533
+ "application/json": {
1534
+ schema: resolver(Session.Info),
1535
+ },
1536
+ },
1537
+ },
1538
+ ...errors(400, 404),
1539
+ },
1540
+ }),
1541
+ validator(
1542
+ "param",
1543
+ z.object({
1544
+ sessionID: z.string(),
1545
+ }),
1546
+ ),
1547
+ validator("json", SessionRevert.RevertInput.omit({ sessionID: true })),
1548
+ async (c) => {
1549
+ const sessionID = c.req.valid("param").sessionID
1550
+ log.info("revert", c.req.valid("json"))
1551
+ const session = await SessionRevert.revert({
1552
+ sessionID,
1553
+ ...c.req.valid("json"),
1554
+ })
1555
+ return c.json(session)
1556
+ },
1557
+ )
1558
+ .post(
1559
+ "/session/:sessionID/unrevert",
1560
+ describeRoute({
1561
+ summary: "Restore reverted messages",
1562
+ description: "Restore all previously reverted messages in a session.",
1563
+ operationId: "session.unrevert",
1564
+ responses: {
1565
+ 200: {
1566
+ description: "Updated session",
1567
+ content: {
1568
+ "application/json": {
1569
+ schema: resolver(Session.Info),
1570
+ },
1571
+ },
1572
+ },
1573
+ ...errors(400, 404),
1574
+ },
1575
+ }),
1576
+ validator(
1577
+ "param",
1578
+ z.object({
1579
+ sessionID: z.string(),
1580
+ }),
1581
+ ),
1582
+ async (c) => {
1583
+ const sessionID = c.req.valid("param").sessionID
1584
+ const session = await SessionRevert.unrevert({ sessionID })
1585
+ return c.json(session)
1586
+ },
1587
+ )
1588
+ .post(
1589
+ "/session/:sessionID/permissions/:permissionID",
1590
+ describeRoute({
1591
+ summary: "Respond to permission",
1592
+ deprecated: true,
1593
+ description: "Approve or deny a permission request from the AI assistant.",
1594
+ operationId: "permission.respond",
1595
+ responses: {
1596
+ 200: {
1597
+ description: "Permission processed successfully",
1598
+ content: {
1599
+ "application/json": {
1600
+ schema: resolver(z.boolean()),
1601
+ },
1602
+ },
1603
+ },
1604
+ ...errors(400, 404),
1605
+ },
1606
+ }),
1607
+ validator(
1608
+ "param",
1609
+ z.object({
1610
+ sessionID: z.string(),
1611
+ permissionID: z.string(),
1612
+ }),
1613
+ ),
1614
+ validator("json", z.object({ response: PermissionNext.Reply })),
1615
+ async (c) => {
1616
+ const params = c.req.valid("param")
1617
+ PermissionNext.reply({
1618
+ requestID: params.permissionID,
1619
+ reply: c.req.valid("json").response,
1620
+ })
1621
+ return c.json(true)
1622
+ },
1623
+ )
1624
+ .post(
1625
+ "/permission/:requestID/reply",
1626
+ describeRoute({
1627
+ summary: "Respond to permission request",
1628
+ description: "Approve or deny a permission request from the AI assistant.",
1629
+ operationId: "permission.reply",
1630
+ responses: {
1631
+ 200: {
1632
+ description: "Permission processed successfully",
1633
+ content: {
1634
+ "application/json": {
1635
+ schema: resolver(z.boolean()),
1636
+ },
1637
+ },
1638
+ },
1639
+ ...errors(400, 404),
1640
+ },
1641
+ }),
1642
+ validator(
1643
+ "param",
1644
+ z.object({
1645
+ requestID: z.string(),
1646
+ }),
1647
+ ),
1648
+ validator("json", z.object({ reply: PermissionNext.Reply, message: z.string().optional() })),
1649
+ async (c) => {
1650
+ const params = c.req.valid("param")
1651
+ const json = c.req.valid("json")
1652
+ await PermissionNext.reply({
1653
+ requestID: params.requestID,
1654
+ reply: json.reply,
1655
+ message: json.message,
1656
+ })
1657
+ return c.json(true)
1658
+ },
1659
+ )
1660
+ .get(
1661
+ "/permission",
1662
+ describeRoute({
1663
+ summary: "List pending permissions",
1664
+ description: "Get all pending permission requests across all sessions.",
1665
+ operationId: "permission.list",
1666
+ responses: {
1667
+ 200: {
1668
+ description: "List of pending permissions",
1669
+ content: {
1670
+ "application/json": {
1671
+ schema: resolver(PermissionNext.Request.array()),
1672
+ },
1673
+ },
1674
+ },
1675
+ },
1676
+ }),
1677
+ async (c) => {
1678
+ const permissions = await PermissionNext.list()
1679
+ return c.json(permissions)
1680
+ },
1681
+ )
1682
+ .get(
1683
+ "/command",
1684
+ describeRoute({
1685
+ summary: "List commands",
1686
+ description: "Get a list of all available commands in the OpenCode system.",
1687
+ operationId: "command.list",
1688
+ responses: {
1689
+ 200: {
1690
+ description: "List of commands",
1691
+ content: {
1692
+ "application/json": {
1693
+ schema: resolver(Command.Info.array()),
1694
+ },
1695
+ },
1696
+ },
1697
+ },
1698
+ }),
1699
+ async (c) => {
1700
+ const commands = await Command.list()
1701
+ return c.json(commands)
1702
+ },
1703
+ )
1704
+ .get(
1705
+ "/config/providers",
1706
+ describeRoute({
1707
+ summary: "List config providers",
1708
+ description: "Get a list of all configured AI providers and their default models.",
1709
+ operationId: "config.providers",
1710
+ responses: {
1711
+ 200: {
1712
+ description: "List of providers",
1713
+ content: {
1714
+ "application/json": {
1715
+ schema: resolver(
1716
+ z.object({
1717
+ providers: Provider.Info.array(),
1718
+ default: z.record(z.string(), z.string()),
1719
+ }),
1720
+ ),
1721
+ },
1722
+ },
1723
+ },
1724
+ },
1725
+ }),
1726
+ async (c) => {
1727
+ using _ = log.time("providers")
1728
+ const providers = await Provider.list().then((x) => mapValues(x, (item) => item))
1729
+ return c.json({
1730
+ providers: Object.values(providers),
1731
+ default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
1732
+ })
1733
+ },
1734
+ )
1735
+ .get(
1736
+ "/provider",
1737
+ describeRoute({
1738
+ summary: "List providers",
1739
+ description: "Get a list of all available AI providers, including both available and connected ones.",
1740
+ operationId: "provider.list",
1741
+ responses: {
1742
+ 200: {
1743
+ description: "List of providers",
1744
+ content: {
1745
+ "application/json": {
1746
+ schema: resolver(
1747
+ z.object({
1748
+ all: ModelsDev.Provider.array(),
1749
+ default: z.record(z.string(), z.string()),
1750
+ connected: z.array(z.string()),
1751
+ }),
1752
+ ),
1753
+ },
1754
+ },
1755
+ },
1756
+ },
1757
+ }),
1758
+ async (c) => {
1759
+ const config = await Config.get()
1760
+ const disabled = new Set(config.disabled_providers ?? [])
1761
+ const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
1762
+
1763
+ const allProviders = await ModelsDev.get()
1764
+ const filteredProviders: Record<string, (typeof allProviders)[string]> = {}
1765
+ for (const [key, value] of Object.entries(allProviders)) {
1766
+ if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
1767
+ filteredProviders[key] = value
1768
+ }
1769
+ }
1770
+
1771
+ const connected = await Provider.list()
1772
+ const providers = Object.assign(
1773
+ mapValues(filteredProviders, (x) => Provider.fromModelsDevProvider(x)),
1774
+ connected,
1775
+ )
1776
+ return c.json({
1777
+ all: Object.values(providers),
1778
+ default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
1779
+ connected: Object.keys(connected),
1780
+ })
1781
+ },
1782
+ )
1783
+ .get(
1784
+ "/provider/auth",
1785
+ describeRoute({
1786
+ summary: "Get provider auth methods",
1787
+ description: "Retrieve available authentication methods for all AI providers.",
1788
+ operationId: "provider.auth",
1789
+ responses: {
1790
+ 200: {
1791
+ description: "Provider auth methods",
1792
+ content: {
1793
+ "application/json": {
1794
+ schema: resolver(z.record(z.string(), z.array(ProviderAuth.Method))),
1795
+ },
1796
+ },
1797
+ },
1798
+ },
1799
+ }),
1800
+ async (c) => {
1801
+ return c.json(await ProviderAuth.methods())
1802
+ },
1803
+ )
1804
+ .post(
1805
+ "/provider/:providerID/oauth/authorize",
1806
+ describeRoute({
1807
+ summary: "OAuth authorize",
1808
+ description: "Initiate OAuth authorization for a specific AI provider to get an authorization URL.",
1809
+ operationId: "provider.oauth.authorize",
1810
+ responses: {
1811
+ 200: {
1812
+ description: "Authorization URL and method",
1813
+ content: {
1814
+ "application/json": {
1815
+ schema: resolver(ProviderAuth.Authorization.optional()),
1816
+ },
1817
+ },
1818
+ },
1819
+ ...errors(400),
1820
+ },
1821
+ }),
1822
+ validator(
1823
+ "param",
1824
+ z.object({
1825
+ providerID: z.string().meta({ description: "Provider ID" }),
1826
+ }),
1827
+ ),
1828
+ validator(
1829
+ "json",
1830
+ z.object({
1831
+ method: z.number().meta({ description: "Auth method index" }),
1832
+ }),
1833
+ ),
1834
+ async (c) => {
1835
+ const providerID = c.req.valid("param").providerID
1836
+ const { method } = c.req.valid("json")
1837
+ const result = await ProviderAuth.authorize({
1838
+ providerID,
1839
+ method,
1840
+ })
1841
+ return c.json(result)
1842
+ },
1843
+ )
1844
+ .post(
1845
+ "/provider/:providerID/oauth/callback",
1846
+ describeRoute({
1847
+ summary: "OAuth callback",
1848
+ description: "Handle the OAuth callback from a provider after user authorization.",
1849
+ operationId: "provider.oauth.callback",
1850
+ responses: {
1851
+ 200: {
1852
+ description: "OAuth callback processed successfully",
1853
+ content: {
1854
+ "application/json": {
1855
+ schema: resolver(z.boolean()),
1856
+ },
1857
+ },
1858
+ },
1859
+ ...errors(400),
1860
+ },
1861
+ }),
1862
+ validator(
1863
+ "param",
1864
+ z.object({
1865
+ providerID: z.string().meta({ description: "Provider ID" }),
1866
+ }),
1867
+ ),
1868
+ validator(
1869
+ "json",
1870
+ z.object({
1871
+ method: z.number().meta({ description: "Auth method index" }),
1872
+ code: z.string().optional().meta({ description: "OAuth authorization code" }),
1873
+ }),
1874
+ ),
1875
+ async (c) => {
1876
+ const providerID = c.req.valid("param").providerID
1877
+ const { method, code } = c.req.valid("json")
1878
+ await ProviderAuth.callback({
1879
+ providerID,
1880
+ method,
1881
+ code,
1882
+ })
1883
+ return c.json(true)
1884
+ },
1885
+ )
1886
+ .get(
1887
+ "/find",
1888
+ describeRoute({
1889
+ summary: "Find text",
1890
+ description: "Search for text patterns across files in the project using ripgrep.",
1891
+ operationId: "find.text",
1892
+ responses: {
1893
+ 200: {
1894
+ description: "Matches",
1895
+ content: {
1896
+ "application/json": {
1897
+ schema: resolver(Ripgrep.Match.shape.data.array()),
1898
+ },
1899
+ },
1900
+ },
1901
+ },
1902
+ }),
1903
+ validator(
1904
+ "query",
1905
+ z.object({
1906
+ pattern: z.string(),
1907
+ }),
1908
+ ),
1909
+ async (c) => {
1910
+ const pattern = c.req.valid("query").pattern
1911
+ const result = await Ripgrep.search({
1912
+ cwd: Instance.directory,
1913
+ pattern,
1914
+ limit: 10,
1915
+ })
1916
+ return c.json(result)
1917
+ },
1918
+ )
1919
+ .get(
1920
+ "/find/file",
1921
+ describeRoute({
1922
+ summary: "Find files",
1923
+ description: "Search for files or directories by name or pattern in the project directory.",
1924
+ operationId: "find.files",
1925
+ responses: {
1926
+ 200: {
1927
+ description: "File paths",
1928
+ content: {
1929
+ "application/json": {
1930
+ schema: resolver(z.string().array()),
1931
+ },
1932
+ },
1933
+ },
1934
+ },
1935
+ }),
1936
+ validator(
1937
+ "query",
1938
+ z.object({
1939
+ query: z.string(),
1940
+ dirs: z.enum(["true", "false"]).optional(),
1941
+ type: z.enum(["file", "directory"]).optional(),
1942
+ limit: z.coerce.number().int().min(1).max(200).optional(),
1943
+ }),
1944
+ ),
1945
+ async (c) => {
1946
+ const query = c.req.valid("query").query
1947
+ const dirs = c.req.valid("query").dirs
1948
+ const type = c.req.valid("query").type
1949
+ const limit = c.req.valid("query").limit
1950
+ const results = await File.search({
1951
+ query,
1952
+ limit: limit ?? 10,
1953
+ dirs: dirs !== "false",
1954
+ type,
1955
+ })
1956
+ return c.json(results)
1957
+ },
1958
+ )
1959
+ .get(
1960
+ "/find/symbol",
1961
+ describeRoute({
1962
+ summary: "Find symbols",
1963
+ description: "Search for workspace symbols like functions, classes, and variables using LSP.",
1964
+ operationId: "find.symbols",
1965
+ responses: {
1966
+ 200: {
1967
+ description: "Symbols",
1968
+ content: {
1969
+ "application/json": {
1970
+ schema: resolver(LSP.Symbol.array()),
1971
+ },
1972
+ },
1973
+ },
1974
+ },
1975
+ }),
1976
+ validator(
1977
+ "query",
1978
+ z.object({
1979
+ query: z.string(),
1980
+ }),
1981
+ ),
1982
+ async (c) => {
1983
+ /*
1984
+ const query = c.req.valid("query").query
1985
+ const result = await LSP.workspaceSymbol(query)
1986
+ return c.json(result)
1987
+ */
1988
+ return c.json([])
1989
+ },
1990
+ )
1991
+ .get(
1992
+ "/file",
1993
+ describeRoute({
1994
+ summary: "List files",
1995
+ description: "List files and directories in a specified path.",
1996
+ operationId: "file.list",
1997
+ responses: {
1998
+ 200: {
1999
+ description: "Files and directories",
2000
+ content: {
2001
+ "application/json": {
2002
+ schema: resolver(File.Node.array()),
2003
+ },
2004
+ },
2005
+ },
2006
+ },
2007
+ }),
2008
+ validator(
2009
+ "query",
2010
+ z.object({
2011
+ path: z.string(),
2012
+ }),
2013
+ ),
2014
+ async (c) => {
2015
+ const path = c.req.valid("query").path
2016
+ const content = await File.list(path)
2017
+ return c.json(content)
2018
+ },
2019
+ )
2020
+ .get(
2021
+ "/file/content",
2022
+ describeRoute({
2023
+ summary: "Read file",
2024
+ description: "Read the content of a specified file.",
2025
+ operationId: "file.read",
2026
+ responses: {
2027
+ 200: {
2028
+ description: "File content",
2029
+ content: {
2030
+ "application/json": {
2031
+ schema: resolver(File.Content),
2032
+ },
2033
+ },
2034
+ },
2035
+ },
2036
+ }),
2037
+ validator(
2038
+ "query",
2039
+ z.object({
2040
+ path: z.string(),
2041
+ }),
2042
+ ),
2043
+ async (c) => {
2044
+ const path = c.req.valid("query").path
2045
+ const content = await File.read(path)
2046
+ return c.json(content)
2047
+ },
2048
+ )
2049
+ .get(
2050
+ "/file/status",
2051
+ describeRoute({
2052
+ summary: "Get file status",
2053
+ description: "Get the git status of all files in the project.",
2054
+ operationId: "file.status",
2055
+ responses: {
2056
+ 200: {
2057
+ description: "File status",
2058
+ content: {
2059
+ "application/json": {
2060
+ schema: resolver(File.Info.array()),
2061
+ },
2062
+ },
2063
+ },
2064
+ },
2065
+ }),
2066
+ async (c) => {
2067
+ const content = await File.status()
2068
+ return c.json(content)
2069
+ },
2070
+ )
2071
+ .post(
2072
+ "/log",
2073
+ describeRoute({
2074
+ summary: "Write log",
2075
+ description: "Write a log entry to the server logs with specified level and metadata.",
2076
+ operationId: "app.log",
2077
+ responses: {
2078
+ 200: {
2079
+ description: "Log entry written successfully",
2080
+ content: {
2081
+ "application/json": {
2082
+ schema: resolver(z.boolean()),
2083
+ },
2084
+ },
2085
+ },
2086
+ ...errors(400),
2087
+ },
2088
+ }),
2089
+ validator(
2090
+ "json",
2091
+ z.object({
2092
+ service: z.string().meta({ description: "Service name for the log entry" }),
2093
+ level: z.enum(["debug", "info", "error", "warn"]).meta({ description: "Log level" }),
2094
+ message: z.string().meta({ description: "Log message" }),
2095
+ extra: z
2096
+ .record(z.string(), z.any())
2097
+ .optional()
2098
+ .meta({ description: "Additional metadata for the log entry" }),
2099
+ }),
2100
+ ),
2101
+ async (c) => {
2102
+ const { service, level, message, extra } = c.req.valid("json")
2103
+ const logger = Log.create({ service })
2104
+
2105
+ switch (level) {
2106
+ case "debug":
2107
+ logger.debug(message, extra)
2108
+ break
2109
+ case "info":
2110
+ logger.info(message, extra)
2111
+ break
2112
+ case "error":
2113
+ logger.error(message, extra)
2114
+ break
2115
+ case "warn":
2116
+ logger.warn(message, extra)
2117
+ break
2118
+ }
2119
+
2120
+ return c.json(true)
2121
+ },
2122
+ )
2123
+ .get(
2124
+ "/agent",
2125
+ describeRoute({
2126
+ summary: "List agents",
2127
+ description: "Get a list of all available AI agents in the OpenCode system.",
2128
+ operationId: "app.agents",
2129
+ responses: {
2130
+ 200: {
2131
+ description: "List of agents",
2132
+ content: {
2133
+ "application/json": {
2134
+ schema: resolver(Agent.Info.array()),
2135
+ },
2136
+ },
2137
+ },
2138
+ },
2139
+ }),
2140
+ async (c) => {
2141
+ const modes = await Agent.list()
2142
+ return c.json(modes)
2143
+ },
2144
+ )
2145
+ .get(
2146
+ "/mcp",
2147
+ describeRoute({
2148
+ summary: "Get MCP status",
2149
+ description: "Get the status of all Model Context Protocol (MCP) servers.",
2150
+ operationId: "mcp.status",
2151
+ responses: {
2152
+ 200: {
2153
+ description: "MCP server status",
2154
+ content: {
2155
+ "application/json": {
2156
+ schema: resolver(z.record(z.string(), MCP.Status)),
2157
+ },
2158
+ },
2159
+ },
2160
+ },
2161
+ }),
2162
+ async (c) => {
2163
+ return c.json(await MCP.status())
2164
+ },
2165
+ )
2166
+ .post(
2167
+ "/mcp",
2168
+ describeRoute({
2169
+ summary: "Add MCP server",
2170
+ description: "Dynamically add a new Model Context Protocol (MCP) server to the system.",
2171
+ operationId: "mcp.add",
2172
+ responses: {
2173
+ 200: {
2174
+ description: "MCP server added successfully",
2175
+ content: {
2176
+ "application/json": {
2177
+ schema: resolver(z.record(z.string(), MCP.Status)),
2178
+ },
2179
+ },
2180
+ },
2181
+ ...errors(400),
2182
+ },
2183
+ }),
2184
+ validator(
2185
+ "json",
2186
+ z.object({
2187
+ name: z.string(),
2188
+ config: Config.Mcp,
2189
+ }),
2190
+ ),
2191
+ async (c) => {
2192
+ const { name, config } = c.req.valid("json")
2193
+ const result = await MCP.add(name, config)
2194
+ return c.json(result.status)
2195
+ },
2196
+ )
2197
+ .post(
2198
+ "/mcp/:name/auth",
2199
+ describeRoute({
2200
+ summary: "Start MCP OAuth",
2201
+ description: "Start OAuth authentication flow for a Model Context Protocol (MCP) server.",
2202
+ operationId: "mcp.auth.start",
2203
+ responses: {
2204
+ 200: {
2205
+ description: "OAuth flow started",
2206
+ content: {
2207
+ "application/json": {
2208
+ schema: resolver(
2209
+ z.object({
2210
+ authorizationUrl: z.string().describe("URL to open in browser for authorization"),
2211
+ }),
2212
+ ),
2213
+ },
2214
+ },
2215
+ },
2216
+ ...errors(400, 404),
2217
+ },
2218
+ }),
2219
+ async (c) => {
2220
+ const name = c.req.param("name")
2221
+ const supportsOAuth = await MCP.supportsOAuth(name)
2222
+ if (!supportsOAuth) {
2223
+ return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
2224
+ }
2225
+ const result = await MCP.startAuth(name)
2226
+ return c.json(result)
2227
+ },
2228
+ )
2229
+ .post(
2230
+ "/mcp/:name/auth/callback",
2231
+ describeRoute({
2232
+ summary: "Complete MCP OAuth",
2233
+ description:
2234
+ "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.",
2235
+ operationId: "mcp.auth.callback",
2236
+ responses: {
2237
+ 200: {
2238
+ description: "OAuth authentication completed",
2239
+ content: {
2240
+ "application/json": {
2241
+ schema: resolver(MCP.Status),
2242
+ },
2243
+ },
2244
+ },
2245
+ ...errors(400, 404),
2246
+ },
2247
+ }),
2248
+ validator(
2249
+ "json",
2250
+ z.object({
2251
+ code: z.string().describe("Authorization code from OAuth callback"),
2252
+ }),
2253
+ ),
2254
+ async (c) => {
2255
+ const name = c.req.param("name")
2256
+ const { code } = c.req.valid("json")
2257
+ const status = await MCP.finishAuth(name, code)
2258
+ return c.json(status)
2259
+ },
2260
+ )
2261
+ .post(
2262
+ "/mcp/:name/auth/authenticate",
2263
+ describeRoute({
2264
+ summary: "Authenticate MCP OAuth",
2265
+ description: "Start OAuth flow and wait for callback (opens browser)",
2266
+ operationId: "mcp.auth.authenticate",
2267
+ responses: {
2268
+ 200: {
2269
+ description: "OAuth authentication completed",
2270
+ content: {
2271
+ "application/json": {
2272
+ schema: resolver(MCP.Status),
2273
+ },
2274
+ },
2275
+ },
2276
+ ...errors(400, 404),
2277
+ },
2278
+ }),
2279
+ async (c) => {
2280
+ const name = c.req.param("name")
2281
+ const supportsOAuth = await MCP.supportsOAuth(name)
2282
+ if (!supportsOAuth) {
2283
+ return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
2284
+ }
2285
+ const status = await MCP.authenticate(name)
2286
+ return c.json(status)
2287
+ },
2288
+ )
2289
+ .delete(
2290
+ "/mcp/:name/auth",
2291
+ describeRoute({
2292
+ summary: "Remove MCP OAuth",
2293
+ description: "Remove OAuth credentials for an MCP server",
2294
+ operationId: "mcp.auth.remove",
2295
+ responses: {
2296
+ 200: {
2297
+ description: "OAuth credentials removed",
2298
+ content: {
2299
+ "application/json": {
2300
+ schema: resolver(z.object({ success: z.literal(true) })),
2301
+ },
2302
+ },
2303
+ },
2304
+ ...errors(404),
2305
+ },
2306
+ }),
2307
+ async (c) => {
2308
+ const name = c.req.param("name")
2309
+ await MCP.removeAuth(name)
2310
+ return c.json({ success: true as const })
2311
+ },
2312
+ )
2313
+ .post(
2314
+ "/mcp/:name/connect",
2315
+ describeRoute({
2316
+ description: "Connect an MCP server",
2317
+ operationId: "mcp.connect",
2318
+ responses: {
2319
+ 200: {
2320
+ description: "MCP server connected successfully",
2321
+ content: {
2322
+ "application/json": {
2323
+ schema: resolver(z.boolean()),
2324
+ },
2325
+ },
2326
+ },
2327
+ },
2328
+ }),
2329
+ validator("param", z.object({ name: z.string() })),
2330
+ async (c) => {
2331
+ const { name } = c.req.valid("param")
2332
+ await MCP.connect(name)
2333
+ return c.json(true)
2334
+ },
2335
+ )
2336
+ .post(
2337
+ "/mcp/:name/disconnect",
2338
+ describeRoute({
2339
+ description: "Disconnect an MCP server",
2340
+ operationId: "mcp.disconnect",
2341
+ responses: {
2342
+ 200: {
2343
+ description: "MCP server disconnected successfully",
2344
+ content: {
2345
+ "application/json": {
2346
+ schema: resolver(z.boolean()),
2347
+ },
2348
+ },
2349
+ },
2350
+ },
2351
+ }),
2352
+ validator("param", z.object({ name: z.string() })),
2353
+ async (c) => {
2354
+ const { name } = c.req.valid("param")
2355
+ await MCP.disconnect(name)
2356
+ return c.json(true)
2357
+ },
2358
+ )
2359
+ .get(
2360
+ "/experimental/resource",
2361
+ describeRoute({
2362
+ summary: "Get MCP resources",
2363
+ description: "Get all available MCP resources from connected servers. Optionally filter by name.",
2364
+ operationId: "experimental.resource.list",
2365
+ responses: {
2366
+ 200: {
2367
+ description: "MCP resources",
2368
+ content: {
2369
+ "application/json": {
2370
+ schema: resolver(z.record(z.string(), MCP.Resource)),
2371
+ },
2372
+ },
2373
+ },
2374
+ },
2375
+ }),
2376
+ async (c) => {
2377
+ return c.json(await MCP.resources())
2378
+ },
2379
+ )
2380
+ .get(
2381
+ "/lsp",
2382
+ describeRoute({
2383
+ summary: "Get LSP status",
2384
+ description: "Get LSP server status",
2385
+ operationId: "lsp.status",
2386
+ responses: {
2387
+ 200: {
2388
+ description: "LSP server status",
2389
+ content: {
2390
+ "application/json": {
2391
+ schema: resolver(LSP.Status.array()),
2392
+ },
2393
+ },
2394
+ },
2395
+ },
2396
+ }),
2397
+ async (c) => {
2398
+ return c.json(await LSP.status())
2399
+ },
2400
+ )
2401
+ .get(
2402
+ "/formatter",
2403
+ describeRoute({
2404
+ summary: "Get formatter status",
2405
+ description: "Get formatter status",
2406
+ operationId: "formatter.status",
2407
+ responses: {
2408
+ 200: {
2409
+ description: "Formatter status",
2410
+ content: {
2411
+ "application/json": {
2412
+ schema: resolver(Format.Status.array()),
2413
+ },
2414
+ },
2415
+ },
2416
+ },
2417
+ }),
2418
+ async (c) => {
2419
+ return c.json(await Format.status())
2420
+ },
2421
+ )
2422
+ .post(
2423
+ "/tui/append-prompt",
2424
+ describeRoute({
2425
+ summary: "Append TUI prompt",
2426
+ description: "Append prompt to the TUI",
2427
+ operationId: "tui.appendPrompt",
2428
+ responses: {
2429
+ 200: {
2430
+ description: "Prompt processed successfully",
2431
+ content: {
2432
+ "application/json": {
2433
+ schema: resolver(z.boolean()),
2434
+ },
2435
+ },
2436
+ },
2437
+ ...errors(400),
2438
+ },
2439
+ }),
2440
+ validator("json", TuiEvent.PromptAppend.properties),
2441
+ async (c) => {
2442
+ await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json"))
2443
+ return c.json(true)
2444
+ },
2445
+ )
2446
+ .post(
2447
+ "/tui/open-help",
2448
+ describeRoute({
2449
+ summary: "Open help dialog",
2450
+ description: "Open the help dialog in the TUI to display user assistance information.",
2451
+ operationId: "tui.openHelp",
2452
+ responses: {
2453
+ 200: {
2454
+ description: "Help dialog opened successfully",
2455
+ content: {
2456
+ "application/json": {
2457
+ schema: resolver(z.boolean()),
2458
+ },
2459
+ },
2460
+ },
2461
+ },
2462
+ }),
2463
+ async (c) => {
2464
+ // TODO: open dialog
2465
+ return c.json(true)
2466
+ },
2467
+ )
2468
+ .post(
2469
+ "/tui/open-sessions",
2470
+ describeRoute({
2471
+ summary: "Open sessions dialog",
2472
+ description: "Open the session dialog",
2473
+ operationId: "tui.openSessions",
2474
+ responses: {
2475
+ 200: {
2476
+ description: "Session dialog opened successfully",
2477
+ content: {
2478
+ "application/json": {
2479
+ schema: resolver(z.boolean()),
2480
+ },
2481
+ },
2482
+ },
2483
+ },
2484
+ }),
2485
+ async (c) => {
2486
+ await Bus.publish(TuiEvent.CommandExecute, {
2487
+ command: "session.list",
2488
+ })
2489
+ return c.json(true)
2490
+ },
2491
+ )
2492
+ .post(
2493
+ "/tui/open-themes",
2494
+ describeRoute({
2495
+ summary: "Open themes dialog",
2496
+ description: "Open the theme dialog",
2497
+ operationId: "tui.openThemes",
2498
+ responses: {
2499
+ 200: {
2500
+ description: "Theme dialog opened successfully",
2501
+ content: {
2502
+ "application/json": {
2503
+ schema: resolver(z.boolean()),
2504
+ },
2505
+ },
2506
+ },
2507
+ },
2508
+ }),
2509
+ async (c) => {
2510
+ await Bus.publish(TuiEvent.CommandExecute, {
2511
+ command: "session.list",
2512
+ })
2513
+ return c.json(true)
2514
+ },
2515
+ )
2516
+ .post(
2517
+ "/tui/open-models",
2518
+ describeRoute({
2519
+ summary: "Open models dialog",
2520
+ description: "Open the model dialog",
2521
+ operationId: "tui.openModels",
2522
+ responses: {
2523
+ 200: {
2524
+ description: "Model dialog opened successfully",
2525
+ content: {
2526
+ "application/json": {
2527
+ schema: resolver(z.boolean()),
2528
+ },
2529
+ },
2530
+ },
2531
+ },
2532
+ }),
2533
+ async (c) => {
2534
+ await Bus.publish(TuiEvent.CommandExecute, {
2535
+ command: "model.list",
2536
+ })
2537
+ return c.json(true)
2538
+ },
2539
+ )
2540
+ .post(
2541
+ "/tui/submit-prompt",
2542
+ describeRoute({
2543
+ summary: "Submit TUI prompt",
2544
+ description: "Submit the prompt",
2545
+ operationId: "tui.submitPrompt",
2546
+ responses: {
2547
+ 200: {
2548
+ description: "Prompt submitted successfully",
2549
+ content: {
2550
+ "application/json": {
2551
+ schema: resolver(z.boolean()),
2552
+ },
2553
+ },
2554
+ },
2555
+ },
2556
+ }),
2557
+ async (c) => {
2558
+ await Bus.publish(TuiEvent.CommandExecute, {
2559
+ command: "prompt.submit",
2560
+ })
2561
+ return c.json(true)
2562
+ },
2563
+ )
2564
+ .post(
2565
+ "/tui/clear-prompt",
2566
+ describeRoute({
2567
+ summary: "Clear TUI prompt",
2568
+ description: "Clear the prompt",
2569
+ operationId: "tui.clearPrompt",
2570
+ responses: {
2571
+ 200: {
2572
+ description: "Prompt cleared successfully",
2573
+ content: {
2574
+ "application/json": {
2575
+ schema: resolver(z.boolean()),
2576
+ },
2577
+ },
2578
+ },
2579
+ },
2580
+ }),
2581
+ async (c) => {
2582
+ await Bus.publish(TuiEvent.CommandExecute, {
2583
+ command: "prompt.clear",
2584
+ })
2585
+ return c.json(true)
2586
+ },
2587
+ )
2588
+ .post(
2589
+ "/tui/execute-command",
2590
+ describeRoute({
2591
+ summary: "Execute TUI command",
2592
+ description: "Execute a TUI command (e.g. agent_cycle)",
2593
+ operationId: "tui.executeCommand",
2594
+ responses: {
2595
+ 200: {
2596
+ description: "Command executed successfully",
2597
+ content: {
2598
+ "application/json": {
2599
+ schema: resolver(z.boolean()),
2600
+ },
2601
+ },
2602
+ },
2603
+ ...errors(400),
2604
+ },
2605
+ }),
2606
+ validator("json", z.object({ command: z.string() })),
2607
+ async (c) => {
2608
+ const command = c.req.valid("json").command
2609
+ await Bus.publish(TuiEvent.CommandExecute, {
2610
+ // @ts-expect-error
2611
+ command: {
2612
+ session_new: "session.new",
2613
+ session_share: "session.share",
2614
+ session_interrupt: "session.interrupt",
2615
+ session_compact: "session.compact",
2616
+ messages_page_up: "session.page.up",
2617
+ messages_page_down: "session.page.down",
2618
+ messages_half_page_up: "session.half.page.up",
2619
+ messages_half_page_down: "session.half.page.down",
2620
+ messages_first: "session.first",
2621
+ messages_last: "session.last",
2622
+ agent_cycle: "agent.cycle",
2623
+ }[command],
2624
+ })
2625
+ return c.json(true)
2626
+ },
2627
+ )
2628
+ .post(
2629
+ "/tui/show-toast",
2630
+ describeRoute({
2631
+ summary: "Show TUI toast",
2632
+ description: "Show a toast notification in the TUI",
2633
+ operationId: "tui.showToast",
2634
+ responses: {
2635
+ 200: {
2636
+ description: "Toast notification shown successfully",
2637
+ content: {
2638
+ "application/json": {
2639
+ schema: resolver(z.boolean()),
2640
+ },
2641
+ },
2642
+ },
2643
+ },
2644
+ }),
2645
+ validator("json", TuiEvent.ToastShow.properties),
2646
+ async (c) => {
2647
+ await Bus.publish(TuiEvent.ToastShow, c.req.valid("json"))
2648
+ return c.json(true)
2649
+ },
2650
+ )
2651
+ .post(
2652
+ "/tui/publish",
2653
+ describeRoute({
2654
+ summary: "Publish TUI event",
2655
+ description: "Publish a TUI event",
2656
+ operationId: "tui.publish",
2657
+ responses: {
2658
+ 200: {
2659
+ description: "Event published successfully",
2660
+ content: {
2661
+ "application/json": {
2662
+ schema: resolver(z.boolean()),
2663
+ },
2664
+ },
2665
+ },
2666
+ ...errors(400),
2667
+ },
2668
+ }),
2669
+ validator(
2670
+ "json",
2671
+ z.union(
2672
+ Object.values(TuiEvent).map((def) => {
2673
+ return z
2674
+ .object({
2675
+ type: z.literal(def.type),
2676
+ properties: def.properties,
2677
+ })
2678
+ .meta({
2679
+ ref: "Event" + "." + def.type,
2680
+ })
2681
+ }),
2682
+ ),
2683
+ ),
2684
+ async (c) => {
2685
+ const evt = c.req.valid("json")
2686
+ await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type)!, evt.properties)
2687
+ return c.json(true)
2688
+ },
2689
+ )
2690
+ .post(
2691
+ "/tui/select-session",
2692
+ describeRoute({
2693
+ summary: "Select session",
2694
+ description: "Navigate the TUI to display the specified session.",
2695
+ operationId: "tui.selectSession",
2696
+ responses: {
2697
+ 200: {
2698
+ description: "Session selected successfully",
2699
+ content: {
2700
+ "application/json": {
2701
+ schema: resolver(z.boolean()),
2702
+ },
2703
+ },
2704
+ },
2705
+ ...errors(400, 404),
2706
+ },
2707
+ }),
2708
+ validator("json", TuiEvent.SessionSelect.properties),
2709
+ async (c) => {
2710
+ const { sessionID } = c.req.valid("json")
2711
+ await Session.get(sessionID)
2712
+ await Bus.publish(TuiEvent.SessionSelect, { sessionID })
2713
+ return c.json(true)
2714
+ },
2715
+ )
2716
+ .route("/tui/control", TuiRoute)
2717
+ .put(
2718
+ "/auth/:providerID",
2719
+ describeRoute({
2720
+ summary: "Set auth credentials",
2721
+ description: "Set authentication credentials",
2722
+ operationId: "auth.set",
2723
+ responses: {
2724
+ 200: {
2725
+ description: "Successfully set authentication credentials",
2726
+ content: {
2727
+ "application/json": {
2728
+ schema: resolver(z.boolean()),
2729
+ },
2730
+ },
2731
+ },
2732
+ ...errors(400),
2733
+ },
2734
+ }),
2735
+ validator(
2736
+ "param",
2737
+ z.object({
2738
+ providerID: z.string(),
2739
+ }),
2740
+ ),
2741
+ validator("json", Auth.Info),
2742
+ async (c) => {
2743
+ const providerID = c.req.valid("param").providerID
2744
+ const info = c.req.valid("json")
2745
+ await Auth.set(providerID, info)
2746
+ return c.json(true)
2747
+ },
2748
+ )
2749
+ .get(
2750
+ "/event",
2751
+ describeRoute({
2752
+ summary: "Subscribe to events",
2753
+ description: "Get events",
2754
+ operationId: "event.subscribe",
2755
+ responses: {
2756
+ 200: {
2757
+ description: "Event stream",
2758
+ content: {
2759
+ "text/event-stream": {
2760
+ schema: resolver(BusEvent.payloads()),
2761
+ },
2762
+ },
2763
+ },
2764
+ },
2765
+ }),
2766
+ async (c) => {
2767
+ log.info("event connected")
2768
+ return streamSSE(c, async (stream) => {
2769
+ stream.writeSSE({
2770
+ data: JSON.stringify({
2771
+ type: "server.connected",
2772
+ properties: {},
2773
+ }),
2774
+ })
2775
+ const unsub = Bus.subscribeAll(async (event) => {
2776
+ await stream.writeSSE({
2777
+ data: JSON.stringify(event),
2778
+ })
2779
+ if (event.type === Bus.InstanceDisposed.type) {
2780
+ stream.close()
2781
+ }
2782
+ })
2783
+
2784
+ // Send heartbeat every 30s to prevent WKWebView timeout (60s default)
2785
+ const heartbeat = setInterval(() => {
2786
+ stream.writeSSE({
2787
+ data: JSON.stringify({
2788
+ type: "server.heartbeat",
2789
+ properties: {},
2790
+ }),
2791
+ })
2792
+ }, 30000)
2793
+
2794
+ await new Promise<void>((resolve) => {
2795
+ stream.onAbort(() => {
2796
+ clearInterval(heartbeat)
2797
+ unsub()
2798
+ resolve()
2799
+ log.info("event disconnected")
2800
+ })
2801
+ })
2802
+ })
2803
+ },
2804
+ )
2805
+ // Catch-all route - return 404 for unmatched routes
2806
+ .all("/*", async (c) => {
2807
+ return c.json({ error: "Not found" }, 404)
2808
+ }),
2809
+ )
2810
+
2811
+ export async function openapi() {
2812
+ const result = await generateSpecs(App(), {
2813
+ documentation: {
2814
+ info: {
2815
+ title: "opencode",
2816
+ version: "1.0.0",
2817
+ description: "opencode api",
2818
+ },
2819
+ openapi: "3.1.1",
2820
+ },
2821
+ })
2822
+ return result
2823
+ }
2824
+
2825
+ export function listen(opts: { port: number; hostname: string; mdns?: boolean; cors?: string[] }) {
2826
+ _corsWhitelist = opts.cors ?? []
2827
+
2828
+ const args = {
2829
+ hostname: opts.hostname,
2830
+ idleTimeout: 0,
2831
+ fetch: App().fetch,
2832
+ websocket: websocket,
2833
+ } as const
2834
+ const tryServe = (port: number) => {
2835
+ try {
2836
+ return Bun.serve({ ...args, port })
2837
+ } catch {
2838
+ return undefined
2839
+ }
2840
+ }
2841
+ const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port)
2842
+ if (!server) throw new Error(`Failed to start server on port ${opts.port}`)
2843
+
2844
+ _url = server.url
2845
+
2846
+ const shouldPublishMDNS =
2847
+ opts.mdns &&
2848
+ server.port &&
2849
+ opts.hostname !== "127.0.0.1" &&
2850
+ opts.hostname !== "localhost" &&
2851
+ opts.hostname !== "::1"
2852
+ if (shouldPublishMDNS) {
2853
+ MDNS.publish(server.port!, `opencode-${server.port!}`)
2854
+ } else if (opts.mdns) {
2855
+ log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
2856
+ }
2857
+
2858
+ const originalStop = server.stop.bind(server)
2859
+ server.stop = async (closeActiveConnections?: boolean) => {
2860
+ if (shouldPublishMDNS) MDNS.unpublish()
2861
+ return originalStop(closeActiveConnections)
2862
+ }
2863
+
2864
+ return server
2865
+ }
2866
+ }