@stonerzju/opencode 1.2.16-offline.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 (496) hide show
  1. package/AGENTS.md +10 -0
  2. package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
  3. package/Dockerfile +18 -0
  4. package/README.md +15 -0
  5. package/bin/opencode +179 -0
  6. package/bunfig.toml +7 -0
  7. package/drizzle.config.ts +10 -0
  8. package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
  9. package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
  10. package/migration/20260211171708_add_project_commands/migration.sql +1 -0
  11. package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
  12. package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
  13. package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
  14. package/migration/20260225215848_workspace/migration.sql +7 -0
  15. package/migration/20260225215848_workspace/snapshot.json +959 -0
  16. package/package.json +140 -0
  17. package/package.json.bak +140 -0
  18. package/parsers-config.ts +254 -0
  19. package/script/build.ts +224 -0
  20. package/script/check-migrations.ts +16 -0
  21. package/script/postinstall.mjs +131 -0
  22. package/script/publish.ts +181 -0
  23. package/script/schema.ts +63 -0
  24. package/script/seed-e2e.ts +50 -0
  25. package/src/acp/README.md +174 -0
  26. package/src/acp/agent.ts +1741 -0
  27. package/src/acp/session.ts +116 -0
  28. package/src/acp/types.ts +23 -0
  29. package/src/agent/agent.ts +339 -0
  30. package/src/agent/generate.txt +75 -0
  31. package/src/agent/prompt/compaction.txt +14 -0
  32. package/src/agent/prompt/explore.txt +18 -0
  33. package/src/agent/prompt/summary.txt +11 -0
  34. package/src/agent/prompt/title.txt +44 -0
  35. package/src/auth/index.ts +68 -0
  36. package/src/bun/index.ts +131 -0
  37. package/src/bun/registry.ts +50 -0
  38. package/src/bus/bus-event.ts +43 -0
  39. package/src/bus/global.ts +10 -0
  40. package/src/bus/index.ts +105 -0
  41. package/src/cli/bootstrap.ts +17 -0
  42. package/src/cli/cmd/acp.ts +70 -0
  43. package/src/cli/cmd/agent.ts +257 -0
  44. package/src/cli/cmd/auth.ts +449 -0
  45. package/src/cli/cmd/cmd.ts +7 -0
  46. package/src/cli/cmd/db.ts +118 -0
  47. package/src/cli/cmd/debug/agent.ts +167 -0
  48. package/src/cli/cmd/debug/config.ts +16 -0
  49. package/src/cli/cmd/debug/file.ts +97 -0
  50. package/src/cli/cmd/debug/index.ts +48 -0
  51. package/src/cli/cmd/debug/lsp.ts +52 -0
  52. package/src/cli/cmd/debug/ripgrep.ts +87 -0
  53. package/src/cli/cmd/debug/scrap.ts +16 -0
  54. package/src/cli/cmd/debug/skill.ts +16 -0
  55. package/src/cli/cmd/debug/snapshot.ts +52 -0
  56. package/src/cli/cmd/export.ts +88 -0
  57. package/src/cli/cmd/generate.ts +38 -0
  58. package/src/cli/cmd/github.ts +1631 -0
  59. package/src/cli/cmd/import.ts +170 -0
  60. package/src/cli/cmd/mcp.ts +754 -0
  61. package/src/cli/cmd/models.ts +77 -0
  62. package/src/cli/cmd/pr.ts +112 -0
  63. package/src/cli/cmd/run.ts +625 -0
  64. package/src/cli/cmd/serve.ts +31 -0
  65. package/src/cli/cmd/session.ts +156 -0
  66. package/src/cli/cmd/stats.ts +410 -0
  67. package/src/cli/cmd/tui/app.tsx +845 -0
  68. package/src/cli/cmd/tui/attach.ts +88 -0
  69. package/src/cli/cmd/tui/component/border.tsx +21 -0
  70. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  71. package/src/cli/cmd/tui/component/dialog-command.tsx +147 -0
  72. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  73. package/src/cli/cmd/tui/component/dialog-model.tsx +165 -0
  74. package/src/cli/cmd/tui/component/dialog-provider.tsx +259 -0
  75. package/src/cli/cmd/tui/component/dialog-session-list.tsx +108 -0
  76. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  77. package/src/cli/cmd/tui/component/dialog-skill.tsx +36 -0
  78. package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
  79. package/src/cli/cmd/tui/component/dialog-status.tsx +167 -0
  80. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  81. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  82. package/src/cli/cmd/tui/component/logo.tsx +85 -0
  83. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +667 -0
  84. package/src/cli/cmd/tui/component/prompt/frecency.tsx +90 -0
  85. package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
  86. package/src/cli/cmd/tui/component/prompt/index.tsx +1155 -0
  87. package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
  88. package/src/cli/cmd/tui/component/spinner.tsx +24 -0
  89. package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
  90. package/src/cli/cmd/tui/component/tips.tsx +152 -0
  91. package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
  92. package/src/cli/cmd/tui/context/args.tsx +15 -0
  93. package/src/cli/cmd/tui/context/directory.ts +13 -0
  94. package/src/cli/cmd/tui/context/exit.tsx +53 -0
  95. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  96. package/src/cli/cmd/tui/context/keybind.tsx +102 -0
  97. package/src/cli/cmd/tui/context/kv.tsx +52 -0
  98. package/src/cli/cmd/tui/context/local.tsx +406 -0
  99. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  100. package/src/cli/cmd/tui/context/route.tsx +46 -0
  101. package/src/cli/cmd/tui/context/sdk.tsx +101 -0
  102. package/src/cli/cmd/tui/context/sync.tsx +488 -0
  103. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  104. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  105. package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
  106. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
  107. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  108. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  109. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  110. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  111. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  112. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  113. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  114. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  115. package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
  116. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  117. package/src/cli/cmd/tui/context/theme/lucent-orng.json +237 -0
  118. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  119. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  120. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  121. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  122. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  123. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  124. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  125. package/src/cli/cmd/tui/context/theme/orng.json +249 -0
  126. package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
  127. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  128. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  129. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  130. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  131. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  132. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  133. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  134. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  135. package/src/cli/cmd/tui/context/theme.tsx +1152 -0
  136. package/src/cli/cmd/tui/context/tui-config.tsx +9 -0
  137. package/src/cli/cmd/tui/event.ts +48 -0
  138. package/src/cli/cmd/tui/routes/home.tsx +145 -0
  139. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
  140. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
  141. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
  142. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  143. package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
  144. package/src/cli/cmd/tui/routes/session/header.tsx +135 -0
  145. package/src/cli/cmd/tui/routes/session/index.tsx +2219 -0
  146. package/src/cli/cmd/tui/routes/session/permission.tsx +685 -0
  147. package/src/cli/cmd/tui/routes/session/question.tsx +466 -0
  148. package/src/cli/cmd/tui/routes/session/sidebar.tsx +321 -0
  149. package/src/cli/cmd/tui/thread.ts +199 -0
  150. package/src/cli/cmd/tui/ui/dialog-alert.tsx +59 -0
  151. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +85 -0
  152. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +207 -0
  153. package/src/cli/cmd/tui/ui/dialog-help.tsx +40 -0
  154. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +80 -0
  155. package/src/cli/cmd/tui/ui/dialog-select.tsx +401 -0
  156. package/src/cli/cmd/tui/ui/dialog.tsx +182 -0
  157. package/src/cli/cmd/tui/ui/link.tsx +28 -0
  158. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  159. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  160. package/src/cli/cmd/tui/util/clipboard.ts +164 -0
  161. package/src/cli/cmd/tui/util/editor.ts +33 -0
  162. package/src/cli/cmd/tui/util/selection.ts +25 -0
  163. package/src/cli/cmd/tui/util/signal.ts +7 -0
  164. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  165. package/src/cli/cmd/tui/util/transcript.ts +98 -0
  166. package/src/cli/cmd/tui/win32.ts +129 -0
  167. package/src/cli/cmd/tui/worker.ts +157 -0
  168. package/src/cli/cmd/uninstall.ts +356 -0
  169. package/src/cli/cmd/upgrade.ts +73 -0
  170. package/src/cli/cmd/web.ts +81 -0
  171. package/src/cli/cmd/workspace-serve.ts +16 -0
  172. package/src/cli/error.ts +57 -0
  173. package/src/cli/logo.ts +6 -0
  174. package/src/cli/network.ts +60 -0
  175. package/src/cli/ui.ts +116 -0
  176. package/src/cli/upgrade.ts +25 -0
  177. package/src/command/index.ts +150 -0
  178. package/src/command/template/initialize.txt +10 -0
  179. package/src/command/template/review.txt +101 -0
  180. package/src/config/config.ts +1408 -0
  181. package/src/config/markdown.ts +99 -0
  182. package/src/config/migrate-tui-config.ts +155 -0
  183. package/src/config/paths.ts +174 -0
  184. package/src/config/tui-schema.ts +34 -0
  185. package/src/config/tui.ts +118 -0
  186. package/src/control/control.sql.ts +22 -0
  187. package/src/control/index.ts +67 -0
  188. package/src/control-plane/adaptors/index.ts +10 -0
  189. package/src/control-plane/adaptors/types.ts +7 -0
  190. package/src/control-plane/adaptors/worktree.ts +26 -0
  191. package/src/control-plane/config.ts +10 -0
  192. package/src/control-plane/session-proxy-middleware.ts +46 -0
  193. package/src/control-plane/sse.ts +66 -0
  194. package/src/control-plane/workspace-server/routes.ts +33 -0
  195. package/src/control-plane/workspace-server/server.ts +24 -0
  196. package/src/control-plane/workspace.sql.ts +12 -0
  197. package/src/control-plane/workspace.ts +160 -0
  198. package/src/env/index.ts +28 -0
  199. package/src/file/ignore.ts +82 -0
  200. package/src/file/index.ts +646 -0
  201. package/src/file/ripgrep.ts +372 -0
  202. package/src/file/time.ts +71 -0
  203. package/src/file/watcher.ts +128 -0
  204. package/src/flag/flag.ts +109 -0
  205. package/src/format/formatter.ts +395 -0
  206. package/src/format/index.ts +140 -0
  207. package/src/global/index.ts +54 -0
  208. package/src/id/id.ts +84 -0
  209. package/src/ide/index.ts +76 -0
  210. package/src/index.ts +210 -0
  211. package/src/installation/index.ts +266 -0
  212. package/src/lsp/client.ts +251 -0
  213. package/src/lsp/index.ts +485 -0
  214. package/src/lsp/language.ts +120 -0
  215. package/src/lsp/server.ts +2142 -0
  216. package/src/mcp/auth.ts +130 -0
  217. package/src/mcp/index.ts +937 -0
  218. package/src/mcp/oauth-callback.ts +200 -0
  219. package/src/mcp/oauth-provider.ts +176 -0
  220. package/src/patch/index.ts +680 -0
  221. package/src/permission/arity.ts +163 -0
  222. package/src/permission/index.ts +210 -0
  223. package/src/permission/next.ts +286 -0
  224. package/src/plugin/codex.ts +624 -0
  225. package/src/plugin/copilot.ts +327 -0
  226. package/src/plugin/index.ts +143 -0
  227. package/src/project/bootstrap.ts +33 -0
  228. package/src/project/instance.ts +114 -0
  229. package/src/project/project.sql.ts +15 -0
  230. package/src/project/project.ts +441 -0
  231. package/src/project/state.ts +70 -0
  232. package/src/project/vcs.ts +76 -0
  233. package/src/provider/auth.ts +147 -0
  234. package/src/provider/error.ts +189 -0
  235. package/src/provider/models.ts +146 -0
  236. package/src/provider/provider.ts +1338 -0
  237. package/src/provider/sdk/copilot/README.md +5 -0
  238. package/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +164 -0
  239. package/src/provider/sdk/copilot/chat/get-response-metadata.ts +15 -0
  240. package/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +17 -0
  241. package/src/provider/sdk/copilot/chat/openai-compatible-api-types.ts +64 -0
  242. package/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +780 -0
  243. package/src/provider/sdk/copilot/chat/openai-compatible-chat-options.ts +28 -0
  244. package/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
  245. package/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts +87 -0
  246. package/src/provider/sdk/copilot/copilot-provider.ts +100 -0
  247. package/src/provider/sdk/copilot/index.ts +2 -0
  248. package/src/provider/sdk/copilot/openai-compatible-error.ts +27 -0
  249. package/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts +303 -0
  250. package/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +22 -0
  251. package/src/provider/sdk/copilot/responses/openai-config.ts +18 -0
  252. package/src/provider/sdk/copilot/responses/openai-error.ts +22 -0
  253. package/src/provider/sdk/copilot/responses/openai-responses-api-types.ts +207 -0
  254. package/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +1732 -0
  255. package/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts +177 -0
  256. package/src/provider/sdk/copilot/responses/openai-responses-settings.ts +1 -0
  257. package/src/provider/sdk/copilot/responses/tool/code-interpreter.ts +88 -0
  258. package/src/provider/sdk/copilot/responses/tool/file-search.ts +128 -0
  259. package/src/provider/sdk/copilot/responses/tool/image-generation.ts +115 -0
  260. package/src/provider/sdk/copilot/responses/tool/local-shell.ts +65 -0
  261. package/src/provider/sdk/copilot/responses/tool/web-search-preview.ts +104 -0
  262. package/src/provider/sdk/copilot/responses/tool/web-search.ts +103 -0
  263. package/src/provider/transform.ts +955 -0
  264. package/src/pty/index.ts +324 -0
  265. package/src/question/index.ts +171 -0
  266. package/src/scheduler/index.ts +61 -0
  267. package/src/server/error.ts +36 -0
  268. package/src/server/event.ts +7 -0
  269. package/src/server/mdns.ts +60 -0
  270. package/src/server/routes/config.ts +92 -0
  271. package/src/server/routes/experimental.ts +270 -0
  272. package/src/server/routes/file.ts +197 -0
  273. package/src/server/routes/global.ts +185 -0
  274. package/src/server/routes/mcp.ts +225 -0
  275. package/src/server/routes/permission.ts +68 -0
  276. package/src/server/routes/project.ts +82 -0
  277. package/src/server/routes/provider.ts +165 -0
  278. package/src/server/routes/pty.ts +200 -0
  279. package/src/server/routes/question.ts +98 -0
  280. package/src/server/routes/session.ts +974 -0
  281. package/src/server/routes/tui.ts +379 -0
  282. package/src/server/routes/workspace.ts +104 -0
  283. package/src/server/server.ts +623 -0
  284. package/src/session/compaction.ts +261 -0
  285. package/src/session/index.ts +877 -0
  286. package/src/session/instruction.ts +192 -0
  287. package/src/session/llm.ts +279 -0
  288. package/src/session/message-v2.ts +899 -0
  289. package/src/session/message.ts +189 -0
  290. package/src/session/processor.ts +421 -0
  291. package/src/session/prompt/anthropic-20250930.txt +166 -0
  292. package/src/session/prompt/anthropic.txt +105 -0
  293. package/src/session/prompt/beast.txt +147 -0
  294. package/src/session/prompt/build-switch.txt +5 -0
  295. package/src/session/prompt/codex_header.txt +79 -0
  296. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  297. package/src/session/prompt/gemini.txt +155 -0
  298. package/src/session/prompt/max-steps.txt +16 -0
  299. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  300. package/src/session/prompt/plan.txt +26 -0
  301. package/src/session/prompt/qwen.txt +109 -0
  302. package/src/session/prompt/trinity.txt +97 -0
  303. package/src/session/prompt.ts +1959 -0
  304. package/src/session/retry.ts +101 -0
  305. package/src/session/revert.ts +138 -0
  306. package/src/session/session.sql.ts +88 -0
  307. package/src/session/status.ts +76 -0
  308. package/src/session/summary.ts +161 -0
  309. package/src/session/system.ts +54 -0
  310. package/src/session/todo.ts +56 -0
  311. package/src/share/share-next.ts +210 -0
  312. package/src/share/share.sql.ts +13 -0
  313. package/src/shell/shell.ts +68 -0
  314. package/src/skill/discovery.ts +98 -0
  315. package/src/skill/index.ts +1 -0
  316. package/src/skill/skill.ts +189 -0
  317. package/src/snapshot/index.ts +297 -0
  318. package/src/sql.d.ts +4 -0
  319. package/src/storage/db.ts +155 -0
  320. package/src/storage/json-migration.ts +425 -0
  321. package/src/storage/schema.sql.ts +10 -0
  322. package/src/storage/schema.ts +5 -0
  323. package/src/storage/storage.ts +220 -0
  324. package/src/tool/apply_patch.ts +281 -0
  325. package/src/tool/apply_patch.txt +33 -0
  326. package/src/tool/bash.ts +274 -0
  327. package/src/tool/bash.txt +115 -0
  328. package/src/tool/batch.ts +181 -0
  329. package/src/tool/batch.txt +24 -0
  330. package/src/tool/codesearch.ts +132 -0
  331. package/src/tool/codesearch.txt +12 -0
  332. package/src/tool/edit.ts +654 -0
  333. package/src/tool/edit.txt +10 -0
  334. package/src/tool/external-directory.ts +32 -0
  335. package/src/tool/glob.ts +78 -0
  336. package/src/tool/glob.txt +6 -0
  337. package/src/tool/grep.ts +156 -0
  338. package/src/tool/grep.txt +8 -0
  339. package/src/tool/invalid.ts +17 -0
  340. package/src/tool/ls.ts +121 -0
  341. package/src/tool/ls.txt +1 -0
  342. package/src/tool/lsp.ts +97 -0
  343. package/src/tool/lsp.txt +19 -0
  344. package/src/tool/multiedit.ts +46 -0
  345. package/src/tool/multiedit.txt +41 -0
  346. package/src/tool/plan-enter.txt +14 -0
  347. package/src/tool/plan-exit.txt +13 -0
  348. package/src/tool/plan.ts +131 -0
  349. package/src/tool/question.ts +33 -0
  350. package/src/tool/question.txt +10 -0
  351. package/src/tool/read.ts +293 -0
  352. package/src/tool/read.txt +14 -0
  353. package/src/tool/registry.ts +173 -0
  354. package/src/tool/skill.ts +123 -0
  355. package/src/tool/task.ts +165 -0
  356. package/src/tool/task.txt +60 -0
  357. package/src/tool/todo.ts +53 -0
  358. package/src/tool/todoread.txt +14 -0
  359. package/src/tool/todowrite.txt +167 -0
  360. package/src/tool/tool.ts +89 -0
  361. package/src/tool/truncation.ts +107 -0
  362. package/src/tool/webfetch.ts +206 -0
  363. package/src/tool/webfetch.txt +13 -0
  364. package/src/tool/websearch.ts +150 -0
  365. package/src/tool/websearch.txt +14 -0
  366. package/src/tool/write.ts +84 -0
  367. package/src/tool/write.txt +8 -0
  368. package/src/util/abort.ts +35 -0
  369. package/src/util/archive.ts +16 -0
  370. package/src/util/color.ts +19 -0
  371. package/src/util/context.ts +25 -0
  372. package/src/util/defer.ts +12 -0
  373. package/src/util/eventloop.ts +20 -0
  374. package/src/util/filesystem.ts +189 -0
  375. package/src/util/fn.ts +11 -0
  376. package/src/util/format.ts +20 -0
  377. package/src/util/git.ts +35 -0
  378. package/src/util/glob.ts +34 -0
  379. package/src/util/iife.ts +3 -0
  380. package/src/util/keybind.ts +103 -0
  381. package/src/util/lazy.ts +23 -0
  382. package/src/util/locale.ts +81 -0
  383. package/src/util/lock.ts +98 -0
  384. package/src/util/log.ts +182 -0
  385. package/src/util/process.ts +126 -0
  386. package/src/util/proxied.ts +3 -0
  387. package/src/util/queue.ts +32 -0
  388. package/src/util/rpc.ts +66 -0
  389. package/src/util/scrap.ts +10 -0
  390. package/src/util/signal.ts +12 -0
  391. package/src/util/timeout.ts +14 -0
  392. package/src/util/token.ts +7 -0
  393. package/src/util/wildcard.ts +59 -0
  394. package/src/worktree/index.ts +643 -0
  395. package/sst-env.d.ts +10 -0
  396. package/test/AGENTS.md +81 -0
  397. package/test/acp/agent-interface.test.ts +51 -0
  398. package/test/acp/event-subscription.test.ts +683 -0
  399. package/test/agent/agent.test.ts +689 -0
  400. package/test/bun.test.ts +53 -0
  401. package/test/cli/github-action.test.ts +197 -0
  402. package/test/cli/github-remote.test.ts +80 -0
  403. package/test/cli/import.test.ts +38 -0
  404. package/test/cli/plugin-auth-picker.test.ts +120 -0
  405. package/test/cli/tui/transcript.test.ts +322 -0
  406. package/test/config/agent-color.test.ts +71 -0
  407. package/test/config/config.test.ts +1886 -0
  408. package/test/config/fixtures/empty-frontmatter.md +4 -0
  409. package/test/config/fixtures/frontmatter.md +28 -0
  410. package/test/config/fixtures/markdown-header.md +11 -0
  411. package/test/config/fixtures/no-frontmatter.md +1 -0
  412. package/test/config/fixtures/weird-model-id.md +13 -0
  413. package/test/config/markdown.test.ts +228 -0
  414. package/test/config/tui.test.ts +510 -0
  415. package/test/control-plane/session-proxy-middleware.test.ts +147 -0
  416. package/test/control-plane/sse.test.ts +56 -0
  417. package/test/control-plane/workspace-server-sse.test.ts +65 -0
  418. package/test/control-plane/workspace-sync.test.ts +97 -0
  419. package/test/file/ignore.test.ts +10 -0
  420. package/test/file/index.test.ts +394 -0
  421. package/test/file/path-traversal.test.ts +198 -0
  422. package/test/file/ripgrep.test.ts +39 -0
  423. package/test/file/time.test.ts +361 -0
  424. package/test/fixture/db.ts +11 -0
  425. package/test/fixture/fixture.ts +45 -0
  426. package/test/fixture/lsp/fake-lsp-server.js +77 -0
  427. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  428. package/test/fixture/skills/agents-sdk/references/callable.md +92 -0
  429. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  430. package/test/fixture/skills/index.json +6 -0
  431. package/test/ide/ide.test.ts +82 -0
  432. package/test/keybind.test.ts +421 -0
  433. package/test/lsp/client.test.ts +95 -0
  434. package/test/mcp/headers.test.ts +153 -0
  435. package/test/mcp/oauth-browser.test.ts +249 -0
  436. package/test/memory/abort-leak.test.ts +136 -0
  437. package/test/patch/patch.test.ts +348 -0
  438. package/test/permission/arity.test.ts +33 -0
  439. package/test/permission/next.test.ts +689 -0
  440. package/test/permission-task.test.ts +319 -0
  441. package/test/plugin/auth-override.test.ts +44 -0
  442. package/test/plugin/codex.test.ts +123 -0
  443. package/test/preload.ts +80 -0
  444. package/test/project/project.test.ts +348 -0
  445. package/test/project/worktree-remove.test.ts +65 -0
  446. package/test/provider/amazon-bedrock.test.ts +446 -0
  447. package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
  448. package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
  449. package/test/provider/gitlab-duo.test.ts +262 -0
  450. package/test/provider/provider.test.ts +2220 -0
  451. package/test/provider/transform.test.ts +2353 -0
  452. package/test/pty/pty-output-isolation.test.ts +140 -0
  453. package/test/question/question.test.ts +300 -0
  454. package/test/scheduler.test.ts +73 -0
  455. package/test/server/global-session-list.test.ts +89 -0
  456. package/test/server/session-list.test.ts +90 -0
  457. package/test/server/session-select.test.ts +78 -0
  458. package/test/session/compaction.test.ts +423 -0
  459. package/test/session/instruction.test.ts +170 -0
  460. package/test/session/llm.test.ts +667 -0
  461. package/test/session/message-v2.test.ts +924 -0
  462. package/test/session/prompt.test.ts +211 -0
  463. package/test/session/retry.test.ts +188 -0
  464. package/test/session/revert-compact.test.ts +285 -0
  465. package/test/session/session.test.ts +71 -0
  466. package/test/session/structured-output-integration.test.ts +233 -0
  467. package/test/session/structured-output.test.ts +385 -0
  468. package/test/skill/discovery.test.ts +110 -0
  469. package/test/skill/skill.test.ts +388 -0
  470. package/test/snapshot/snapshot.test.ts +1180 -0
  471. package/test/storage/json-migration.test.ts +846 -0
  472. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  473. package/test/tool/apply_patch.test.ts +566 -0
  474. package/test/tool/bash.test.ts +402 -0
  475. package/test/tool/edit.test.ts +496 -0
  476. package/test/tool/external-directory.test.ts +127 -0
  477. package/test/tool/fixtures/large-image.png +0 -0
  478. package/test/tool/fixtures/models-api.json +38413 -0
  479. package/test/tool/grep.test.ts +110 -0
  480. package/test/tool/question.test.ts +107 -0
  481. package/test/tool/read.test.ts +504 -0
  482. package/test/tool/registry.test.ts +122 -0
  483. package/test/tool/skill.test.ts +112 -0
  484. package/test/tool/truncation.test.ts +160 -0
  485. package/test/tool/webfetch.test.ts +100 -0
  486. package/test/tool/write.test.ts +348 -0
  487. package/test/util/filesystem.test.ts +443 -0
  488. package/test/util/format.test.ts +59 -0
  489. package/test/util/glob.test.ts +164 -0
  490. package/test/util/iife.test.ts +36 -0
  491. package/test/util/lazy.test.ts +50 -0
  492. package/test/util/lock.test.ts +72 -0
  493. package/test/util/process.test.ts +59 -0
  494. package/test/util/timeout.test.ts +21 -0
  495. package/test/util/wildcard.test.ts +90 -0
  496. package/tsconfig.json +16 -0
@@ -0,0 +1,1741 @@
1
+ import {
2
+ RequestError,
3
+ type Agent as ACPAgent,
4
+ type AgentSideConnection,
5
+ type AuthenticateRequest,
6
+ type AuthMethod,
7
+ type CancelNotification,
8
+ type ForkSessionRequest,
9
+ type ForkSessionResponse,
10
+ type InitializeRequest,
11
+ type InitializeResponse,
12
+ type ListSessionsRequest,
13
+ type ListSessionsResponse,
14
+ type LoadSessionRequest,
15
+ type NewSessionRequest,
16
+ type PermissionOption,
17
+ type PlanEntry,
18
+ type PromptRequest,
19
+ type ResumeSessionRequest,
20
+ type ResumeSessionResponse,
21
+ type Role,
22
+ type SessionInfo,
23
+ type SetSessionModelRequest,
24
+ type SetSessionModeRequest,
25
+ type SetSessionModeResponse,
26
+ type ToolCallContent,
27
+ type ToolKind,
28
+ type Usage,
29
+ } from "@agentclientprotocol/sdk"
30
+
31
+ import { Log } from "../util/log"
32
+ import { pathToFileURL } from "bun"
33
+ import { Filesystem } from "../util/filesystem"
34
+ import { ACPSessionManager } from "./session"
35
+ import type { ACPConfig } from "./types"
36
+ import { Provider } from "../provider/provider"
37
+ import { Agent as AgentModule } from "../agent/agent"
38
+ import { Installation } from "@/installation"
39
+ import { MessageV2 } from "@/session/message-v2"
40
+ import { Config } from "@/config/config"
41
+ import { Todo } from "@/session/todo"
42
+ import { z } from "zod"
43
+ import { LoadAPIKeyError } from "ai"
44
+ import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
45
+ import { applyPatch } from "diff"
46
+
47
+ type ModeOption = { id: string; name: string; description?: string }
48
+ type ModelOption = { modelId: string; name: string }
49
+
50
+ const DEFAULT_VARIANT_VALUE = "default"
51
+
52
+ export namespace ACP {
53
+ const log = Log.create({ service: "acp-agent" })
54
+
55
+ async function getContextLimit(
56
+ sdk: OpencodeClient,
57
+ providerID: string,
58
+ modelID: string,
59
+ directory: string,
60
+ ): Promise<number | null> {
61
+ const providers = await sdk.config
62
+ .providers({ directory })
63
+ .then((x) => x.data?.providers ?? [])
64
+ .catch((error) => {
65
+ log.error("failed to get providers for context limit", { error })
66
+ return []
67
+ })
68
+
69
+ const provider = providers.find((p) => p.id === providerID)
70
+ const model = provider?.models[modelID]
71
+ return model?.limit.context ?? null
72
+ }
73
+
74
+ async function sendUsageUpdate(
75
+ connection: AgentSideConnection,
76
+ sdk: OpencodeClient,
77
+ sessionID: string,
78
+ directory: string,
79
+ ): Promise<void> {
80
+ const messages = await sdk.session
81
+ .messages({ sessionID, directory }, { throwOnError: true })
82
+ .then((x) => x.data)
83
+ .catch((error) => {
84
+ log.error("failed to fetch messages for usage update", { error })
85
+ return undefined
86
+ })
87
+
88
+ if (!messages) return
89
+
90
+ const assistantMessages = messages.filter(
91
+ (m): m is { info: AssistantMessage; parts: SessionMessageResponse["parts"] } => m.info.role === "assistant",
92
+ )
93
+
94
+ const lastAssistant = assistantMessages[assistantMessages.length - 1]
95
+ if (!lastAssistant) return
96
+
97
+ const msg = lastAssistant.info
98
+ const size = await getContextLimit(sdk, msg.providerID, msg.modelID, directory)
99
+
100
+ if (!size) {
101
+ // Cannot calculate usage without known context size
102
+ return
103
+ }
104
+
105
+ const used = msg.tokens.input + (msg.tokens.cache?.read ?? 0)
106
+ const totalCost = assistantMessages.reduce((sum, m) => sum + m.info.cost, 0)
107
+
108
+ await connection
109
+ .sessionUpdate({
110
+ sessionId: sessionID,
111
+ update: {
112
+ sessionUpdate: "usage_update",
113
+ used,
114
+ size,
115
+ cost: { amount: totalCost, currency: "USD" },
116
+ },
117
+ })
118
+ .catch((error) => {
119
+ log.error("failed to send usage update", { error })
120
+ })
121
+ }
122
+
123
+ export async function init({ sdk: _sdk }: { sdk: OpencodeClient }) {
124
+ return {
125
+ create: (connection: AgentSideConnection, fullConfig: ACPConfig) => {
126
+ return new Agent(connection, fullConfig)
127
+ },
128
+ }
129
+ }
130
+
131
+ export class Agent implements ACPAgent {
132
+ private connection: AgentSideConnection
133
+ private config: ACPConfig
134
+ private sdk: OpencodeClient
135
+ private sessionManager: ACPSessionManager
136
+ private eventAbort = new AbortController()
137
+ private eventStarted = false
138
+ private bashSnapshots = new Map<string, string>()
139
+ private toolStarts = new Set<string>()
140
+ private permissionQueues = new Map<string, Promise<void>>()
141
+ private permissionOptions: PermissionOption[] = [
142
+ { optionId: "once", kind: "allow_once", name: "Allow once" },
143
+ { optionId: "always", kind: "allow_always", name: "Always allow" },
144
+ { optionId: "reject", kind: "reject_once", name: "Reject" },
145
+ ]
146
+
147
+ constructor(connection: AgentSideConnection, config: ACPConfig) {
148
+ this.connection = connection
149
+ this.config = config
150
+ this.sdk = config.sdk
151
+ this.sessionManager = new ACPSessionManager(this.sdk)
152
+ this.startEventSubscription()
153
+ }
154
+
155
+ private startEventSubscription() {
156
+ if (this.eventStarted) return
157
+ this.eventStarted = true
158
+ this.runEventSubscription().catch((error) => {
159
+ if (this.eventAbort.signal.aborted) return
160
+ log.error("event subscription failed", { error })
161
+ })
162
+ }
163
+
164
+ private async runEventSubscription() {
165
+ while (true) {
166
+ if (this.eventAbort.signal.aborted) return
167
+ const events = await this.sdk.global.event({
168
+ signal: this.eventAbort.signal,
169
+ })
170
+ for await (const event of events.stream) {
171
+ if (this.eventAbort.signal.aborted) return
172
+ const payload = (event as any)?.payload
173
+ if (!payload) continue
174
+ await this.handleEvent(payload as Event).catch((error) => {
175
+ log.error("failed to handle event", { error, type: payload.type })
176
+ })
177
+ }
178
+ }
179
+ }
180
+
181
+ private async handleEvent(event: Event) {
182
+ switch (event.type) {
183
+ case "permission.asked": {
184
+ const permission = event.properties
185
+ const session = this.sessionManager.tryGet(permission.sessionID)
186
+ if (!session) return
187
+
188
+ const prev = this.permissionQueues.get(permission.sessionID) ?? Promise.resolve()
189
+ const next = prev
190
+ .then(async () => {
191
+ const directory = session.cwd
192
+
193
+ const res = await this.connection
194
+ .requestPermission({
195
+ sessionId: permission.sessionID,
196
+ toolCall: {
197
+ toolCallId: permission.tool?.callID ?? permission.id,
198
+ status: "pending",
199
+ title: permission.permission,
200
+ rawInput: permission.metadata,
201
+ kind: toToolKind(permission.permission),
202
+ locations: toLocations(permission.permission, permission.metadata),
203
+ },
204
+ options: this.permissionOptions,
205
+ })
206
+ .catch(async (error) => {
207
+ log.error("failed to request permission from ACP", {
208
+ error,
209
+ permissionID: permission.id,
210
+ sessionID: permission.sessionID,
211
+ })
212
+ await this.sdk.permission.reply({
213
+ requestID: permission.id,
214
+ reply: "reject",
215
+ directory,
216
+ })
217
+ return undefined
218
+ })
219
+
220
+ if (!res) return
221
+ if (res.outcome.outcome !== "selected") {
222
+ await this.sdk.permission.reply({
223
+ requestID: permission.id,
224
+ reply: "reject",
225
+ directory,
226
+ })
227
+ return
228
+ }
229
+
230
+ if (res.outcome.optionId !== "reject" && permission.permission == "edit") {
231
+ const metadata = permission.metadata || {}
232
+ const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : ""
233
+ const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : ""
234
+ const content = (await Filesystem.exists(filepath)) ? await Filesystem.readText(filepath) : ""
235
+ const newContent = getNewContent(content, diff)
236
+
237
+ if (newContent) {
238
+ this.connection.writeTextFile({
239
+ sessionId: session.id,
240
+ path: filepath,
241
+ content: newContent,
242
+ })
243
+ }
244
+ }
245
+
246
+ await this.sdk.permission.reply({
247
+ requestID: permission.id,
248
+ reply: res.outcome.optionId as "once" | "always" | "reject",
249
+ directory,
250
+ })
251
+ })
252
+ .catch((error) => {
253
+ log.error("failed to handle permission", { error, permissionID: permission.id })
254
+ })
255
+ .finally(() => {
256
+ if (this.permissionQueues.get(permission.sessionID) === next) {
257
+ this.permissionQueues.delete(permission.sessionID)
258
+ }
259
+ })
260
+ this.permissionQueues.set(permission.sessionID, next)
261
+ return
262
+ }
263
+
264
+ case "message.part.updated": {
265
+ log.info("message part updated", { event: event.properties })
266
+ const props = event.properties
267
+ const part = props.part
268
+ const session = this.sessionManager.tryGet(part.sessionID)
269
+ if (!session) return
270
+ const sessionId = session.id
271
+
272
+ if (part.type === "tool") {
273
+ await this.toolStart(sessionId, part)
274
+
275
+ switch (part.state.status) {
276
+ case "pending":
277
+ this.bashSnapshots.delete(part.callID)
278
+ return
279
+
280
+ case "running":
281
+ const output = this.bashOutput(part)
282
+ const content: ToolCallContent[] = []
283
+ if (output) {
284
+ const hash = String(Bun.hash(output))
285
+ if (part.tool === "bash") {
286
+ if (this.bashSnapshots.get(part.callID) === hash) {
287
+ await this.connection
288
+ .sessionUpdate({
289
+ sessionId,
290
+ update: {
291
+ sessionUpdate: "tool_call_update",
292
+ toolCallId: part.callID,
293
+ status: "in_progress",
294
+ kind: toToolKind(part.tool),
295
+ title: part.tool,
296
+ locations: toLocations(part.tool, part.state.input),
297
+ rawInput: part.state.input,
298
+ },
299
+ })
300
+ .catch((error) => {
301
+ log.error("failed to send tool in_progress to ACP", { error })
302
+ })
303
+ return
304
+ }
305
+ this.bashSnapshots.set(part.callID, hash)
306
+ }
307
+ content.push({
308
+ type: "content",
309
+ content: {
310
+ type: "text",
311
+ text: output,
312
+ },
313
+ })
314
+ }
315
+ await this.connection
316
+ .sessionUpdate({
317
+ sessionId,
318
+ update: {
319
+ sessionUpdate: "tool_call_update",
320
+ toolCallId: part.callID,
321
+ status: "in_progress",
322
+ kind: toToolKind(part.tool),
323
+ title: part.tool,
324
+ locations: toLocations(part.tool, part.state.input),
325
+ rawInput: part.state.input,
326
+ ...(content.length > 0 && { content }),
327
+ },
328
+ })
329
+ .catch((error) => {
330
+ log.error("failed to send tool in_progress to ACP", { error })
331
+ })
332
+ return
333
+
334
+ case "completed": {
335
+ this.toolStarts.delete(part.callID)
336
+ this.bashSnapshots.delete(part.callID)
337
+ const kind = toToolKind(part.tool)
338
+ const content: ToolCallContent[] = [
339
+ {
340
+ type: "content",
341
+ content: {
342
+ type: "text",
343
+ text: part.state.output,
344
+ },
345
+ },
346
+ ]
347
+
348
+ if (kind === "edit") {
349
+ const input = part.state.input
350
+ const filePath = typeof input["filePath"] === "string" ? input["filePath"] : ""
351
+ const oldText = typeof input["oldString"] === "string" ? input["oldString"] : ""
352
+ const newText =
353
+ typeof input["newString"] === "string"
354
+ ? input["newString"]
355
+ : typeof input["content"] === "string"
356
+ ? input["content"]
357
+ : ""
358
+ content.push({
359
+ type: "diff",
360
+ path: filePath,
361
+ oldText,
362
+ newText,
363
+ })
364
+ }
365
+
366
+ if (part.tool === "todowrite") {
367
+ const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
368
+ if (parsedTodos.success) {
369
+ await this.connection
370
+ .sessionUpdate({
371
+ sessionId,
372
+ update: {
373
+ sessionUpdate: "plan",
374
+ entries: parsedTodos.data.map((todo) => {
375
+ const status: PlanEntry["status"] =
376
+ todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
377
+ return {
378
+ priority: "medium",
379
+ status,
380
+ content: todo.content,
381
+ }
382
+ }),
383
+ },
384
+ })
385
+ .catch((error) => {
386
+ log.error("failed to send session update for todo", { error })
387
+ })
388
+ } else {
389
+ log.error("failed to parse todo output", { error: parsedTodos.error })
390
+ }
391
+ }
392
+
393
+ await this.connection
394
+ .sessionUpdate({
395
+ sessionId,
396
+ update: {
397
+ sessionUpdate: "tool_call_update",
398
+ toolCallId: part.callID,
399
+ status: "completed",
400
+ kind,
401
+ content,
402
+ title: part.state.title,
403
+ rawInput: part.state.input,
404
+ rawOutput: {
405
+ output: part.state.output,
406
+ metadata: part.state.metadata,
407
+ },
408
+ },
409
+ })
410
+ .catch((error) => {
411
+ log.error("failed to send tool completed to ACP", { error })
412
+ })
413
+ return
414
+ }
415
+ case "error":
416
+ this.toolStarts.delete(part.callID)
417
+ this.bashSnapshots.delete(part.callID)
418
+ await this.connection
419
+ .sessionUpdate({
420
+ sessionId,
421
+ update: {
422
+ sessionUpdate: "tool_call_update",
423
+ toolCallId: part.callID,
424
+ status: "failed",
425
+ kind: toToolKind(part.tool),
426
+ title: part.tool,
427
+ rawInput: part.state.input,
428
+ content: [
429
+ {
430
+ type: "content",
431
+ content: {
432
+ type: "text",
433
+ text: part.state.error,
434
+ },
435
+ },
436
+ ],
437
+ rawOutput: {
438
+ error: part.state.error,
439
+ metadata: part.state.metadata,
440
+ },
441
+ },
442
+ })
443
+ .catch((error) => {
444
+ log.error("failed to send tool error to ACP", { error })
445
+ })
446
+ return
447
+ }
448
+ }
449
+ return
450
+ }
451
+
452
+ case "message.part.delta": {
453
+ const props = event.properties
454
+ const session = this.sessionManager.tryGet(props.sessionID)
455
+ if (!session) return
456
+ const sessionId = session.id
457
+
458
+ const message = await this.sdk.session
459
+ .message(
460
+ {
461
+ sessionID: props.sessionID,
462
+ messageID: props.messageID,
463
+ directory: session.cwd,
464
+ },
465
+ { throwOnError: true },
466
+ )
467
+ .then((x) => x.data)
468
+ .catch((error) => {
469
+ log.error("unexpected error when fetching message", { error })
470
+ return undefined
471
+ })
472
+
473
+ if (!message || message.info.role !== "assistant") return
474
+
475
+ const part = message.parts.find((p) => p.id === props.partID)
476
+ if (!part) return
477
+
478
+ if (part.type === "text" && props.field === "text" && part.ignored !== true) {
479
+ await this.connection
480
+ .sessionUpdate({
481
+ sessionId,
482
+ update: {
483
+ sessionUpdate: "agent_message_chunk",
484
+ content: {
485
+ type: "text",
486
+ text: props.delta,
487
+ },
488
+ },
489
+ })
490
+ .catch((error) => {
491
+ log.error("failed to send text delta to ACP", { error })
492
+ })
493
+ return
494
+ }
495
+
496
+ if (part.type === "reasoning" && props.field === "text") {
497
+ await this.connection
498
+ .sessionUpdate({
499
+ sessionId,
500
+ update: {
501
+ sessionUpdate: "agent_thought_chunk",
502
+ content: {
503
+ type: "text",
504
+ text: props.delta,
505
+ },
506
+ },
507
+ })
508
+ .catch((error) => {
509
+ log.error("failed to send reasoning delta to ACP", { error })
510
+ })
511
+ }
512
+ return
513
+ }
514
+ }
515
+ }
516
+
517
+ async initialize(params: InitializeRequest): Promise<InitializeResponse> {
518
+ log.info("initialize", { protocolVersion: params.protocolVersion })
519
+
520
+ const authMethod: AuthMethod = {
521
+ description: "Run `opencode auth login` in the terminal",
522
+ name: "Login with opencode",
523
+ id: "opencode-login",
524
+ }
525
+
526
+ // If client supports terminal-auth capability, use that instead.
527
+ if (params.clientCapabilities?._meta?.["terminal-auth"] === true) {
528
+ authMethod._meta = {
529
+ "terminal-auth": {
530
+ command: "opencode",
531
+ args: ["auth", "login"],
532
+ label: "OpenCode Login",
533
+ },
534
+ }
535
+ }
536
+
537
+ return {
538
+ protocolVersion: 1,
539
+ agentCapabilities: {
540
+ loadSession: true,
541
+ mcpCapabilities: {
542
+ http: true,
543
+ sse: true,
544
+ },
545
+ promptCapabilities: {
546
+ embeddedContext: true,
547
+ image: true,
548
+ },
549
+ sessionCapabilities: {
550
+ fork: {},
551
+ list: {},
552
+ resume: {},
553
+ },
554
+ },
555
+ authMethods: [authMethod],
556
+ agentInfo: {
557
+ name: "OpenCode",
558
+ version: Installation.VERSION,
559
+ },
560
+ }
561
+ }
562
+
563
+ async authenticate(_params: AuthenticateRequest) {
564
+ throw new Error("Authentication not implemented")
565
+ }
566
+
567
+ async newSession(params: NewSessionRequest) {
568
+ const directory = params.cwd
569
+ try {
570
+ const model = await defaultModel(this.config, directory)
571
+
572
+ // Store ACP session state
573
+ const state = await this.sessionManager.create(params.cwd, params.mcpServers, model)
574
+ const sessionId = state.id
575
+
576
+ log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length })
577
+
578
+ const load = await this.loadSessionMode({
579
+ cwd: directory,
580
+ mcpServers: params.mcpServers,
581
+ sessionId,
582
+ })
583
+
584
+ return {
585
+ sessionId,
586
+ models: load.models,
587
+ modes: load.modes,
588
+ _meta: load._meta,
589
+ }
590
+ } catch (e) {
591
+ const error = MessageV2.fromError(e, {
592
+ providerID: this.config.defaultModel?.providerID ?? "unknown",
593
+ })
594
+ if (LoadAPIKeyError.isInstance(error)) {
595
+ throw RequestError.authRequired()
596
+ }
597
+ throw e
598
+ }
599
+ }
600
+
601
+ async loadSession(params: LoadSessionRequest) {
602
+ const directory = params.cwd
603
+ const sessionId = params.sessionId
604
+
605
+ try {
606
+ const model = await defaultModel(this.config, directory)
607
+
608
+ // Store ACP session state
609
+ await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model)
610
+
611
+ log.info("load_session", { sessionId, mcpServers: params.mcpServers.length })
612
+
613
+ const result = await this.loadSessionMode({
614
+ cwd: directory,
615
+ mcpServers: params.mcpServers,
616
+ sessionId,
617
+ })
618
+
619
+ // Replay session history
620
+ const messages = await this.sdk.session
621
+ .messages(
622
+ {
623
+ sessionID: sessionId,
624
+ directory,
625
+ },
626
+ { throwOnError: true },
627
+ )
628
+ .then((x) => x.data)
629
+ .catch((err) => {
630
+ log.error("unexpected error when fetching message", { error: err })
631
+ return undefined
632
+ })
633
+
634
+ const lastUser = messages?.findLast((m) => m.info.role === "user")?.info
635
+ if (lastUser?.role === "user") {
636
+ result.models.currentModelId = `${lastUser.model.providerID}/${lastUser.model.modelID}`
637
+ this.sessionManager.setModel(sessionId, {
638
+ providerID: lastUser.model.providerID,
639
+ modelID: lastUser.model.modelID,
640
+ })
641
+ if (result.modes?.availableModes.some((m) => m.id === lastUser.agent)) {
642
+ result.modes.currentModeId = lastUser.agent
643
+ this.sessionManager.setMode(sessionId, lastUser.agent)
644
+ }
645
+ }
646
+
647
+ for (const msg of messages ?? []) {
648
+ log.debug("replay message", msg)
649
+ await this.processMessage(msg)
650
+ }
651
+
652
+ await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
653
+
654
+ return result
655
+ } catch (e) {
656
+ const error = MessageV2.fromError(e, {
657
+ providerID: this.config.defaultModel?.providerID ?? "unknown",
658
+ })
659
+ if (LoadAPIKeyError.isInstance(error)) {
660
+ throw RequestError.authRequired()
661
+ }
662
+ throw e
663
+ }
664
+ }
665
+
666
+ async unstable_listSessions(params: ListSessionsRequest): Promise<ListSessionsResponse> {
667
+ try {
668
+ const cursor = params.cursor ? Number(params.cursor) : undefined
669
+ const limit = 100
670
+
671
+ const sessions = await this.sdk.session
672
+ .list(
673
+ {
674
+ directory: params.cwd ?? undefined,
675
+ roots: true,
676
+ },
677
+ { throwOnError: true },
678
+ )
679
+ .then((x) => x.data ?? [])
680
+
681
+ const sorted = sessions.toSorted((a, b) => b.time.updated - a.time.updated)
682
+ const filtered = cursor ? sorted.filter((s) => s.time.updated < cursor) : sorted
683
+ const page = filtered.slice(0, limit)
684
+
685
+ const entries: SessionInfo[] = page.map((session) => ({
686
+ sessionId: session.id,
687
+ cwd: session.directory,
688
+ title: session.title,
689
+ updatedAt: new Date(session.time.updated).toISOString(),
690
+ }))
691
+
692
+ const last = page[page.length - 1]
693
+ const next = filtered.length > limit && last ? String(last.time.updated) : undefined
694
+
695
+ const response: ListSessionsResponse = {
696
+ sessions: entries,
697
+ }
698
+ if (next) response.nextCursor = next
699
+ return response
700
+ } catch (e) {
701
+ const error = MessageV2.fromError(e, {
702
+ providerID: this.config.defaultModel?.providerID ?? "unknown",
703
+ })
704
+ if (LoadAPIKeyError.isInstance(error)) {
705
+ throw RequestError.authRequired()
706
+ }
707
+ throw e
708
+ }
709
+ }
710
+
711
+ async unstable_forkSession(params: ForkSessionRequest): Promise<ForkSessionResponse> {
712
+ const directory = params.cwd
713
+ const mcpServers = params.mcpServers ?? []
714
+
715
+ try {
716
+ const model = await defaultModel(this.config, directory)
717
+
718
+ const forked = await this.sdk.session
719
+ .fork(
720
+ {
721
+ sessionID: params.sessionId,
722
+ directory,
723
+ },
724
+ { throwOnError: true },
725
+ )
726
+ .then((x) => x.data)
727
+
728
+ if (!forked) {
729
+ throw new Error("Fork session returned no data")
730
+ }
731
+
732
+ const sessionId = forked.id
733
+ await this.sessionManager.load(sessionId, directory, mcpServers, model)
734
+
735
+ log.info("fork_session", { sessionId, mcpServers: mcpServers.length })
736
+
737
+ const mode = await this.loadSessionMode({
738
+ cwd: directory,
739
+ mcpServers,
740
+ sessionId,
741
+ })
742
+
743
+ const messages = await this.sdk.session
744
+ .messages(
745
+ {
746
+ sessionID: sessionId,
747
+ directory,
748
+ },
749
+ { throwOnError: true },
750
+ )
751
+ .then((x) => x.data)
752
+ .catch((err) => {
753
+ log.error("unexpected error when fetching message", { error: err })
754
+ return undefined
755
+ })
756
+
757
+ for (const msg of messages ?? []) {
758
+ log.debug("replay message", msg)
759
+ await this.processMessage(msg)
760
+ }
761
+
762
+ await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
763
+
764
+ return mode
765
+ } catch (e) {
766
+ const error = MessageV2.fromError(e, {
767
+ providerID: this.config.defaultModel?.providerID ?? "unknown",
768
+ })
769
+ if (LoadAPIKeyError.isInstance(error)) {
770
+ throw RequestError.authRequired()
771
+ }
772
+ throw e
773
+ }
774
+ }
775
+
776
+ async unstable_resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
777
+ const directory = params.cwd
778
+ const sessionId = params.sessionId
779
+ const mcpServers = params.mcpServers ?? []
780
+
781
+ try {
782
+ const model = await defaultModel(this.config, directory)
783
+ await this.sessionManager.load(sessionId, directory, mcpServers, model)
784
+
785
+ log.info("resume_session", { sessionId, mcpServers: mcpServers.length })
786
+
787
+ const result = await this.loadSessionMode({
788
+ cwd: directory,
789
+ mcpServers,
790
+ sessionId,
791
+ })
792
+
793
+ await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
794
+
795
+ return result
796
+ } catch (e) {
797
+ const error = MessageV2.fromError(e, {
798
+ providerID: this.config.defaultModel?.providerID ?? "unknown",
799
+ })
800
+ if (LoadAPIKeyError.isInstance(error)) {
801
+ throw RequestError.authRequired()
802
+ }
803
+ throw e
804
+ }
805
+ }
806
+
807
+ private async processMessage(message: SessionMessageResponse) {
808
+ log.debug("process message", message)
809
+ if (message.info.role !== "assistant" && message.info.role !== "user") return
810
+ const sessionId = message.info.sessionID
811
+
812
+ for (const part of message.parts) {
813
+ if (part.type === "tool") {
814
+ await this.toolStart(sessionId, part)
815
+ switch (part.state.status) {
816
+ case "pending":
817
+ this.bashSnapshots.delete(part.callID)
818
+ break
819
+ case "running":
820
+ const output = this.bashOutput(part)
821
+ const runningContent: ToolCallContent[] = []
822
+ if (output) {
823
+ runningContent.push({
824
+ type: "content",
825
+ content: {
826
+ type: "text",
827
+ text: output,
828
+ },
829
+ })
830
+ }
831
+ await this.connection
832
+ .sessionUpdate({
833
+ sessionId,
834
+ update: {
835
+ sessionUpdate: "tool_call_update",
836
+ toolCallId: part.callID,
837
+ status: "in_progress",
838
+ kind: toToolKind(part.tool),
839
+ title: part.tool,
840
+ locations: toLocations(part.tool, part.state.input),
841
+ rawInput: part.state.input,
842
+ ...(runningContent.length > 0 && { content: runningContent }),
843
+ },
844
+ })
845
+ .catch((err) => {
846
+ log.error("failed to send tool in_progress to ACP", { error: err })
847
+ })
848
+ break
849
+ case "completed":
850
+ this.toolStarts.delete(part.callID)
851
+ this.bashSnapshots.delete(part.callID)
852
+ const kind = toToolKind(part.tool)
853
+ const content: ToolCallContent[] = [
854
+ {
855
+ type: "content",
856
+ content: {
857
+ type: "text",
858
+ text: part.state.output,
859
+ },
860
+ },
861
+ ]
862
+
863
+ if (kind === "edit") {
864
+ const input = part.state.input
865
+ const filePath = typeof input["filePath"] === "string" ? input["filePath"] : ""
866
+ const oldText = typeof input["oldString"] === "string" ? input["oldString"] : ""
867
+ const newText =
868
+ typeof input["newString"] === "string"
869
+ ? input["newString"]
870
+ : typeof input["content"] === "string"
871
+ ? input["content"]
872
+ : ""
873
+ content.push({
874
+ type: "diff",
875
+ path: filePath,
876
+ oldText,
877
+ newText,
878
+ })
879
+ }
880
+
881
+ if (part.tool === "todowrite") {
882
+ const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
883
+ if (parsedTodos.success) {
884
+ await this.connection
885
+ .sessionUpdate({
886
+ sessionId,
887
+ update: {
888
+ sessionUpdate: "plan",
889
+ entries: parsedTodos.data.map((todo) => {
890
+ const status: PlanEntry["status"] =
891
+ todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
892
+ return {
893
+ priority: "medium",
894
+ status,
895
+ content: todo.content,
896
+ }
897
+ }),
898
+ },
899
+ })
900
+ .catch((err) => {
901
+ log.error("failed to send session update for todo", { error: err })
902
+ })
903
+ } else {
904
+ log.error("failed to parse todo output", { error: parsedTodos.error })
905
+ }
906
+ }
907
+
908
+ await this.connection
909
+ .sessionUpdate({
910
+ sessionId,
911
+ update: {
912
+ sessionUpdate: "tool_call_update",
913
+ toolCallId: part.callID,
914
+ status: "completed",
915
+ kind,
916
+ content,
917
+ title: part.state.title,
918
+ rawInput: part.state.input,
919
+ rawOutput: {
920
+ output: part.state.output,
921
+ metadata: part.state.metadata,
922
+ },
923
+ },
924
+ })
925
+ .catch((err) => {
926
+ log.error("failed to send tool completed to ACP", { error: err })
927
+ })
928
+ break
929
+ case "error":
930
+ this.toolStarts.delete(part.callID)
931
+ this.bashSnapshots.delete(part.callID)
932
+ await this.connection
933
+ .sessionUpdate({
934
+ sessionId,
935
+ update: {
936
+ sessionUpdate: "tool_call_update",
937
+ toolCallId: part.callID,
938
+ status: "failed",
939
+ kind: toToolKind(part.tool),
940
+ title: part.tool,
941
+ rawInput: part.state.input,
942
+ content: [
943
+ {
944
+ type: "content",
945
+ content: {
946
+ type: "text",
947
+ text: part.state.error,
948
+ },
949
+ },
950
+ ],
951
+ rawOutput: {
952
+ error: part.state.error,
953
+ metadata: part.state.metadata,
954
+ },
955
+ },
956
+ })
957
+ .catch((err) => {
958
+ log.error("failed to send tool error to ACP", { error: err })
959
+ })
960
+ break
961
+ }
962
+ } else if (part.type === "text") {
963
+ if (part.text) {
964
+ const audience: Role[] | undefined = part.synthetic ? ["assistant"] : part.ignored ? ["user"] : undefined
965
+ await this.connection
966
+ .sessionUpdate({
967
+ sessionId,
968
+ update: {
969
+ sessionUpdate: message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk",
970
+ content: {
971
+ type: "text",
972
+ text: part.text,
973
+ ...(audience && { annotations: { audience } }),
974
+ },
975
+ },
976
+ })
977
+ .catch((err) => {
978
+ log.error("failed to send text to ACP", { error: err })
979
+ })
980
+ }
981
+ } else if (part.type === "file") {
982
+ // Replay file attachments as appropriate ACP content blocks.
983
+ // OpenCode stores files internally as { type: "file", url, filename, mime }.
984
+ // We convert these back to ACP blocks based on the URL scheme and MIME type:
985
+ // - file:// URLs → resource_link
986
+ // - data: URLs with image/* → image block
987
+ // - data: URLs with text/* or application/json → resource with text
988
+ // - data: URLs with other types → resource with blob
989
+ const url = part.url
990
+ const filename = part.filename ?? "file"
991
+ const mime = part.mime || "application/octet-stream"
992
+ const messageChunk = message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk"
993
+
994
+ if (url.startsWith("file://")) {
995
+ // Local file reference - send as resource_link
996
+ await this.connection
997
+ .sessionUpdate({
998
+ sessionId,
999
+ update: {
1000
+ sessionUpdate: messageChunk,
1001
+ content: { type: "resource_link", uri: url, name: filename, mimeType: mime },
1002
+ },
1003
+ })
1004
+ .catch((err) => {
1005
+ log.error("failed to send resource_link to ACP", { error: err })
1006
+ })
1007
+ } else if (url.startsWith("data:")) {
1008
+ // Embedded content - parse data URL and send as appropriate block type
1009
+ const base64Match = url.match(/^data:([^;]+);base64,(.*)$/)
1010
+ const dataMime = base64Match?.[1]
1011
+ const base64Data = base64Match?.[2] ?? ""
1012
+
1013
+ const effectiveMime = dataMime || mime
1014
+
1015
+ if (effectiveMime.startsWith("image/")) {
1016
+ // Image - send as image block
1017
+ await this.connection
1018
+ .sessionUpdate({
1019
+ sessionId,
1020
+ update: {
1021
+ sessionUpdate: messageChunk,
1022
+ content: {
1023
+ type: "image",
1024
+ mimeType: effectiveMime,
1025
+ data: base64Data,
1026
+ uri: pathToFileURL(filename).href,
1027
+ },
1028
+ },
1029
+ })
1030
+ .catch((err) => {
1031
+ log.error("failed to send image to ACP", { error: err })
1032
+ })
1033
+ } else {
1034
+ // Non-image: text types get decoded, binary types stay as blob
1035
+ const isText = effectiveMime.startsWith("text/") || effectiveMime === "application/json"
1036
+ const fileUri = pathToFileURL(filename).href
1037
+ const resource = isText
1038
+ ? {
1039
+ uri: fileUri,
1040
+ mimeType: effectiveMime,
1041
+ text: Buffer.from(base64Data, "base64").toString("utf-8"),
1042
+ }
1043
+ : { uri: fileUri, mimeType: effectiveMime, blob: base64Data }
1044
+
1045
+ await this.connection
1046
+ .sessionUpdate({
1047
+ sessionId,
1048
+ update: {
1049
+ sessionUpdate: messageChunk,
1050
+ content: { type: "resource", resource },
1051
+ },
1052
+ })
1053
+ .catch((err) => {
1054
+ log.error("failed to send resource to ACP", { error: err })
1055
+ })
1056
+ }
1057
+ }
1058
+ // URLs that don't match file:// or data: are skipped (unsupported)
1059
+ } else if (part.type === "reasoning") {
1060
+ if (part.text) {
1061
+ await this.connection
1062
+ .sessionUpdate({
1063
+ sessionId,
1064
+ update: {
1065
+ sessionUpdate: "agent_thought_chunk",
1066
+ content: {
1067
+ type: "text",
1068
+ text: part.text,
1069
+ },
1070
+ },
1071
+ })
1072
+ .catch((err) => {
1073
+ log.error("failed to send reasoning to ACP", { error: err })
1074
+ })
1075
+ }
1076
+ }
1077
+ }
1078
+ }
1079
+
1080
+ private bashOutput(part: ToolPart) {
1081
+ if (part.tool !== "bash") return
1082
+ if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return
1083
+ const output = part.state.metadata["output"]
1084
+ if (typeof output !== "string") return
1085
+ return output
1086
+ }
1087
+
1088
+ private async toolStart(sessionId: string, part: ToolPart) {
1089
+ if (this.toolStarts.has(part.callID)) return
1090
+ this.toolStarts.add(part.callID)
1091
+ await this.connection
1092
+ .sessionUpdate({
1093
+ sessionId,
1094
+ update: {
1095
+ sessionUpdate: "tool_call",
1096
+ toolCallId: part.callID,
1097
+ title: part.tool,
1098
+ kind: toToolKind(part.tool),
1099
+ status: "pending",
1100
+ locations: [],
1101
+ rawInput: {},
1102
+ },
1103
+ })
1104
+ .catch((error) => {
1105
+ log.error("failed to send tool pending to ACP", { error })
1106
+ })
1107
+ }
1108
+
1109
+ private async loadAvailableModes(directory: string): Promise<ModeOption[]> {
1110
+ const agents = await this.config.sdk.app
1111
+ .agents(
1112
+ {
1113
+ directory,
1114
+ },
1115
+ { throwOnError: true },
1116
+ )
1117
+ .then((resp) => resp.data!)
1118
+
1119
+ return agents
1120
+ .filter((agent) => agent.mode !== "subagent" && !agent.hidden)
1121
+ .map((agent) => ({
1122
+ id: agent.name,
1123
+ name: agent.name,
1124
+ description: agent.description,
1125
+ }))
1126
+ }
1127
+
1128
+ private async resolveModeState(
1129
+ directory: string,
1130
+ sessionId: string,
1131
+ ): Promise<{ availableModes: ModeOption[]; currentModeId?: string }> {
1132
+ const availableModes = await this.loadAvailableModes(directory)
1133
+ const currentModeId =
1134
+ this.sessionManager.get(sessionId).modeId ||
1135
+ (await (async () => {
1136
+ if (!availableModes.length) return undefined
1137
+ const defaultAgentName = await AgentModule.defaultAgent()
1138
+ const resolvedModeId =
1139
+ availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id
1140
+ this.sessionManager.setMode(sessionId, resolvedModeId)
1141
+ return resolvedModeId
1142
+ })())
1143
+
1144
+ return { availableModes, currentModeId }
1145
+ }
1146
+
1147
+ private async loadSessionMode(params: LoadSessionRequest) {
1148
+ const directory = params.cwd
1149
+ const model = await defaultModel(this.config, directory)
1150
+ const sessionId = params.sessionId
1151
+
1152
+ const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers)
1153
+ const entries = sortProvidersByName(providers)
1154
+ const availableVariants = modelVariantsFromProviders(entries, model)
1155
+ const currentVariant = this.sessionManager.getVariant(sessionId)
1156
+ if (currentVariant && !availableVariants.includes(currentVariant)) {
1157
+ this.sessionManager.setVariant(sessionId, undefined)
1158
+ }
1159
+ const availableModels = buildAvailableModels(entries, { includeVariants: true })
1160
+ const modeState = await this.resolveModeState(directory, sessionId)
1161
+ const currentModeId = modeState.currentModeId
1162
+ const modes = currentModeId
1163
+ ? {
1164
+ availableModes: modeState.availableModes,
1165
+ currentModeId,
1166
+ }
1167
+ : undefined
1168
+
1169
+ const commands = await this.config.sdk.command
1170
+ .list(
1171
+ {
1172
+ directory,
1173
+ },
1174
+ { throwOnError: true },
1175
+ )
1176
+ .then((resp) => resp.data!)
1177
+
1178
+ const availableCommands = commands.map((command) => ({
1179
+ name: command.name,
1180
+ description: command.description ?? "",
1181
+ }))
1182
+ const names = new Set(availableCommands.map((c) => c.name))
1183
+ if (!names.has("compact"))
1184
+ availableCommands.push({
1185
+ name: "compact",
1186
+ description: "compact the session",
1187
+ })
1188
+
1189
+ const mcpServers: Record<string, Config.Mcp> = {}
1190
+ for (const server of params.mcpServers) {
1191
+ if ("type" in server) {
1192
+ mcpServers[server.name] = {
1193
+ url: server.url,
1194
+ headers: server.headers.reduce<Record<string, string>>((acc, { name, value }) => {
1195
+ acc[name] = value
1196
+ return acc
1197
+ }, {}),
1198
+ type: "remote",
1199
+ }
1200
+ } else {
1201
+ mcpServers[server.name] = {
1202
+ type: "local",
1203
+ command: [server.command, ...server.args],
1204
+ environment: server.env.reduce<Record<string, string>>((acc, { name, value }) => {
1205
+ acc[name] = value
1206
+ return acc
1207
+ }, {}),
1208
+ }
1209
+ }
1210
+ }
1211
+
1212
+ await Promise.all(
1213
+ Object.entries(mcpServers).map(async ([key, mcp]) => {
1214
+ await this.sdk.mcp
1215
+ .add(
1216
+ {
1217
+ directory,
1218
+ name: key,
1219
+ config: mcp,
1220
+ },
1221
+ { throwOnError: true },
1222
+ )
1223
+ .catch((error) => {
1224
+ log.error("failed to add mcp server", { name: key, error })
1225
+ })
1226
+ }),
1227
+ )
1228
+
1229
+ setTimeout(() => {
1230
+ this.connection.sessionUpdate({
1231
+ sessionId,
1232
+ update: {
1233
+ sessionUpdate: "available_commands_update",
1234
+ availableCommands,
1235
+ },
1236
+ })
1237
+ }, 0)
1238
+
1239
+ return {
1240
+ sessionId,
1241
+ models: {
1242
+ currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true),
1243
+ availableModels,
1244
+ },
1245
+ modes,
1246
+ _meta: buildVariantMeta({
1247
+ model,
1248
+ variant: this.sessionManager.getVariant(sessionId),
1249
+ availableVariants,
1250
+ }),
1251
+ }
1252
+ }
1253
+
1254
+ async unstable_setSessionModel(params: SetSessionModelRequest) {
1255
+ const session = this.sessionManager.get(params.sessionId)
1256
+ const providers = await this.sdk.config
1257
+ .providers({ directory: session.cwd }, { throwOnError: true })
1258
+ .then((x) => x.data!.providers)
1259
+
1260
+ const selection = parseModelSelection(params.modelId, providers)
1261
+ this.sessionManager.setModel(session.id, selection.model)
1262
+ this.sessionManager.setVariant(session.id, selection.variant)
1263
+
1264
+ const entries = sortProvidersByName(providers)
1265
+ const availableVariants = modelVariantsFromProviders(entries, selection.model)
1266
+
1267
+ return {
1268
+ _meta: buildVariantMeta({
1269
+ model: selection.model,
1270
+ variant: selection.variant,
1271
+ availableVariants,
1272
+ }),
1273
+ }
1274
+ }
1275
+
1276
+ async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse | void> {
1277
+ const session = this.sessionManager.get(params.sessionId)
1278
+ const availableModes = await this.loadAvailableModes(session.cwd)
1279
+ if (!availableModes.some((mode) => mode.id === params.modeId)) {
1280
+ throw new Error(`Agent not found: ${params.modeId}`)
1281
+ }
1282
+ this.sessionManager.setMode(params.sessionId, params.modeId)
1283
+ }
1284
+
1285
+ async prompt(params: PromptRequest) {
1286
+ const sessionID = params.sessionId
1287
+ const session = this.sessionManager.get(sessionID)
1288
+ const directory = session.cwd
1289
+
1290
+ const current = session.model
1291
+ const model = current ?? (await defaultModel(this.config, directory))
1292
+ if (!current) {
1293
+ this.sessionManager.setModel(session.id, model)
1294
+ }
1295
+ const agent = session.modeId ?? (await AgentModule.defaultAgent())
1296
+
1297
+ const parts: Array<
1298
+ | { type: "text"; text: string; synthetic?: boolean; ignored?: boolean }
1299
+ | { type: "file"; url: string; filename: string; mime: string }
1300
+ > = []
1301
+ for (const part of params.prompt) {
1302
+ switch (part.type) {
1303
+ case "text":
1304
+ const audience = part.annotations?.audience
1305
+ const forAssistant = audience?.length === 1 && audience[0] === "assistant"
1306
+ const forUser = audience?.length === 1 && audience[0] === "user"
1307
+ parts.push({
1308
+ type: "text" as const,
1309
+ text: part.text,
1310
+ ...(forAssistant && { synthetic: true }),
1311
+ ...(forUser && { ignored: true }),
1312
+ })
1313
+ break
1314
+ case "image": {
1315
+ const parsed = parseUri(part.uri ?? "")
1316
+ const filename = parsed.type === "file" ? parsed.filename : "image"
1317
+ if (part.data) {
1318
+ parts.push({
1319
+ type: "file",
1320
+ url: `data:${part.mimeType};base64,${part.data}`,
1321
+ filename,
1322
+ mime: part.mimeType,
1323
+ })
1324
+ } else if (part.uri && part.uri.startsWith("http:")) {
1325
+ parts.push({
1326
+ type: "file",
1327
+ url: part.uri,
1328
+ filename,
1329
+ mime: part.mimeType,
1330
+ })
1331
+ }
1332
+ break
1333
+ }
1334
+
1335
+ case "resource_link":
1336
+ const parsed = parseUri(part.uri)
1337
+ // Use the name from resource_link if available
1338
+ if (part.name && parsed.type === "file") {
1339
+ parsed.filename = part.name
1340
+ }
1341
+ parts.push(parsed)
1342
+
1343
+ break
1344
+
1345
+ case "resource": {
1346
+ const resource = part.resource
1347
+ if ("text" in resource && resource.text) {
1348
+ parts.push({
1349
+ type: "text",
1350
+ text: resource.text,
1351
+ })
1352
+ } else if ("blob" in resource && resource.blob && resource.mimeType) {
1353
+ // Binary resource (PDFs, etc.): store as file part with data URL
1354
+ const parsed = parseUri(resource.uri ?? "")
1355
+ const filename = parsed.type === "file" ? parsed.filename : "file"
1356
+ parts.push({
1357
+ type: "file",
1358
+ url: `data:${resource.mimeType};base64,${resource.blob}`,
1359
+ filename,
1360
+ mime: resource.mimeType,
1361
+ })
1362
+ }
1363
+ break
1364
+ }
1365
+
1366
+ default:
1367
+ break
1368
+ }
1369
+ }
1370
+
1371
+ log.info("parts", { parts })
1372
+
1373
+ const cmd = (() => {
1374
+ const text = parts
1375
+ .filter((p): p is { type: "text"; text: string } => p.type === "text")
1376
+ .map((p) => p.text)
1377
+ .join("")
1378
+ .trim()
1379
+
1380
+ if (!text.startsWith("/")) return
1381
+
1382
+ const [name, ...rest] = text.slice(1).split(/\s+/)
1383
+ return { name, args: rest.join(" ").trim() }
1384
+ })()
1385
+
1386
+ const buildUsage = (msg: AssistantMessage): Usage => ({
1387
+ totalTokens:
1388
+ msg.tokens.input +
1389
+ msg.tokens.output +
1390
+ msg.tokens.reasoning +
1391
+ (msg.tokens.cache?.read ?? 0) +
1392
+ (msg.tokens.cache?.write ?? 0),
1393
+ inputTokens: msg.tokens.input,
1394
+ outputTokens: msg.tokens.output,
1395
+ thoughtTokens: msg.tokens.reasoning || undefined,
1396
+ cachedReadTokens: msg.tokens.cache?.read || undefined,
1397
+ cachedWriteTokens: msg.tokens.cache?.write || undefined,
1398
+ })
1399
+
1400
+ if (!cmd) {
1401
+ const response = await this.sdk.session.prompt({
1402
+ sessionID,
1403
+ model: {
1404
+ providerID: model.providerID,
1405
+ modelID: model.modelID,
1406
+ },
1407
+ variant: this.sessionManager.getVariant(sessionID),
1408
+ parts,
1409
+ agent,
1410
+ directory,
1411
+ })
1412
+ const msg = response.data?.info
1413
+
1414
+ await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
1415
+
1416
+ return {
1417
+ stopReason: "end_turn" as const,
1418
+ usage: msg ? buildUsage(msg) : undefined,
1419
+ _meta: {},
1420
+ }
1421
+ }
1422
+
1423
+ const command = await this.config.sdk.command
1424
+ .list({ directory }, { throwOnError: true })
1425
+ .then((x) => x.data!.find((c) => c.name === cmd.name))
1426
+ if (command) {
1427
+ const response = await this.sdk.session.command({
1428
+ sessionID,
1429
+ command: command.name,
1430
+ arguments: cmd.args,
1431
+ model: model.providerID + "/" + model.modelID,
1432
+ agent,
1433
+ directory,
1434
+ })
1435
+ const msg = response.data?.info
1436
+
1437
+ await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
1438
+
1439
+ return {
1440
+ stopReason: "end_turn" as const,
1441
+ usage: msg ? buildUsage(msg) : undefined,
1442
+ _meta: {},
1443
+ }
1444
+ }
1445
+
1446
+ switch (cmd.name) {
1447
+ case "compact":
1448
+ await this.config.sdk.session.summarize(
1449
+ {
1450
+ sessionID,
1451
+ directory,
1452
+ providerID: model.providerID,
1453
+ modelID: model.modelID,
1454
+ },
1455
+ { throwOnError: true },
1456
+ )
1457
+ break
1458
+ }
1459
+
1460
+ await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
1461
+
1462
+ return {
1463
+ stopReason: "end_turn" as const,
1464
+ _meta: {},
1465
+ }
1466
+ }
1467
+
1468
+ async cancel(params: CancelNotification) {
1469
+ const session = this.sessionManager.get(params.sessionId)
1470
+ await this.config.sdk.session.abort(
1471
+ {
1472
+ sessionID: params.sessionId,
1473
+ directory: session.cwd,
1474
+ },
1475
+ { throwOnError: true },
1476
+ )
1477
+ }
1478
+ }
1479
+
1480
+ function toToolKind(toolName: string): ToolKind {
1481
+ const tool = toolName.toLocaleLowerCase()
1482
+ switch (tool) {
1483
+ case "bash":
1484
+ return "execute"
1485
+ case "webfetch":
1486
+ return "fetch"
1487
+
1488
+ case "edit":
1489
+ case "patch":
1490
+ case "write":
1491
+ return "edit"
1492
+
1493
+ case "grep":
1494
+ case "glob":
1495
+ case "context7_resolve_library_id":
1496
+ case "context7_get_library_docs":
1497
+ return "search"
1498
+
1499
+ case "list":
1500
+ case "read":
1501
+ return "read"
1502
+
1503
+ default:
1504
+ return "other"
1505
+ }
1506
+ }
1507
+
1508
+ function toLocations(toolName: string, input: Record<string, any>): { path: string }[] {
1509
+ const tool = toolName.toLocaleLowerCase()
1510
+ switch (tool) {
1511
+ case "read":
1512
+ case "edit":
1513
+ case "write":
1514
+ return input["filePath"] ? [{ path: input["filePath"] }] : []
1515
+ case "glob":
1516
+ case "grep":
1517
+ return input["path"] ? [{ path: input["path"] }] : []
1518
+ case "bash":
1519
+ return []
1520
+ case "list":
1521
+ return input["path"] ? [{ path: input["path"] }] : []
1522
+ default:
1523
+ return []
1524
+ }
1525
+ }
1526
+
1527
+ async function defaultModel(config: ACPConfig, cwd?: string) {
1528
+ const sdk = config.sdk
1529
+ const configured = config.defaultModel
1530
+ if (configured) return configured
1531
+
1532
+ const directory = cwd ?? process.cwd()
1533
+
1534
+ const specified = await sdk.config
1535
+ .get({ directory }, { throwOnError: true })
1536
+ .then((resp) => {
1537
+ const cfg = resp.data
1538
+ if (!cfg || !cfg.model) return undefined
1539
+ const parsed = Provider.parseModel(cfg.model)
1540
+ return {
1541
+ providerID: parsed.providerID,
1542
+ modelID: parsed.modelID,
1543
+ }
1544
+ })
1545
+ .catch((error) => {
1546
+ log.error("failed to load user config for default model", { error })
1547
+ return undefined
1548
+ })
1549
+
1550
+ const providers = await sdk.config
1551
+ .providers({ directory }, { throwOnError: true })
1552
+ .then((x) => x.data?.providers ?? [])
1553
+ .catch((error) => {
1554
+ log.error("failed to list providers for default model", { error })
1555
+ return []
1556
+ })
1557
+
1558
+ if (specified && providers.length) {
1559
+ const provider = providers.find((p) => p.id === specified.providerID)
1560
+ if (provider && provider.models[specified.modelID]) return specified
1561
+ }
1562
+
1563
+ if (specified && !providers.length) return specified
1564
+
1565
+ const opencodeProvider = providers.find((p) => p.id === "opencode")
1566
+ if (opencodeProvider) {
1567
+ if (opencodeProvider.models["big-pickle"]) {
1568
+ return { providerID: "opencode", modelID: "big-pickle" }
1569
+ }
1570
+ const [best] = Provider.sort(Object.values(opencodeProvider.models))
1571
+ if (best) {
1572
+ return {
1573
+ providerID: best.providerID,
1574
+ modelID: best.id,
1575
+ }
1576
+ }
1577
+ }
1578
+
1579
+ const models = providers.flatMap((p) => Object.values(p.models))
1580
+ const [best] = Provider.sort(models)
1581
+ if (best) {
1582
+ return {
1583
+ providerID: best.providerID,
1584
+ modelID: best.id,
1585
+ }
1586
+ }
1587
+
1588
+ if (specified) return specified
1589
+
1590
+ return { providerID: "opencode", modelID: "big-pickle" }
1591
+ }
1592
+
1593
+ function parseUri(
1594
+ uri: string,
1595
+ ): { type: "file"; url: string; filename: string; mime: string } | { type: "text"; text: string } {
1596
+ try {
1597
+ if (uri.startsWith("file://")) {
1598
+ const path = uri.slice(7)
1599
+ const name = path.split("/").pop() || path
1600
+ return {
1601
+ type: "file",
1602
+ url: uri,
1603
+ filename: name,
1604
+ mime: "text/plain",
1605
+ }
1606
+ }
1607
+ if (uri.startsWith("zed://")) {
1608
+ const url = new URL(uri)
1609
+ const path = url.searchParams.get("path")
1610
+ if (path) {
1611
+ const name = path.split("/").pop() || path
1612
+ return {
1613
+ type: "file",
1614
+ url: pathToFileURL(path).href,
1615
+ filename: name,
1616
+ mime: "text/plain",
1617
+ }
1618
+ }
1619
+ }
1620
+ return {
1621
+ type: "text",
1622
+ text: uri,
1623
+ }
1624
+ } catch {
1625
+ return {
1626
+ type: "text",
1627
+ text: uri,
1628
+ }
1629
+ }
1630
+ }
1631
+
1632
+ function getNewContent(fileOriginal: string, unifiedDiff: string): string | undefined {
1633
+ const result = applyPatch(fileOriginal, unifiedDiff)
1634
+ if (result === false) {
1635
+ log.error("Failed to apply unified diff (context mismatch)")
1636
+ return undefined
1637
+ }
1638
+ return result
1639
+ }
1640
+
1641
+ function sortProvidersByName<T extends { name: string }>(providers: T[]): T[] {
1642
+ return [...providers].sort((a, b) => {
1643
+ const nameA = a.name.toLowerCase()
1644
+ const nameB = b.name.toLowerCase()
1645
+ if (nameA < nameB) return -1
1646
+ if (nameA > nameB) return 1
1647
+ return 0
1648
+ })
1649
+ }
1650
+
1651
+ function modelVariantsFromProviders(
1652
+ providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
1653
+ model: { providerID: string; modelID: string },
1654
+ ): string[] {
1655
+ const provider = providers.find((entry) => entry.id === model.providerID)
1656
+ if (!provider) return []
1657
+ const modelInfo = provider.models[model.modelID]
1658
+ if (!modelInfo?.variants) return []
1659
+ return Object.keys(modelInfo.variants)
1660
+ }
1661
+
1662
+ function buildAvailableModels(
1663
+ providers: Array<{ id: string; name: string; models: Record<string, any> }>,
1664
+ options: { includeVariants?: boolean } = {},
1665
+ ): ModelOption[] {
1666
+ const includeVariants = options.includeVariants ?? false
1667
+ return providers.flatMap((provider) => {
1668
+ const models = Provider.sort(Object.values(provider.models) as any)
1669
+ return models.flatMap((model) => {
1670
+ const base: ModelOption = {
1671
+ modelId: `${provider.id}/${model.id}`,
1672
+ name: `${provider.name}/${model.name}`,
1673
+ }
1674
+ if (!includeVariants || !model.variants) return [base]
1675
+ const variants = Object.keys(model.variants).filter((variant) => variant !== DEFAULT_VARIANT_VALUE)
1676
+ const variantOptions = variants.map((variant) => ({
1677
+ modelId: `${provider.id}/${model.id}/${variant}`,
1678
+ name: `${provider.name}/${model.name} (${variant})`,
1679
+ }))
1680
+ return [base, ...variantOptions]
1681
+ })
1682
+ })
1683
+ }
1684
+
1685
+ function formatModelIdWithVariant(
1686
+ model: { providerID: string; modelID: string },
1687
+ variant: string | undefined,
1688
+ availableVariants: string[],
1689
+ includeVariant: boolean,
1690
+ ) {
1691
+ const base = `${model.providerID}/${model.modelID}`
1692
+ if (!includeVariant || !variant || !availableVariants.includes(variant)) return base
1693
+ return `${base}/${variant}`
1694
+ }
1695
+
1696
+ function buildVariantMeta(input: {
1697
+ model: { providerID: string; modelID: string }
1698
+ variant?: string
1699
+ availableVariants: string[]
1700
+ }) {
1701
+ return {
1702
+ opencode: {
1703
+ modelId: `${input.model.providerID}/${input.model.modelID}`,
1704
+ variant: input.variant ?? null,
1705
+ availableVariants: input.availableVariants,
1706
+ },
1707
+ }
1708
+ }
1709
+
1710
+ function parseModelSelection(
1711
+ modelId: string,
1712
+ providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
1713
+ ): { model: { providerID: string; modelID: string }; variant?: string } {
1714
+ const parsed = Provider.parseModel(modelId)
1715
+ const provider = providers.find((p) => p.id === parsed.providerID)
1716
+ if (!provider) {
1717
+ return { model: parsed, variant: undefined }
1718
+ }
1719
+
1720
+ // Check if modelID exists directly
1721
+ if (provider.models[parsed.modelID]) {
1722
+ return { model: parsed, variant: undefined }
1723
+ }
1724
+
1725
+ // Try to extract variant from end of modelID (e.g., "claude-sonnet-4/high" -> model: "claude-sonnet-4", variant: "high")
1726
+ const segments = parsed.modelID.split("/")
1727
+ if (segments.length > 1) {
1728
+ const candidateVariant = segments[segments.length - 1]
1729
+ const baseModelId = segments.slice(0, -1).join("/")
1730
+ const baseModelInfo = provider.models[baseModelId]
1731
+ if (baseModelInfo?.variants && candidateVariant in baseModelInfo.variants) {
1732
+ return {
1733
+ model: { providerID: parsed.providerID, modelID: baseModelId },
1734
+ variant: candidateVariant,
1735
+ }
1736
+ }
1737
+ }
1738
+
1739
+ return { model: parsed, variant: undefined }
1740
+ }
1741
+ }