nuwaxcode 1.1.34 → 1.1.44

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 (398) hide show
  1. package/bin/nuwaxcode +19 -1
  2. package/package.json +15 -117
  3. package/{script/postinstall.mjs → postinstall.mjs} +18 -6
  4. package/AGENTS.md +0 -27
  5. package/Dockerfile +0 -18
  6. package/README.md +0 -15
  7. package/bunfig.toml +0 -7
  8. package/parsers-config.ts +0 -253
  9. package/script/build.ts +0 -172
  10. package/script/publish-registries.ts +0 -187
  11. package/script/publish.ts +0 -70
  12. package/script/schema.ts +0 -47
  13. package/src/acp/README.md +0 -164
  14. package/src/acp/agent.ts +0 -1280
  15. package/src/acp/session.ts +0 -111
  16. package/src/acp/types.ts +0 -24
  17. package/src/agent/agent.ts +0 -332
  18. package/src/agent/generate.txt +0 -75
  19. package/src/agent/prompt/compaction.txt +0 -12
  20. package/src/agent/prompt/explore.txt +0 -18
  21. package/src/agent/prompt/summary.txt +0 -11
  22. package/src/agent/prompt/title.txt +0 -43
  23. package/src/auth/index.ts +0 -73
  24. package/src/bun/index.ts +0 -134
  25. package/src/bus/bus-event.ts +0 -43
  26. package/src/bus/global.ts +0 -10
  27. package/src/bus/index.ts +0 -105
  28. package/src/cli/bootstrap.ts +0 -17
  29. package/src/cli/cmd/acp.ts +0 -69
  30. package/src/cli/cmd/agent.ts +0 -257
  31. package/src/cli/cmd/auth.ts +0 -400
  32. package/src/cli/cmd/cmd.ts +0 -7
  33. package/src/cli/cmd/debug/agent.ts +0 -166
  34. package/src/cli/cmd/debug/config.ts +0 -16
  35. package/src/cli/cmd/debug/file.ts +0 -97
  36. package/src/cli/cmd/debug/index.ts +0 -48
  37. package/src/cli/cmd/debug/lsp.ts +0 -52
  38. package/src/cli/cmd/debug/ripgrep.ts +0 -87
  39. package/src/cli/cmd/debug/scrap.ts +0 -16
  40. package/src/cli/cmd/debug/skill.ts +0 -16
  41. package/src/cli/cmd/debug/snapshot.ts +0 -52
  42. package/src/cli/cmd/export.ts +0 -88
  43. package/src/cli/cmd/generate.ts +0 -38
  44. package/src/cli/cmd/github.ts +0 -1548
  45. package/src/cli/cmd/import.ts +0 -98
  46. package/src/cli/cmd/mcp.ts +0 -755
  47. package/src/cli/cmd/models.ts +0 -77
  48. package/src/cli/cmd/pr.ts +0 -112
  49. package/src/cli/cmd/run.ts +0 -395
  50. package/src/cli/cmd/serve.ts +0 -20
  51. package/src/cli/cmd/session.ts +0 -135
  52. package/src/cli/cmd/stats.ts +0 -402
  53. package/src/cli/cmd/tui/app.tsx +0 -761
  54. package/src/cli/cmd/tui/attach.ts +0 -31
  55. package/src/cli/cmd/tui/component/border.tsx +0 -21
  56. package/src/cli/cmd/tui/component/dialog-agent.tsx +0 -31
  57. package/src/cli/cmd/tui/component/dialog-command.tsx +0 -148
  58. package/src/cli/cmd/tui/component/dialog-mcp.tsx +0 -86
  59. package/src/cli/cmd/tui/component/dialog-model.tsx +0 -234
  60. package/src/cli/cmd/tui/component/dialog-provider.tsx +0 -256
  61. package/src/cli/cmd/tui/component/dialog-session-list.tsx +0 -114
  62. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +0 -31
  63. package/src/cli/cmd/tui/component/dialog-stash.tsx +0 -87
  64. package/src/cli/cmd/tui/component/dialog-status.tsx +0 -164
  65. package/src/cli/cmd/tui/component/dialog-tag.tsx +0 -44
  66. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +0 -50
  67. package/src/cli/cmd/tui/component/logo.tsx +0 -88
  68. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +0 -632
  69. package/src/cli/cmd/tui/component/prompt/frecency.tsx +0 -89
  70. package/src/cli/cmd/tui/component/prompt/history.tsx +0 -108
  71. package/src/cli/cmd/tui/component/prompt/index.tsx +0 -1096
  72. package/src/cli/cmd/tui/component/prompt/stash.tsx +0 -101
  73. package/src/cli/cmd/tui/component/textarea-keybindings.ts +0 -73
  74. package/src/cli/cmd/tui/component/tips.tsx +0 -153
  75. package/src/cli/cmd/tui/component/todo-item.tsx +0 -32
  76. package/src/cli/cmd/tui/context/args.tsx +0 -14
  77. package/src/cli/cmd/tui/context/directory.ts +0 -13
  78. package/src/cli/cmd/tui/context/exit.tsx +0 -23
  79. package/src/cli/cmd/tui/context/helper.tsx +0 -25
  80. package/src/cli/cmd/tui/context/keybind.tsx +0 -101
  81. package/src/cli/cmd/tui/context/kv.tsx +0 -52
  82. package/src/cli/cmd/tui/context/local.tsx +0 -402
  83. package/src/cli/cmd/tui/context/prompt.tsx +0 -18
  84. package/src/cli/cmd/tui/context/route.tsx +0 -46
  85. package/src/cli/cmd/tui/context/sdk.tsx +0 -94
  86. package/src/cli/cmd/tui/context/sync.tsx +0 -427
  87. package/src/cli/cmd/tui/context/theme/aura.json +0 -69
  88. package/src/cli/cmd/tui/context/theme/ayu.json +0 -80
  89. package/src/cli/cmd/tui/context/theme/carbonfox.json +0 -248
  90. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +0 -233
  91. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +0 -233
  92. package/src/cli/cmd/tui/context/theme/catppuccin.json +0 -112
  93. package/src/cli/cmd/tui/context/theme/cobalt2.json +0 -228
  94. package/src/cli/cmd/tui/context/theme/cursor.json +0 -249
  95. package/src/cli/cmd/tui/context/theme/dracula.json +0 -219
  96. package/src/cli/cmd/tui/context/theme/everforest.json +0 -241
  97. package/src/cli/cmd/tui/context/theme/flexoki.json +0 -237
  98. package/src/cli/cmd/tui/context/theme/github.json +0 -233
  99. package/src/cli/cmd/tui/context/theme/gruvbox.json +0 -95
  100. package/src/cli/cmd/tui/context/theme/kanagawa.json +0 -77
  101. package/src/cli/cmd/tui/context/theme/lucent-orng.json +0 -237
  102. package/src/cli/cmd/tui/context/theme/material.json +0 -235
  103. package/src/cli/cmd/tui/context/theme/matrix.json +0 -77
  104. package/src/cli/cmd/tui/context/theme/mercury.json +0 -252
  105. package/src/cli/cmd/tui/context/theme/monokai.json +0 -221
  106. package/src/cli/cmd/tui/context/theme/nightowl.json +0 -221
  107. package/src/cli/cmd/tui/context/theme/nord.json +0 -223
  108. package/src/cli/cmd/tui/context/theme/one-dark.json +0 -84
  109. package/src/cli/cmd/tui/context/theme/orng.json +0 -249
  110. package/src/cli/cmd/tui/context/theme/osaka-jade.json +0 -93
  111. package/src/cli/cmd/tui/context/theme/palenight.json +0 -222
  112. package/src/cli/cmd/tui/context/theme/rosepine.json +0 -234
  113. package/src/cli/cmd/tui/context/theme/solarized.json +0 -223
  114. package/src/cli/cmd/tui/context/theme/synthwave84.json +0 -226
  115. package/src/cli/cmd/tui/context/theme/tokyonight.json +0 -243
  116. package/src/cli/cmd/tui/context/theme/vercel.json +0 -245
  117. package/src/cli/cmd/tui/context/theme/vesper.json +0 -218
  118. package/src/cli/cmd/tui/context/theme/zenburn.json +0 -223
  119. package/src/cli/cmd/tui/context/theme.tsx +0 -1152
  120. package/src/cli/cmd/tui/event.ts +0 -48
  121. package/src/cli/cmd/tui/routes/home.tsx +0 -140
  122. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +0 -64
  123. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +0 -109
  124. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +0 -26
  125. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +0 -47
  126. package/src/cli/cmd/tui/routes/session/footer.tsx +0 -91
  127. package/src/cli/cmd/tui/routes/session/header.tsx +0 -136
  128. package/src/cli/cmd/tui/routes/session/index.tsx +0 -2050
  129. package/src/cli/cmd/tui/routes/session/permission.tsx +0 -495
  130. package/src/cli/cmd/tui/routes/session/question.tsx +0 -435
  131. package/src/cli/cmd/tui/routes/session/sidebar.tsx +0 -312
  132. package/src/cli/cmd/tui/thread.ts +0 -165
  133. package/src/cli/cmd/tui/ui/dialog-alert.tsx +0 -57
  134. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +0 -83
  135. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +0 -204
  136. package/src/cli/cmd/tui/ui/dialog-help.tsx +0 -38
  137. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +0 -77
  138. package/src/cli/cmd/tui/ui/dialog-select.tsx +0 -354
  139. package/src/cli/cmd/tui/ui/dialog.tsx +0 -167
  140. package/src/cli/cmd/tui/ui/link.tsx +0 -28
  141. package/src/cli/cmd/tui/ui/spinner.ts +0 -368
  142. package/src/cli/cmd/tui/ui/toast.tsx +0 -100
  143. package/src/cli/cmd/tui/util/clipboard.ts +0 -160
  144. package/src/cli/cmd/tui/util/editor.ts +0 -32
  145. package/src/cli/cmd/tui/util/signal.ts +0 -7
  146. package/src/cli/cmd/tui/util/terminal.ts +0 -114
  147. package/src/cli/cmd/tui/util/transcript.ts +0 -98
  148. package/src/cli/cmd/tui/worker.ts +0 -152
  149. package/src/cli/cmd/uninstall.ts +0 -357
  150. package/src/cli/cmd/upgrade.ts +0 -73
  151. package/src/cli/cmd/web.ts +0 -81
  152. package/src/cli/error.ts +0 -57
  153. package/src/cli/network.ts +0 -53
  154. package/src/cli/ui.ts +0 -84
  155. package/src/cli/upgrade.ts +0 -25
  156. package/src/command/index.ts +0 -131
  157. package/src/command/template/initialize.txt +0 -10
  158. package/src/command/template/review.txt +0 -99
  159. package/src/config/config.ts +0 -1255
  160. package/src/config/markdown.ts +0 -93
  161. package/src/env/index.ts +0 -26
  162. package/src/file/ignore.ts +0 -83
  163. package/src/file/index.ts +0 -411
  164. package/src/file/ripgrep.ts +0 -409
  165. package/src/file/time.ts +0 -64
  166. package/src/file/watcher.ts +0 -118
  167. package/src/flag/flag.ts +0 -54
  168. package/src/format/formatter.ts +0 -359
  169. package/src/format/index.ts +0 -137
  170. package/src/global/index.ts +0 -55
  171. package/src/id/id.ts +0 -83
  172. package/src/ide/index.ts +0 -76
  173. package/src/index.ts +0 -159
  174. package/src/installation/index.ts +0 -246
  175. package/src/lsp/client.ts +0 -252
  176. package/src/lsp/index.ts +0 -485
  177. package/src/lsp/language.ts +0 -119
  178. package/src/lsp/server.ts +0 -2046
  179. package/src/mcp/auth.ts +0 -135
  180. package/src/mcp/index.ts +0 -926
  181. package/src/mcp/oauth-callback.ts +0 -200
  182. package/src/mcp/oauth-provider.ts +0 -154
  183. package/src/patch/index.ts +0 -680
  184. package/src/permission/arity.ts +0 -163
  185. package/src/permission/index.ts +0 -210
  186. package/src/permission/next.ts +0 -269
  187. package/src/plugin/codex.ts +0 -493
  188. package/src/plugin/copilot.ts +0 -269
  189. package/src/plugin/index.ts +0 -135
  190. package/src/project/bootstrap.ts +0 -35
  191. package/src/project/instance.ts +0 -91
  192. package/src/project/project.ts +0 -320
  193. package/src/project/state.ts +0 -66
  194. package/src/project/vcs.ts +0 -76
  195. package/src/provider/auth.ts +0 -147
  196. package/src/provider/models-macro.ts +0 -11
  197. package/src/provider/models.ts +0 -112
  198. package/src/provider/provider.ts +0 -1219
  199. package/src/provider/sdk/openai-compatible/src/README.md +0 -5
  200. package/src/provider/sdk/openai-compatible/src/index.ts +0 -2
  201. package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +0 -100
  202. package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +0 -303
  203. package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +0 -22
  204. package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +0 -18
  205. package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +0 -22
  206. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +0 -207
  207. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +0 -1732
  208. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +0 -177
  209. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +0 -1
  210. package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +0 -88
  211. package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +0 -128
  212. package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +0 -115
  213. package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +0 -65
  214. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +0 -104
  215. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +0 -103
  216. package/src/provider/transform.ts +0 -724
  217. package/src/pty/index.ts +0 -229
  218. package/src/question/index.ts +0 -171
  219. package/src/scheduler/index.ts +0 -61
  220. package/src/server/error.ts +0 -36
  221. package/src/server/mdns.ts +0 -59
  222. package/src/server/routes/config.ts +0 -92
  223. package/src/server/routes/experimental.ts +0 -157
  224. package/src/server/routes/file.ts +0 -197
  225. package/src/server/routes/global.ts +0 -135
  226. package/src/server/routes/mcp.ts +0 -225
  227. package/src/server/routes/permission.ts +0 -68
  228. package/src/server/routes/project.ts +0 -82
  229. package/src/server/routes/provider.ts +0 -165
  230. package/src/server/routes/pty.ts +0 -169
  231. package/src/server/routes/question.ts +0 -98
  232. package/src/server/routes/session.ts +0 -935
  233. package/src/server/routes/tui.ts +0 -379
  234. package/src/server/server.ts +0 -578
  235. package/src/session/compaction.ts +0 -225
  236. package/src/session/index.ts +0 -488
  237. package/src/session/llm.ts +0 -279
  238. package/src/session/message-v2.ts +0 -702
  239. package/src/session/message.ts +0 -189
  240. package/src/session/processor.ts +0 -406
  241. package/src/session/prompt/anthropic-20250930.txt +0 -166
  242. package/src/session/prompt/anthropic.txt +0 -105
  243. package/src/session/prompt/anthropic_spoof.txt +0 -1
  244. package/src/session/prompt/beast.txt +0 -147
  245. package/src/session/prompt/build-switch.txt +0 -5
  246. package/src/session/prompt/codex.txt +0 -73
  247. package/src/session/prompt/codex_header.txt +0 -72
  248. package/src/session/prompt/copilot-gpt-5.txt +0 -143
  249. package/src/session/prompt/gemini.txt +0 -155
  250. package/src/session/prompt/max-steps.txt +0 -16
  251. package/src/session/prompt/plan-reminder-anthropic.txt +0 -67
  252. package/src/session/prompt/plan.txt +0 -26
  253. package/src/session/prompt/qwen.txt +0 -109
  254. package/src/session/prompt.ts +0 -1805
  255. package/src/session/retry.ts +0 -90
  256. package/src/session/revert.ts +0 -108
  257. package/src/session/status.ts +0 -76
  258. package/src/session/summary.ts +0 -150
  259. package/src/session/system.ts +0 -138
  260. package/src/session/todo.ts +0 -37
  261. package/src/share/share-next.ts +0 -194
  262. package/src/share/share.ts +0 -87
  263. package/src/shell/shell.ts +0 -67
  264. package/src/skill/index.ts +0 -1
  265. package/src/skill/skill.ts +0 -136
  266. package/src/snapshot/index.ts +0 -236
  267. package/src/storage/storage.ts +0 -227
  268. package/src/tool/apply_patch.ts +0 -277
  269. package/src/tool/apply_patch.txt +0 -1
  270. package/src/tool/bash.ts +0 -258
  271. package/src/tool/bash.txt +0 -115
  272. package/src/tool/batch.ts +0 -175
  273. package/src/tool/batch.txt +0 -24
  274. package/src/tool/codesearch.ts +0 -132
  275. package/src/tool/codesearch.txt +0 -12
  276. package/src/tool/edit.ts +0 -645
  277. package/src/tool/edit.txt +0 -10
  278. package/src/tool/external-directory.ts +0 -32
  279. package/src/tool/glob.ts +0 -77
  280. package/src/tool/glob.txt +0 -6
  281. package/src/tool/grep.ts +0 -154
  282. package/src/tool/grep.txt +0 -8
  283. package/src/tool/invalid.ts +0 -17
  284. package/src/tool/ls.ts +0 -121
  285. package/src/tool/ls.txt +0 -1
  286. package/src/tool/lsp.ts +0 -96
  287. package/src/tool/lsp.txt +0 -19
  288. package/src/tool/multiedit.ts +0 -46
  289. package/src/tool/multiedit.txt +0 -41
  290. package/src/tool/plan-enter.txt +0 -14
  291. package/src/tool/plan-exit.txt +0 -13
  292. package/src/tool/plan.ts +0 -130
  293. package/src/tool/question.ts +0 -33
  294. package/src/tool/question.txt +0 -10
  295. package/src/tool/read.ts +0 -202
  296. package/src/tool/read.txt +0 -12
  297. package/src/tool/registry.ts +0 -158
  298. package/src/tool/skill.ts +0 -75
  299. package/src/tool/task.ts +0 -188
  300. package/src/tool/task.txt +0 -60
  301. package/src/tool/todo.ts +0 -53
  302. package/src/tool/todoread.txt +0 -14
  303. package/src/tool/todowrite.txt +0 -167
  304. package/src/tool/tool.ts +0 -88
  305. package/src/tool/truncation.ts +0 -106
  306. package/src/tool/webfetch.ts +0 -182
  307. package/src/tool/webfetch.txt +0 -13
  308. package/src/tool/websearch.ts +0 -150
  309. package/src/tool/websearch.txt +0 -14
  310. package/src/tool/write.ts +0 -80
  311. package/src/tool/write.txt +0 -8
  312. package/src/util/archive.ts +0 -16
  313. package/src/util/color.ts +0 -19
  314. package/src/util/context.ts +0 -25
  315. package/src/util/defer.ts +0 -12
  316. package/src/util/eventloop.ts +0 -20
  317. package/src/util/filesystem.ts +0 -93
  318. package/src/util/fn.ts +0 -11
  319. package/src/util/format.ts +0 -20
  320. package/src/util/iife.ts +0 -3
  321. package/src/util/keybind.ts +0 -103
  322. package/src/util/lazy.ts +0 -18
  323. package/src/util/locale.ts +0 -81
  324. package/src/util/lock.ts +0 -98
  325. package/src/util/log.ts +0 -180
  326. package/src/util/queue.ts +0 -32
  327. package/src/util/rpc.ts +0 -66
  328. package/src/util/scrap.ts +0 -10
  329. package/src/util/signal.ts +0 -12
  330. package/src/util/timeout.ts +0 -14
  331. package/src/util/token.ts +0 -7
  332. package/src/util/wildcard.ts +0 -56
  333. package/src/worktree/index.ts +0 -217
  334. package/sst-env.d.ts +0 -9
  335. package/test/acp/event-subscription.test.ts +0 -436
  336. package/test/acp/system-prompt.test.ts +0 -262
  337. package/test/agent/agent.test.ts +0 -638
  338. package/test/bun.test.ts +0 -53
  339. package/test/cli/github-action.test.ts +0 -129
  340. package/test/cli/github-remote.test.ts +0 -80
  341. package/test/cli/tui/transcript.test.ts +0 -297
  342. package/test/config/agent-color.test.ts +0 -66
  343. package/test/config/config.test.ts +0 -1414
  344. package/test/config/fixtures/empty-frontmatter.md +0 -4
  345. package/test/config/fixtures/frontmatter.md +0 -28
  346. package/test/config/fixtures/no-frontmatter.md +0 -1
  347. package/test/config/markdown.test.ts +0 -192
  348. package/test/file/ignore.test.ts +0 -10
  349. package/test/file/path-traversal.test.ts +0 -198
  350. package/test/fixture/fixture.ts +0 -45
  351. package/test/fixture/lsp/fake-lsp-server.js +0 -77
  352. package/test/ide/ide.test.ts +0 -82
  353. package/test/keybind.test.ts +0 -421
  354. package/test/lsp/client.test.ts +0 -95
  355. package/test/mcp/headers.test.ts +0 -153
  356. package/test/mcp/oauth-browser.test.ts +0 -261
  357. package/test/patch/patch.test.ts +0 -348
  358. package/test/permission/arity.test.ts +0 -33
  359. package/test/permission/next.test.ts +0 -652
  360. package/test/permission-task.test.ts +0 -319
  361. package/test/plugin/codex.test.ts +0 -123
  362. package/test/preload.ts +0 -65
  363. package/test/project/project.test.ts +0 -120
  364. package/test/provider/amazon-bedrock.test.ts +0 -268
  365. package/test/provider/gitlab-duo.test.ts +0 -286
  366. package/test/provider/provider.test.ts +0 -2149
  367. package/test/provider/transform.test.ts +0 -1596
  368. package/test/question/question.test.ts +0 -300
  369. package/test/scheduler.test.ts +0 -73
  370. package/test/server/session-list.test.ts +0 -39
  371. package/test/server/session-select.test.ts +0 -78
  372. package/test/session/compaction.test.ts +0 -293
  373. package/test/session/llm.test.ts +0 -90
  374. package/test/session/message-v2.test.ts +0 -662
  375. package/test/session/retry.test.ts +0 -131
  376. package/test/session/revert-compact.test.ts +0 -285
  377. package/test/session/session.test.ts +0 -71
  378. package/test/skill/skill.test.ts +0 -185
  379. package/test/snapshot/snapshot.test.ts +0 -939
  380. package/test/tool/__snapshots__/tool.test.ts.snap +0 -9
  381. package/test/tool/apply_patch.test.ts +0 -515
  382. package/test/tool/bash.test.ts +0 -320
  383. package/test/tool/external-directory.test.ts +0 -126
  384. package/test/tool/fixtures/large-image.png +0 -0
  385. package/test/tool/fixtures/models-api.json +0 -33453
  386. package/test/tool/grep.test.ts +0 -109
  387. package/test/tool/question.test.ts +0 -105
  388. package/test/tool/read.test.ts +0 -332
  389. package/test/tool/registry.test.ts +0 -76
  390. package/test/tool/truncation.test.ts +0 -159
  391. package/test/util/filesystem.test.ts +0 -39
  392. package/test/util/format.test.ts +0 -59
  393. package/test/util/iife.test.ts +0 -36
  394. package/test/util/lazy.test.ts +0 -50
  395. package/test/util/lock.test.ts +0 -72
  396. package/test/util/timeout.test.ts +0 -21
  397. package/test/util/wildcard.test.ts +0 -75
  398. package/tsconfig.json +0 -16
@@ -1,1805 +0,0 @@
1
- import path from "path"
2
- import os from "os"
3
- import fs from "fs/promises"
4
- import z from "zod"
5
- import { Identifier } from "../id/id"
6
- import { MessageV2 } from "./message-v2"
7
- import { Log } from "../util/log"
8
- import { SessionRevert } from "./revert"
9
- import { Session } from "."
10
- import { Agent } from "../agent/agent"
11
- import { Provider } from "../provider/provider"
12
- import { type Tool as AITool, tool, jsonSchema, type ToolCallOptions } from "ai"
13
- import { SessionCompaction } from "./compaction"
14
- import { Instance } from "../project/instance"
15
- import { Bus } from "../bus"
16
- import { ProviderTransform } from "../provider/transform"
17
- import { SystemPrompt } from "./system"
18
- import { Plugin } from "../plugin"
19
- import PROMPT_PLAN from "../session/prompt/plan.txt"
20
- import BUILD_SWITCH from "../session/prompt/build-switch.txt"
21
- import MAX_STEPS from "../session/prompt/max-steps.txt"
22
- import { defer } from "../util/defer"
23
- import { clone } from "remeda"
24
- import { ToolRegistry } from "../tool/registry"
25
- import { MCP } from "../mcp"
26
- import { LSP } from "../lsp"
27
- import { ReadTool } from "../tool/read"
28
- import { ListTool } from "../tool/ls"
29
- import { FileTime } from "../file/time"
30
- import { Flag } from "../flag/flag"
31
- import { ulid } from "ulid"
32
- import { spawn } from "child_process"
33
- import { Command } from "../command"
34
- import { $, fileURLToPath } from "bun"
35
- import { ConfigMarkdown } from "../config/markdown"
36
- import { SessionSummary } from "./summary"
37
- import { NamedError } from "@opencode-ai/util/error"
38
- import { fn } from "@/util/fn"
39
- import { SessionProcessor } from "./processor"
40
- import { TaskTool } from "@/tool/task"
41
- import { Tool } from "@/tool/tool"
42
- import { PermissionNext } from "@/permission/next"
43
- import { SessionStatus } from "./status"
44
- import { LLM } from "./llm"
45
- import { iife } from "@/util/iife"
46
- import { Shell } from "@/shell/shell"
47
-
48
- // @ts-ignore
49
- globalThis.AI_SDK_LOG_WARNINGS = false
50
-
51
- export namespace SessionPrompt {
52
- const log = Log.create({ service: "session.prompt" })
53
- export const OUTPUT_TOKEN_MAX = Flag.OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX || 32_000
54
-
55
- const state = Instance.state(
56
- () => {
57
- const data: Record<
58
- string,
59
- {
60
- abort: AbortController
61
- callbacks: {
62
- resolve(input: MessageV2.WithParts): void
63
- reject(): void
64
- }[]
65
- }
66
- > = {}
67
- return data
68
- },
69
- async (current) => {
70
- for (const item of Object.values(current)) {
71
- item.abort.abort()
72
- for (const callback of item.callbacks) {
73
- callback.reject()
74
- }
75
- }
76
- },
77
- )
78
-
79
- export function assertNotBusy(sessionID: string) {
80
- const match = state()[sessionID]
81
- if (match) throw new Session.BusyError(sessionID)
82
- }
83
-
84
- export const PromptInput = z.object({
85
- sessionID: Identifier.schema("session"),
86
- messageID: Identifier.schema("message").optional(),
87
- model: z
88
- .object({
89
- providerID: z.string(),
90
- modelID: z.string(),
91
- })
92
- .optional(),
93
- agent: z.string().optional(),
94
- noReply: z.boolean().optional(),
95
- tools: z
96
- .record(z.string(), z.boolean())
97
- .optional()
98
- .describe(
99
- "@deprecated tools and permissions have been merged, you can set permissions on the session itself now",
100
- ),
101
- system: z.string().optional(),
102
- variant: z.string().optional(),
103
- parts: z.array(
104
- z.discriminatedUnion("type", [
105
- MessageV2.TextPart.omit({
106
- messageID: true,
107
- sessionID: true,
108
- })
109
- .partial({
110
- id: true,
111
- })
112
- .meta({
113
- ref: "TextPartInput",
114
- }),
115
- MessageV2.FilePart.omit({
116
- messageID: true,
117
- sessionID: true,
118
- })
119
- .partial({
120
- id: true,
121
- })
122
- .meta({
123
- ref: "FilePartInput",
124
- }),
125
- MessageV2.AgentPart.omit({
126
- messageID: true,
127
- sessionID: true,
128
- })
129
- .partial({
130
- id: true,
131
- })
132
- .meta({
133
- ref: "AgentPartInput",
134
- }),
135
- MessageV2.SubtaskPart.omit({
136
- messageID: true,
137
- sessionID: true,
138
- })
139
- .partial({
140
- id: true,
141
- })
142
- .meta({
143
- ref: "SubtaskPartInput",
144
- }),
145
- ]),
146
- ),
147
- })
148
- export type PromptInput = z.infer<typeof PromptInput>
149
-
150
- export const prompt = fn(PromptInput, async (input) => {
151
- const session = await Session.get(input.sessionID)
152
- await SessionRevert.cleanup(session)
153
-
154
- const message = await createUserMessage(input)
155
- await Session.touch(input.sessionID)
156
-
157
- // this is backwards compatibility for allowing `tools` to be specified when
158
- // prompting
159
- const permissions: PermissionNext.Ruleset = []
160
- for (const [tool, enabled] of Object.entries(input.tools ?? {})) {
161
- permissions.push({
162
- permission: tool,
163
- action: enabled ? "allow" : "deny",
164
- pattern: "*",
165
- })
166
- }
167
- if (permissions.length > 0) {
168
- session.permission = permissions
169
- await Session.update(session.id, (draft) => {
170
- draft.permission = permissions
171
- })
172
- }
173
-
174
- if (input.noReply === true) {
175
- return message
176
- }
177
-
178
- return loop(input.sessionID)
179
- })
180
-
181
- export async function resolvePromptParts(template: string): Promise<PromptInput["parts"]> {
182
- const parts: PromptInput["parts"] = [
183
- {
184
- type: "text",
185
- text: template,
186
- },
187
- ]
188
- const files = ConfigMarkdown.files(template)
189
- const seen = new Set<string>()
190
- await Promise.all(
191
- files.map(async (match) => {
192
- const name = match[1]
193
- if (seen.has(name)) return
194
- seen.add(name)
195
- const filepath = name.startsWith("~/")
196
- ? path.join(os.homedir(), name.slice(2))
197
- : path.resolve(Instance.worktree, name)
198
-
199
- const stats = await fs.stat(filepath).catch(() => undefined)
200
- if (!stats) {
201
- const agent = await Agent.get(name)
202
- if (agent) {
203
- parts.push({
204
- type: "agent",
205
- name: agent.name,
206
- })
207
- }
208
- return
209
- }
210
-
211
- if (stats.isDirectory()) {
212
- parts.push({
213
- type: "file",
214
- url: `file://${filepath}`,
215
- filename: name,
216
- mime: "application/x-directory",
217
- })
218
- return
219
- }
220
-
221
- parts.push({
222
- type: "file",
223
- url: `file://${filepath}`,
224
- filename: name,
225
- mime: "text/plain",
226
- })
227
- }),
228
- )
229
- return parts
230
- }
231
-
232
- function start(sessionID: string) {
233
- const s = state()
234
- if (s[sessionID]) return
235
- const controller = new AbortController()
236
- s[sessionID] = {
237
- abort: controller,
238
- callbacks: [],
239
- }
240
- return controller.signal
241
- }
242
-
243
- export function cancel(sessionID: string) {
244
- log.info("cancel", { sessionID })
245
- const s = state()
246
- const match = s[sessionID]
247
- if (!match) return
248
- match.abort.abort()
249
- for (const item of match.callbacks) {
250
- item.reject()
251
- }
252
- delete s[sessionID]
253
- SessionStatus.set(sessionID, { type: "idle" })
254
- return
255
- }
256
-
257
- export const loop = fn(Identifier.schema("session"), async (sessionID) => {
258
- const abort = start(sessionID)
259
- if (!abort) {
260
- return new Promise<MessageV2.WithParts>((resolve, reject) => {
261
- const callbacks = state()[sessionID].callbacks
262
- callbacks.push({ resolve, reject })
263
- })
264
- }
265
-
266
- using _ = defer(() => cancel(sessionID))
267
-
268
- let step = 0
269
- const session = await Session.get(sessionID)
270
- while (true) {
271
- SessionStatus.set(sessionID, { type: "busy" })
272
- log.info("loop", { step, sessionID })
273
- if (abort.aborted) break
274
- let msgs = await MessageV2.filterCompacted(MessageV2.stream(sessionID))
275
-
276
- let lastUser: MessageV2.User | undefined
277
- let lastAssistant: MessageV2.Assistant | undefined
278
- let lastFinished: MessageV2.Assistant | undefined
279
- let tasks: (MessageV2.CompactionPart | MessageV2.SubtaskPart)[] = []
280
- for (let i = msgs.length - 1; i >= 0; i--) {
281
- const msg = msgs[i]
282
- if (!lastUser && msg.info.role === "user") lastUser = msg.info as MessageV2.User
283
- if (!lastAssistant && msg.info.role === "assistant") lastAssistant = msg.info as MessageV2.Assistant
284
- if (!lastFinished && msg.info.role === "assistant" && msg.info.finish)
285
- lastFinished = msg.info as MessageV2.Assistant
286
- if (lastUser && lastFinished) break
287
- const task = msg.parts.filter((part) => part.type === "compaction" || part.type === "subtask")
288
- if (task && !lastFinished) {
289
- tasks.push(...task)
290
- }
291
- }
292
-
293
- if (!lastUser) throw new Error("No user message found in stream. This should never happen.")
294
- if (
295
- lastAssistant?.finish &&
296
- !["tool-calls", "unknown"].includes(lastAssistant.finish) &&
297
- lastUser.id < lastAssistant.id
298
- ) {
299
- log.info("exiting loop", { sessionID })
300
- break
301
- }
302
-
303
- step++
304
- if (step === 1)
305
- ensureTitle({
306
- session,
307
- modelID: lastUser.model.modelID,
308
- providerID: lastUser.model.providerID,
309
- history: msgs,
310
- })
311
-
312
- const model = await Provider.getModel(lastUser.model.providerID, lastUser.model.modelID)
313
- const task = tasks.pop()
314
-
315
- // pending subtask
316
- // TODO: centralize "invoke tool" logic
317
- if (task?.type === "subtask") {
318
- const taskTool = await TaskTool.init()
319
- const taskModel = task.model ? await Provider.getModel(task.model.providerID, task.model.modelID) : model
320
- const assistantMessage = (await Session.updateMessage({
321
- id: Identifier.ascending("message"),
322
- role: "assistant",
323
- parentID: lastUser.id,
324
- sessionID,
325
- mode: task.agent,
326
- agent: task.agent,
327
- path: {
328
- cwd: Instance.directory,
329
- root: Instance.worktree,
330
- },
331
- cost: 0,
332
- tokens: {
333
- input: 0,
334
- output: 0,
335
- reasoning: 0,
336
- cache: { read: 0, write: 0 },
337
- },
338
- modelID: taskModel.id,
339
- providerID: taskModel.providerID,
340
- time: {
341
- created: Date.now(),
342
- },
343
- })) as MessageV2.Assistant
344
- let part = (await Session.updatePart({
345
- id: Identifier.ascending("part"),
346
- messageID: assistantMessage.id,
347
- sessionID: assistantMessage.sessionID,
348
- type: "tool",
349
- callID: ulid(),
350
- tool: TaskTool.id,
351
- state: {
352
- status: "running",
353
- input: {
354
- prompt: task.prompt,
355
- description: task.description,
356
- subagent_type: task.agent,
357
- command: task.command,
358
- },
359
- time: {
360
- start: Date.now(),
361
- },
362
- },
363
- })) as MessageV2.ToolPart
364
- const taskArgs = {
365
- prompt: task.prompt,
366
- description: task.description,
367
- subagent_type: task.agent,
368
- command: task.command,
369
- }
370
- await Plugin.trigger(
371
- "tool.execute.before",
372
- {
373
- tool: "task",
374
- sessionID,
375
- callID: part.id,
376
- },
377
- { args: taskArgs },
378
- )
379
- let executionError: Error | undefined
380
- const taskAgent = await Agent.get(task.agent)
381
- const taskCtx: Tool.Context = {
382
- agent: task.agent,
383
- messageID: assistantMessage.id,
384
- sessionID: sessionID,
385
- abort,
386
- callID: part.callID,
387
- extra: { bypassAgentCheck: true },
388
- async metadata(input) {
389
- await Session.updatePart({
390
- ...part,
391
- type: "tool",
392
- state: {
393
- ...part.state,
394
- ...input,
395
- },
396
- } satisfies MessageV2.ToolPart)
397
- },
398
- async ask(req) {
399
- await PermissionNext.ask({
400
- ...req,
401
- sessionID: sessionID,
402
- ruleset: PermissionNext.merge(taskAgent.permission, session.permission ?? []),
403
- })
404
- },
405
- }
406
- const result = await taskTool.execute(taskArgs, taskCtx).catch((error) => {
407
- executionError = error
408
- log.error("subtask execution failed", { error, agent: task.agent, description: task.description })
409
- return undefined
410
- })
411
- await Plugin.trigger(
412
- "tool.execute.after",
413
- {
414
- tool: "task",
415
- sessionID,
416
- callID: part.id,
417
- },
418
- result,
419
- )
420
- assistantMessage.finish = "tool-calls"
421
- assistantMessage.time.completed = Date.now()
422
- await Session.updateMessage(assistantMessage)
423
- if (result && part.state.status === "running") {
424
- await Session.updatePart({
425
- ...part,
426
- state: {
427
- status: "completed",
428
- input: part.state.input,
429
- title: result.title,
430
- metadata: result.metadata,
431
- output: result.output,
432
- attachments: result.attachments,
433
- time: {
434
- ...part.state.time,
435
- end: Date.now(),
436
- },
437
- },
438
- } satisfies MessageV2.ToolPart)
439
- }
440
- if (!result) {
441
- await Session.updatePart({
442
- ...part,
443
- state: {
444
- status: "error",
445
- error: executionError ? `Tool execution failed: ${executionError.message}` : "Tool execution failed",
446
- time: {
447
- start: part.state.status === "running" ? part.state.time.start : Date.now(),
448
- end: Date.now(),
449
- },
450
- metadata: part.metadata,
451
- input: part.state.input,
452
- },
453
- } satisfies MessageV2.ToolPart)
454
- }
455
-
456
- // Add synthetic user message to prevent certain reasoning models from erroring
457
- // If we create assistant messages w/ out user ones following mid loop thinking signatures
458
- // will be missing and it can cause errors for models like gemini for example
459
- const summaryUserMsg: MessageV2.User = {
460
- id: Identifier.ascending("message"),
461
- sessionID,
462
- role: "user",
463
- time: {
464
- created: Date.now(),
465
- },
466
- agent: lastUser.agent,
467
- model: lastUser.model,
468
- }
469
- await Session.updateMessage(summaryUserMsg)
470
- await Session.updatePart({
471
- id: Identifier.ascending("part"),
472
- messageID: summaryUserMsg.id,
473
- sessionID,
474
- type: "text",
475
- text: "Summarize the task tool output above and continue with your task.",
476
- synthetic: true,
477
- } satisfies MessageV2.TextPart)
478
-
479
- continue
480
- }
481
-
482
- // pending compaction
483
- if (task?.type === "compaction") {
484
- const result = await SessionCompaction.process({
485
- messages: msgs,
486
- parentID: lastUser.id,
487
- abort,
488
- sessionID,
489
- auto: task.auto,
490
- })
491
- if (result === "stop") break
492
- continue
493
- }
494
-
495
- // context overflow, needs compaction
496
- if (
497
- lastFinished &&
498
- lastFinished.summary !== true &&
499
- (await SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model }))
500
- ) {
501
- await SessionCompaction.create({
502
- sessionID,
503
- agent: lastUser.agent,
504
- model: lastUser.model,
505
- auto: true,
506
- })
507
- continue
508
- }
509
-
510
- // normal processing
511
- const agent = await Agent.get(lastUser.agent)
512
- const maxSteps = agent.steps ?? Infinity
513
- const isLastStep = step >= maxSteps
514
- msgs = await insertReminders({
515
- messages: msgs,
516
- agent,
517
- session,
518
- })
519
-
520
- const processor = SessionProcessor.create({
521
- assistantMessage: (await Session.updateMessage({
522
- id: Identifier.ascending("message"),
523
- parentID: lastUser.id,
524
- role: "assistant",
525
- mode: agent.name,
526
- agent: agent.name,
527
- path: {
528
- cwd: Instance.directory,
529
- root: Instance.worktree,
530
- },
531
- cost: 0,
532
- tokens: {
533
- input: 0,
534
- output: 0,
535
- reasoning: 0,
536
- cache: { read: 0, write: 0 },
537
- },
538
- modelID: model.id,
539
- providerID: model.providerID,
540
- time: {
541
- created: Date.now(),
542
- },
543
- sessionID,
544
- })) as MessageV2.Assistant,
545
- sessionID: sessionID,
546
- model,
547
- abort,
548
- })
549
-
550
- // Check if user explicitly invoked an agent via @ in this turn
551
- const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
552
- const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false
553
-
554
- const tools = await resolveTools({
555
- agent,
556
- session,
557
- model,
558
- tools: lastUser.tools,
559
- processor,
560
- bypassAgentCheck,
561
- })
562
-
563
- if (step === 1) {
564
- SessionSummary.summarize({
565
- sessionID: sessionID,
566
- messageID: lastUser.id,
567
- })
568
- }
569
-
570
- const sessionMessages = clone(msgs)
571
-
572
- // Ephemerally wrap queued user messages with a reminder to stay on track
573
- if (step > 1 && lastFinished) {
574
- for (const msg of sessionMessages) {
575
- if (msg.info.role !== "user" || msg.info.id <= lastFinished.id) continue
576
- for (const part of msg.parts) {
577
- if (part.type !== "text" || part.ignored || part.synthetic) continue
578
- if (!part.text.trim()) continue
579
- part.text = [
580
- "<system-reminder>",
581
- "The user sent the following message:",
582
- part.text,
583
- "",
584
- "Please address this message and continue with your tasks.",
585
- "</system-reminder>",
586
- ].join("\n")
587
- }
588
- }
589
- }
590
-
591
- await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: sessionMessages })
592
-
593
- const result = await processor.process({
594
- user: lastUser,
595
- agent,
596
- abort,
597
- sessionID,
598
- system: [...(await SystemPrompt.environment()), ...(await SystemPrompt.custom())],
599
- messages: [
600
- ...MessageV2.toModelMessage(sessionMessages),
601
- ...(isLastStep
602
- ? [
603
- {
604
- role: "assistant" as const,
605
- content: MAX_STEPS,
606
- },
607
- ]
608
- : []),
609
- ],
610
- tools,
611
- model,
612
- })
613
- if (result === "stop") break
614
- if (result === "compact") {
615
- await SessionCompaction.create({
616
- sessionID,
617
- agent: lastUser.agent,
618
- model: lastUser.model,
619
- auto: true,
620
- })
621
- }
622
- continue
623
- }
624
- SessionCompaction.prune({ sessionID })
625
- for await (const item of MessageV2.stream(sessionID)) {
626
- if (item.info.role === "user") continue
627
- const queued = state()[sessionID]?.callbacks ?? []
628
- for (const q of queued) {
629
- q.resolve(item)
630
- }
631
- return item
632
- }
633
- throw new Error("Impossible")
634
- })
635
-
636
- async function lastModel(sessionID: string) {
637
- for await (const item of MessageV2.stream(sessionID)) {
638
- if (item.info.role === "user" && item.info.model) return item.info.model
639
- }
640
- return Provider.defaultModel()
641
- }
642
-
643
- async function resolveTools(input: {
644
- agent: Agent.Info
645
- model: Provider.Model
646
- session: Session.Info
647
- tools?: Record<string, boolean>
648
- processor: SessionProcessor.Info
649
- bypassAgentCheck: boolean
650
- }) {
651
- using _ = log.time("resolveTools")
652
- const tools: Record<string, AITool> = {}
653
-
654
- const context = (args: any, options: ToolCallOptions): Tool.Context => ({
655
- sessionID: input.session.id,
656
- abort: options.abortSignal!,
657
- messageID: input.processor.message.id,
658
- callID: options.toolCallId,
659
- extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck },
660
- agent: input.agent.name,
661
- metadata: async (val: { title?: string; metadata?: any }) => {
662
- const match = input.processor.partFromToolCall(options.toolCallId)
663
- if (match && match.state.status === "running") {
664
- await Session.updatePart({
665
- ...match,
666
- state: {
667
- title: val.title,
668
- metadata: val.metadata,
669
- status: "running",
670
- input: args,
671
- time: {
672
- start: Date.now(),
673
- },
674
- },
675
- })
676
- }
677
- },
678
- async ask(req) {
679
- await PermissionNext.ask({
680
- ...req,
681
- sessionID: input.session.id,
682
- tool: { messageID: input.processor.message.id, callID: options.toolCallId },
683
- ruleset: PermissionNext.merge(input.agent.permission, input.session.permission ?? []),
684
- })
685
- },
686
- })
687
-
688
- for (const item of await ToolRegistry.tools(
689
- { modelID: input.model.api.id, providerID: input.model.providerID },
690
- input.agent,
691
- )) {
692
- const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters))
693
- tools[item.id] = tool({
694
- id: item.id as any,
695
- description: item.description,
696
- inputSchema: jsonSchema(schema as any),
697
- async execute(args, options) {
698
- const ctx = context(args, options)
699
- await Plugin.trigger(
700
- "tool.execute.before",
701
- {
702
- tool: item.id,
703
- sessionID: ctx.sessionID,
704
- callID: ctx.callID,
705
- },
706
- {
707
- args,
708
- },
709
- )
710
- const result = await item.execute(args, ctx)
711
- await Plugin.trigger(
712
- "tool.execute.after",
713
- {
714
- tool: item.id,
715
- sessionID: ctx.sessionID,
716
- callID: ctx.callID,
717
- },
718
- result,
719
- )
720
- return result
721
- },
722
- toModelOutput(result) {
723
- return {
724
- type: "text",
725
- value: result.output,
726
- }
727
- },
728
- })
729
- }
730
-
731
- for (const [key, item] of Object.entries(await MCP.tools())) {
732
- const execute = item.execute
733
- if (!execute) continue
734
-
735
- // Wrap execute to add plugin hooks and format output
736
- item.execute = async (args, opts) => {
737
- const ctx = context(args, opts)
738
-
739
- await Plugin.trigger(
740
- "tool.execute.before",
741
- {
742
- tool: key,
743
- sessionID: ctx.sessionID,
744
- callID: opts.toolCallId,
745
- },
746
- {
747
- args,
748
- },
749
- )
750
-
751
- await ctx.ask({
752
- permission: key,
753
- metadata: {},
754
- patterns: ["*"],
755
- always: ["*"],
756
- })
757
-
758
- const result = await execute(args, opts)
759
-
760
- await Plugin.trigger(
761
- "tool.execute.after",
762
- {
763
- tool: key,
764
- sessionID: ctx.sessionID,
765
- callID: opts.toolCallId,
766
- },
767
- result,
768
- )
769
-
770
- const textParts: string[] = []
771
- const attachments: MessageV2.FilePart[] = []
772
-
773
- for (const contentItem of result.content) {
774
- if (contentItem.type === "text") {
775
- textParts.push(contentItem.text)
776
- } else if (contentItem.type === "image") {
777
- attachments.push({
778
- id: Identifier.ascending("part"),
779
- sessionID: input.session.id,
780
- messageID: input.processor.message.id,
781
- type: "file",
782
- mime: contentItem.mimeType,
783
- url: `data:${contentItem.mimeType};base64,${contentItem.data}`,
784
- })
785
- } else if (contentItem.type === "resource") {
786
- const { resource } = contentItem
787
- if (resource.text) {
788
- textParts.push(resource.text)
789
- }
790
- if (resource.blob) {
791
- attachments.push({
792
- id: Identifier.ascending("part"),
793
- sessionID: input.session.id,
794
- messageID: input.processor.message.id,
795
- type: "file",
796
- mime: resource.mimeType ?? "application/octet-stream",
797
- url: `data:${resource.mimeType ?? "application/octet-stream"};base64,${resource.blob}`,
798
- filename: resource.uri,
799
- })
800
- }
801
- }
802
- }
803
-
804
- return {
805
- title: "",
806
- metadata: result.metadata ?? {},
807
- output: textParts.join("\n\n"),
808
- attachments,
809
- content: result.content, // directly return content to preserve ordering when outputting to model
810
- }
811
- }
812
- item.toModelOutput = (result) => {
813
- return {
814
- type: "text",
815
- value: result.output,
816
- }
817
- }
818
- tools[key] = item
819
- }
820
-
821
- return tools
822
- }
823
-
824
- async function createUserMessage(input: PromptInput) {
825
- const agent = await Agent.get(input.agent ?? (await Agent.defaultAgent()))
826
- const info: MessageV2.Info = {
827
- id: input.messageID ?? Identifier.ascending("message"),
828
- role: "user",
829
- sessionID: input.sessionID,
830
- time: {
831
- created: Date.now(),
832
- },
833
- tools: input.tools,
834
- agent: agent.name,
835
- model: input.model ?? agent.model ?? (await lastModel(input.sessionID)),
836
- system: input.system,
837
- variant: input.variant,
838
- }
839
-
840
- const parts = await Promise.all(
841
- input.parts.map(async (part): Promise<MessageV2.Part[]> => {
842
- if (part.type === "file") {
843
- // before checking the protocol we check if this is an mcp resource because it needs special handling
844
- if (part.source?.type === "resource") {
845
- const { clientName, uri } = part.source
846
- log.info("mcp resource", { clientName, uri, mime: part.mime })
847
-
848
- const pieces: MessageV2.Part[] = [
849
- {
850
- id: Identifier.ascending("part"),
851
- messageID: info.id,
852
- sessionID: input.sessionID,
853
- type: "text",
854
- synthetic: true,
855
- text: `Reading MCP resource: ${part.filename} (${uri})`,
856
- },
857
- ]
858
-
859
- try {
860
- const resourceContent = await MCP.readResource(clientName, uri)
861
- if (!resourceContent) {
862
- throw new Error(`Resource not found: ${clientName}/${uri}`)
863
- }
864
-
865
- // Handle different content types
866
- const contents = Array.isArray(resourceContent.contents)
867
- ? resourceContent.contents
868
- : [resourceContent.contents]
869
-
870
- for (const content of contents) {
871
- if ("text" in content && content.text) {
872
- pieces.push({
873
- id: Identifier.ascending("part"),
874
- messageID: info.id,
875
- sessionID: input.sessionID,
876
- type: "text",
877
- synthetic: true,
878
- text: content.text as string,
879
- })
880
- } else if ("blob" in content && content.blob) {
881
- // Handle binary content if needed
882
- const mimeType = "mimeType" in content ? content.mimeType : part.mime
883
- pieces.push({
884
- id: Identifier.ascending("part"),
885
- messageID: info.id,
886
- sessionID: input.sessionID,
887
- type: "text",
888
- synthetic: true,
889
- text: `[Binary content: ${mimeType}]`,
890
- })
891
- }
892
- }
893
-
894
- pieces.push({
895
- ...part,
896
- id: part.id ?? Identifier.ascending("part"),
897
- messageID: info.id,
898
- sessionID: input.sessionID,
899
- })
900
- } catch (error: unknown) {
901
- log.error("failed to read MCP resource", { error, clientName, uri })
902
- const message = error instanceof Error ? error.message : String(error)
903
- pieces.push({
904
- id: Identifier.ascending("part"),
905
- messageID: info.id,
906
- sessionID: input.sessionID,
907
- type: "text",
908
- synthetic: true,
909
- text: `Failed to read MCP resource ${part.filename}: ${message}`,
910
- })
911
- }
912
-
913
- return pieces
914
- }
915
- const url = new URL(part.url)
916
- switch (url.protocol) {
917
- case "data:":
918
- if (part.mime === "text/plain") {
919
- return [
920
- {
921
- id: Identifier.ascending("part"),
922
- messageID: info.id,
923
- sessionID: input.sessionID,
924
- type: "text",
925
- synthetic: true,
926
- text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: part.filename })}`,
927
- },
928
- {
929
- id: Identifier.ascending("part"),
930
- messageID: info.id,
931
- sessionID: input.sessionID,
932
- type: "text",
933
- synthetic: true,
934
- text: Buffer.from(part.url, "base64url").toString(),
935
- },
936
- {
937
- ...part,
938
- id: part.id ?? Identifier.ascending("part"),
939
- messageID: info.id,
940
- sessionID: input.sessionID,
941
- },
942
- ]
943
- }
944
- break
945
- case "file:":
946
- log.info("file", { mime: part.mime })
947
- // have to normalize, symbol search returns absolute paths
948
- // Decode the pathname since URL constructor doesn't automatically decode it
949
- const filepath = fileURLToPath(part.url)
950
- const stat = await Bun.file(filepath).stat()
951
-
952
- if (stat.isDirectory()) {
953
- part.mime = "application/x-directory"
954
- }
955
-
956
- if (part.mime === "text/plain") {
957
- let offset: number | undefined = undefined
958
- let limit: number | undefined = undefined
959
- const range = {
960
- start: url.searchParams.get("start"),
961
- end: url.searchParams.get("end"),
962
- }
963
- if (range.start != null) {
964
- const filePathURI = part.url.split("?")[0]
965
- let start = parseInt(range.start)
966
- let end = range.end ? parseInt(range.end) : undefined
967
- // some LSP servers (eg, gopls) don't give full range in
968
- // workspace/symbol searches, so we'll try to find the
969
- // symbol in the document to get the full range
970
- if (start === end) {
971
- const symbols = await LSP.documentSymbol(filePathURI)
972
- for (const symbol of symbols) {
973
- let range: LSP.Range | undefined
974
- if ("range" in symbol) {
975
- range = symbol.range
976
- } else if ("location" in symbol) {
977
- range = symbol.location.range
978
- }
979
- if (range?.start?.line && range?.start?.line === start) {
980
- start = range.start.line
981
- end = range?.end?.line ?? start
982
- break
983
- }
984
- }
985
- }
986
- offset = Math.max(start - 1, 0)
987
- if (end) {
988
- limit = end - offset
989
- }
990
- }
991
- const args = { filePath: filepath, offset, limit }
992
-
993
- const pieces: MessageV2.Part[] = [
994
- {
995
- id: Identifier.ascending("part"),
996
- messageID: info.id,
997
- sessionID: input.sessionID,
998
- type: "text",
999
- synthetic: true,
1000
- text: `Called the Read tool with the following input: ${JSON.stringify(args)}`,
1001
- },
1002
- ]
1003
-
1004
- await ReadTool.init()
1005
- .then(async (t) => {
1006
- const model = await Provider.getModel(info.model.providerID, info.model.modelID)
1007
- const readCtx: Tool.Context = {
1008
- sessionID: input.sessionID,
1009
- abort: new AbortController().signal,
1010
- agent: input.agent!,
1011
- messageID: info.id,
1012
- extra: { bypassCwdCheck: true, model },
1013
- metadata: async () => {},
1014
- ask: async () => {},
1015
- }
1016
- const result = await t.execute(args, readCtx)
1017
- pieces.push({
1018
- id: Identifier.ascending("part"),
1019
- messageID: info.id,
1020
- sessionID: input.sessionID,
1021
- type: "text",
1022
- synthetic: true,
1023
- text: result.output,
1024
- })
1025
- if (result.attachments?.length) {
1026
- pieces.push(
1027
- ...result.attachments.map((attachment) => ({
1028
- ...attachment,
1029
- synthetic: true,
1030
- filename: attachment.filename ?? part.filename,
1031
- messageID: info.id,
1032
- sessionID: input.sessionID,
1033
- })),
1034
- )
1035
- } else {
1036
- pieces.push({
1037
- ...part,
1038
- id: part.id ?? Identifier.ascending("part"),
1039
- messageID: info.id,
1040
- sessionID: input.sessionID,
1041
- })
1042
- }
1043
- })
1044
- .catch((error) => {
1045
- log.error("failed to read file", { error })
1046
- const message = error instanceof Error ? error.message : error.toString()
1047
- Bus.publish(Session.Event.Error, {
1048
- sessionID: input.sessionID,
1049
- error: new NamedError.Unknown({
1050
- message,
1051
- }).toObject(),
1052
- })
1053
- pieces.push({
1054
- id: Identifier.ascending("part"),
1055
- messageID: info.id,
1056
- sessionID: input.sessionID,
1057
- type: "text",
1058
- synthetic: true,
1059
- text: `Read tool failed to read ${filepath} with the following error: ${message}`,
1060
- })
1061
- })
1062
-
1063
- return pieces
1064
- }
1065
-
1066
- if (part.mime === "application/x-directory") {
1067
- const args = { path: filepath }
1068
- const listCtx: Tool.Context = {
1069
- sessionID: input.sessionID,
1070
- abort: new AbortController().signal,
1071
- agent: input.agent!,
1072
- messageID: info.id,
1073
- extra: { bypassCwdCheck: true },
1074
- metadata: async () => {},
1075
- ask: async () => {},
1076
- }
1077
- const result = await ListTool.init().then((t) => t.execute(args, listCtx))
1078
- return [
1079
- {
1080
- id: Identifier.ascending("part"),
1081
- messageID: info.id,
1082
- sessionID: input.sessionID,
1083
- type: "text",
1084
- synthetic: true,
1085
- text: `Called the list tool with the following input: ${JSON.stringify(args)}`,
1086
- },
1087
- {
1088
- id: Identifier.ascending("part"),
1089
- messageID: info.id,
1090
- sessionID: input.sessionID,
1091
- type: "text",
1092
- synthetic: true,
1093
- text: result.output,
1094
- },
1095
- {
1096
- ...part,
1097
- id: part.id ?? Identifier.ascending("part"),
1098
- messageID: info.id,
1099
- sessionID: input.sessionID,
1100
- },
1101
- ]
1102
- }
1103
-
1104
- const file = Bun.file(filepath)
1105
- FileTime.read(input.sessionID, filepath)
1106
- return [
1107
- {
1108
- id: Identifier.ascending("part"),
1109
- messageID: info.id,
1110
- sessionID: input.sessionID,
1111
- type: "text",
1112
- text: `Called the Read tool with the following input: {\"filePath\":\"${filepath}\"}`,
1113
- synthetic: true,
1114
- },
1115
- {
1116
- id: part.id ?? Identifier.ascending("part"),
1117
- messageID: info.id,
1118
- sessionID: input.sessionID,
1119
- type: "file",
1120
- url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64"),
1121
- mime: part.mime,
1122
- filename: part.filename!,
1123
- source: part.source,
1124
- },
1125
- ]
1126
- }
1127
- }
1128
-
1129
- if (part.type === "agent") {
1130
- // Check if this agent would be denied by task permission
1131
- const perm = PermissionNext.evaluate("task", part.name, agent.permission)
1132
- const hint = perm.action === "deny" ? " . Invoked by user; guaranteed to exist." : ""
1133
- return [
1134
- {
1135
- id: Identifier.ascending("part"),
1136
- ...part,
1137
- messageID: info.id,
1138
- sessionID: input.sessionID,
1139
- },
1140
- {
1141
- id: Identifier.ascending("part"),
1142
- messageID: info.id,
1143
- sessionID: input.sessionID,
1144
- type: "text",
1145
- synthetic: true,
1146
- // An extra space is added here. Otherwise the 'Use' gets appended
1147
- // to user's last word; making a combined word
1148
- text:
1149
- " Use the above message and context to generate a prompt and call the task tool with subagent: " +
1150
- part.name +
1151
- hint,
1152
- },
1153
- ]
1154
- }
1155
-
1156
- return [
1157
- {
1158
- id: Identifier.ascending("part"),
1159
- ...part,
1160
- messageID: info.id,
1161
- sessionID: input.sessionID,
1162
- },
1163
- ]
1164
- }),
1165
- ).then((x) => x.flat())
1166
-
1167
- await Plugin.trigger(
1168
- "chat.message",
1169
- {
1170
- sessionID: input.sessionID,
1171
- agent: input.agent,
1172
- model: input.model,
1173
- messageID: input.messageID,
1174
- variant: input.variant,
1175
- },
1176
- {
1177
- message: info,
1178
- parts,
1179
- },
1180
- )
1181
-
1182
- await Session.updateMessage(info)
1183
- for (const part of parts) {
1184
- await Session.updatePart(part)
1185
- }
1186
-
1187
- return {
1188
- info,
1189
- parts,
1190
- }
1191
- }
1192
-
1193
- async function insertReminders(input: { messages: MessageV2.WithParts[]; agent: Agent.Info; session: Session.Info }) {
1194
- const userMessage = input.messages.findLast((msg) => msg.info.role === "user")
1195
- if (!userMessage) return input.messages
1196
-
1197
- // Original logic when experimental plan mode is disabled
1198
- if (!Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE) {
1199
- if (input.agent.name === "plan") {
1200
- userMessage.parts.push({
1201
- id: Identifier.ascending("part"),
1202
- messageID: userMessage.info.id,
1203
- sessionID: userMessage.info.sessionID,
1204
- type: "text",
1205
- text: PROMPT_PLAN,
1206
- synthetic: true,
1207
- })
1208
- }
1209
- const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan")
1210
- if (wasPlan && input.agent.name === "build") {
1211
- userMessage.parts.push({
1212
- id: Identifier.ascending("part"),
1213
- messageID: userMessage.info.id,
1214
- sessionID: userMessage.info.sessionID,
1215
- type: "text",
1216
- text: BUILD_SWITCH,
1217
- synthetic: true,
1218
- })
1219
- }
1220
- return input.messages
1221
- }
1222
-
1223
- // New plan mode logic when flag is enabled
1224
- const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant")
1225
-
1226
- // Switching from plan mode to build mode
1227
- if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") {
1228
- const plan = Session.plan(input.session)
1229
- const exists = await Bun.file(plan).exists()
1230
- if (exists) {
1231
- const part = await Session.updatePart({
1232
- id: Identifier.ascending("part"),
1233
- messageID: userMessage.info.id,
1234
- sessionID: userMessage.info.sessionID,
1235
- type: "text",
1236
- text:
1237
- BUILD_SWITCH + "\n\n" + `A plan file exists at ${plan}. You should execute on the plan defined within it`,
1238
- synthetic: true,
1239
- })
1240
- userMessage.parts.push(part)
1241
- }
1242
- return input.messages
1243
- }
1244
-
1245
- // Entering plan mode
1246
- if (input.agent.name === "plan" && assistantMessage?.info.agent !== "plan") {
1247
- const plan = Session.plan(input.session)
1248
- const exists = await Bun.file(plan).exists()
1249
- if (!exists) await fs.mkdir(path.dirname(plan), { recursive: true })
1250
- const part = await Session.updatePart({
1251
- id: Identifier.ascending("part"),
1252
- messageID: userMessage.info.id,
1253
- sessionID: userMessage.info.sessionID,
1254
- type: "text",
1255
- text: `<system-reminder>
1256
- Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received.
1257
-
1258
- ## Plan File Info:
1259
- ${exists ? `A plan file already exists at ${plan}. You can read it and make incremental edits using the edit tool.` : `No plan file exists yet. You should create your plan at ${plan} using the write tool.`}
1260
- You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions.
1261
-
1262
- ## Plan Workflow
1263
-
1264
- ### Phase 1: Initial Understanding
1265
- Goal: Gain a comprehensive understanding of the user's request by reading through code and asking them questions. Critical: In this phase you should only use the explore subagent type.
1266
-
1267
- 1. Focus on understanding the user's request and the code associated with their request
1268
-
1269
- 2. **Launch up to 3 explore agents IN PARALLEL** (single message, multiple tool calls) to efficiently explore the codebase.
1270
- - Use 1 agent when the task is isolated to known files, the user provided specific file paths, or you're making a small targeted change.
1271
- - Use multiple agents when: the scope is uncertain, multiple areas of the codebase are involved, or you need to understand existing patterns before planning.
1272
- - Quality over quantity - 3 agents maximum, but you should try to use the minimum number of agents necessary (usually just 1)
1273
- - If using multiple agents: Provide each agent with a specific search focus or area to explore. Example: One agent searches for existing implementations, another explores related components, a third investigates testing patterns
1274
-
1275
- 3. After exploring the code, use the question tool to clarify ambiguities in the user request up front.
1276
-
1277
- ### Phase 2: Design
1278
- Goal: Design an implementation approach.
1279
-
1280
- Launch general agent(s) to design the implementation based on the user's intent and your exploration results from Phase 1.
1281
-
1282
- You can launch up to 1 agent(s) in parallel.
1283
-
1284
- **Guidelines:**
1285
- - **Default**: Launch at least 1 Plan agent for most tasks - it helps validate your understanding and consider alternatives
1286
- - **Skip agents**: Only for truly trivial tasks (typo fixes, single-line changes, simple renames)
1287
-
1288
- Examples of when to use multiple agents:
1289
- - The task touches multiple parts of the codebase
1290
- - It's a large refactor or architectural change
1291
- - There are many edge cases to consider
1292
- - You'd benefit from exploring different approaches
1293
-
1294
- Example perspectives by task type:
1295
- - New feature: simplicity vs performance vs maintainability
1296
- - Bug fix: root cause vs workaround vs prevention
1297
- - Refactoring: minimal change vs clean architecture
1298
-
1299
- In the agent prompt:
1300
- - Provide comprehensive background context from Phase 1 exploration including filenames and code path traces
1301
- - Describe requirements and constraints
1302
- - Request a detailed implementation plan
1303
-
1304
- ### Phase 3: Review
1305
- Goal: Review the plan(s) from Phase 2 and ensure alignment with the user's intentions.
1306
- 1. Read the critical files identified by agents to deepen your understanding
1307
- 2. Ensure that the plans align with the user's original request
1308
- 3. Use question tool to clarify any remaining questions with the user
1309
-
1310
- ### Phase 4: Final Plan
1311
- Goal: Write your final plan to the plan file (the only file you can edit).
1312
- - Include only your recommended approach, not all alternatives
1313
- - Ensure that the plan file is concise enough to scan quickly, but detailed enough to execute effectively
1314
- - Include the paths of critical files to be modified
1315
- - Include a verification section describing how to test the changes end-to-end (run the code, use MCP tools, run tests)
1316
-
1317
- ### Phase 5: Call plan_exit tool
1318
- At the very end of your turn, once you have asked the user questions and are happy with your final plan file - you should always call plan_exit to indicate to the user that you are done planning.
1319
- This is critical - your turn should only end with either asking the user a question or calling plan_exit. Do not stop unless it's for these 2 reasons.
1320
-
1321
- **Important:** Use question tool to clarify requirements/approach, use plan_exit to request plan approval. Do NOT use question tool to ask "Is this plan okay?" - that's what plan_exit does.
1322
-
1323
- NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins.
1324
- </system-reminder>`,
1325
- synthetic: true,
1326
- })
1327
- userMessage.parts.push(part)
1328
- return input.messages
1329
- }
1330
- return input.messages
1331
- }
1332
-
1333
- export const ShellInput = z.object({
1334
- sessionID: Identifier.schema("session"),
1335
- agent: z.string(),
1336
- model: z
1337
- .object({
1338
- providerID: z.string(),
1339
- modelID: z.string(),
1340
- })
1341
- .optional(),
1342
- command: z.string(),
1343
- })
1344
- export type ShellInput = z.infer<typeof ShellInput>
1345
- export async function shell(input: ShellInput) {
1346
- const abort = start(input.sessionID)
1347
- if (!abort) {
1348
- throw new Session.BusyError(input.sessionID)
1349
- }
1350
- using _ = defer(() => cancel(input.sessionID))
1351
-
1352
- const session = await Session.get(input.sessionID)
1353
- if (session.revert) {
1354
- SessionRevert.cleanup(session)
1355
- }
1356
- const agent = await Agent.get(input.agent)
1357
- const model = input.model ?? agent.model ?? (await lastModel(input.sessionID))
1358
- const userMsg: MessageV2.User = {
1359
- id: Identifier.ascending("message"),
1360
- sessionID: input.sessionID,
1361
- time: {
1362
- created: Date.now(),
1363
- },
1364
- role: "user",
1365
- agent: input.agent,
1366
- model: {
1367
- providerID: model.providerID,
1368
- modelID: model.modelID,
1369
- },
1370
- }
1371
- await Session.updateMessage(userMsg)
1372
- const userPart: MessageV2.Part = {
1373
- type: "text",
1374
- id: Identifier.ascending("part"),
1375
- messageID: userMsg.id,
1376
- sessionID: input.sessionID,
1377
- text: "The following tool was executed by the user",
1378
- synthetic: true,
1379
- }
1380
- await Session.updatePart(userPart)
1381
-
1382
- const msg: MessageV2.Assistant = {
1383
- id: Identifier.ascending("message"),
1384
- sessionID: input.sessionID,
1385
- parentID: userMsg.id,
1386
- mode: input.agent,
1387
- agent: input.agent,
1388
- cost: 0,
1389
- path: {
1390
- cwd: Instance.directory,
1391
- root: Instance.worktree,
1392
- },
1393
- time: {
1394
- created: Date.now(),
1395
- },
1396
- role: "assistant",
1397
- tokens: {
1398
- input: 0,
1399
- output: 0,
1400
- reasoning: 0,
1401
- cache: { read: 0, write: 0 },
1402
- },
1403
- modelID: model.modelID,
1404
- providerID: model.providerID,
1405
- }
1406
- await Session.updateMessage(msg)
1407
- const part: MessageV2.Part = {
1408
- type: "tool",
1409
- id: Identifier.ascending("part"),
1410
- messageID: msg.id,
1411
- sessionID: input.sessionID,
1412
- tool: "bash",
1413
- callID: ulid(),
1414
- state: {
1415
- status: "running",
1416
- time: {
1417
- start: Date.now(),
1418
- },
1419
- input: {
1420
- command: input.command,
1421
- },
1422
- },
1423
- }
1424
- await Session.updatePart(part)
1425
- const shell = Shell.preferred()
1426
- const shellName = (
1427
- process.platform === "win32" ? path.win32.basename(shell, ".exe") : path.basename(shell)
1428
- ).toLowerCase()
1429
-
1430
- const invocations: Record<string, { args: string[] }> = {
1431
- nu: {
1432
- args: ["-c", input.command],
1433
- },
1434
- fish: {
1435
- args: ["-c", input.command],
1436
- },
1437
- zsh: {
1438
- args: [
1439
- "-c",
1440
- "-l",
1441
- `
1442
- [[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
1443
- [[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
1444
- eval ${JSON.stringify(input.command)}
1445
- `,
1446
- ],
1447
- },
1448
- bash: {
1449
- args: [
1450
- "-c",
1451
- "-l",
1452
- `
1453
- shopt -s expand_aliases
1454
- [[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
1455
- eval ${JSON.stringify(input.command)}
1456
- `,
1457
- ],
1458
- },
1459
- // Windows cmd
1460
- cmd: {
1461
- args: ["/c", input.command],
1462
- },
1463
- // Windows PowerShell
1464
- powershell: {
1465
- args: ["-NoProfile", "-Command", input.command],
1466
- },
1467
- pwsh: {
1468
- args: ["-NoProfile", "-Command", input.command],
1469
- },
1470
- // Fallback: any shell that doesn't match those above
1471
- // - No -l, for max compatibility
1472
- "": {
1473
- args: ["-c", `${input.command}`],
1474
- },
1475
- }
1476
-
1477
- const matchingInvocation = invocations[shellName] ?? invocations[""]
1478
- const args = matchingInvocation?.args
1479
-
1480
- const proc = spawn(shell, args, {
1481
- cwd: Instance.directory,
1482
- detached: process.platform !== "win32",
1483
- stdio: ["ignore", "pipe", "pipe"],
1484
- env: {
1485
- ...process.env,
1486
- TERM: "dumb",
1487
- },
1488
- })
1489
-
1490
- let output = ""
1491
-
1492
- proc.stdout?.on("data", (chunk) => {
1493
- output += chunk.toString()
1494
- if (part.state.status === "running") {
1495
- part.state.metadata = {
1496
- output: output,
1497
- description: "",
1498
- }
1499
- Session.updatePart(part)
1500
- }
1501
- })
1502
-
1503
- proc.stderr?.on("data", (chunk) => {
1504
- output += chunk.toString()
1505
- if (part.state.status === "running") {
1506
- part.state.metadata = {
1507
- output: output,
1508
- description: "",
1509
- }
1510
- Session.updatePart(part)
1511
- }
1512
- })
1513
-
1514
- let aborted = false
1515
- let exited = false
1516
-
1517
- const kill = () => Shell.killTree(proc, { exited: () => exited })
1518
-
1519
- if (abort.aborted) {
1520
- aborted = true
1521
- await kill()
1522
- }
1523
-
1524
- const abortHandler = () => {
1525
- aborted = true
1526
- void kill()
1527
- }
1528
-
1529
- abort.addEventListener("abort", abortHandler, { once: true })
1530
-
1531
- await new Promise<void>((resolve) => {
1532
- proc.on("close", () => {
1533
- exited = true
1534
- abort.removeEventListener("abort", abortHandler)
1535
- resolve()
1536
- })
1537
- })
1538
-
1539
- if (aborted) {
1540
- output += "\n\n" + ["<metadata>", "User aborted the command", "</metadata>"].join("\n")
1541
- }
1542
- msg.time.completed = Date.now()
1543
- await Session.updateMessage(msg)
1544
- if (part.state.status === "running") {
1545
- part.state = {
1546
- status: "completed",
1547
- time: {
1548
- ...part.state.time,
1549
- end: Date.now(),
1550
- },
1551
- input: part.state.input,
1552
- title: "",
1553
- metadata: {
1554
- output,
1555
- description: "",
1556
- },
1557
- output,
1558
- }
1559
- await Session.updatePart(part)
1560
- }
1561
- return { info: msg, parts: [part] }
1562
- }
1563
-
1564
- export const CommandInput = z.object({
1565
- messageID: Identifier.schema("message").optional(),
1566
- sessionID: Identifier.schema("session"),
1567
- agent: z.string().optional(),
1568
- model: z.string().optional(),
1569
- arguments: z.string(),
1570
- command: z.string(),
1571
- variant: z.string().optional(),
1572
- parts: z
1573
- .array(
1574
- z.discriminatedUnion("type", [
1575
- MessageV2.FilePart.omit({
1576
- messageID: true,
1577
- sessionID: true,
1578
- }).partial({
1579
- id: true,
1580
- }),
1581
- ]),
1582
- )
1583
- .optional(),
1584
- })
1585
- export type CommandInput = z.infer<typeof CommandInput>
1586
- const bashRegex = /!`([^`]+)`/g
1587
- // Match [Image N] as single token, quoted strings, or non-space sequences
1588
- const argsRegex = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi
1589
- const placeholderRegex = /\$(\d+)/g
1590
- const quoteTrimRegex = /^["']|["']$/g
1591
- /**
1592
- * Regular expression to match @ file references in text
1593
- * Matches @ followed by file paths, excluding commas, periods at end of sentences, and backticks
1594
- * Does not match when preceded by word characters or backticks (to avoid email addresses and quoted references)
1595
- */
1596
-
1597
- export async function command(input: CommandInput) {
1598
- log.info("command", input)
1599
- const command = await Command.get(input.command)
1600
- const agentName = command.agent ?? input.agent ?? (await Agent.defaultAgent())
1601
-
1602
- const raw = input.arguments.match(argsRegex) ?? []
1603
- const args = raw.map((arg) => arg.replace(quoteTrimRegex, ""))
1604
-
1605
- const templateCommand = await command.template
1606
-
1607
- const placeholders = templateCommand.match(placeholderRegex) ?? []
1608
- let last = 0
1609
- for (const item of placeholders) {
1610
- const value = Number(item.slice(1))
1611
- if (value > last) last = value
1612
- }
1613
-
1614
- // Let the final placeholder swallow any extra arguments so prompts read naturally
1615
- const withArgs = templateCommand.replaceAll(placeholderRegex, (_, index) => {
1616
- const position = Number(index)
1617
- const argIndex = position - 1
1618
- if (argIndex >= args.length) return ""
1619
- if (position === last) return args.slice(argIndex).join(" ")
1620
- return args[argIndex]
1621
- })
1622
- let template = withArgs.replaceAll("$ARGUMENTS", input.arguments)
1623
-
1624
- const shell = ConfigMarkdown.shell(template)
1625
- if (shell.length > 0) {
1626
- const results = await Promise.all(
1627
- shell.map(async ([, cmd]) => {
1628
- try {
1629
- return await $`${{ raw: cmd }}`.quiet().nothrow().text()
1630
- } catch (error) {
1631
- return `Error executing command: ${error instanceof Error ? error.message : String(error)}`
1632
- }
1633
- }),
1634
- )
1635
- let index = 0
1636
- template = template.replace(bashRegex, () => results[index++])
1637
- }
1638
- template = template.trim()
1639
-
1640
- const taskModel = await (async () => {
1641
- if (command.model) {
1642
- return Provider.parseModel(command.model)
1643
- }
1644
- if (command.agent) {
1645
- const cmdAgent = await Agent.get(command.agent)
1646
- if (cmdAgent?.model) {
1647
- return cmdAgent.model
1648
- }
1649
- }
1650
- if (input.model) return Provider.parseModel(input.model)
1651
- return await lastModel(input.sessionID)
1652
- })()
1653
-
1654
- try {
1655
- await Provider.getModel(taskModel.providerID, taskModel.modelID)
1656
- } catch (e) {
1657
- if (Provider.ModelNotFoundError.isInstance(e)) {
1658
- const { providerID, modelID, suggestions } = e.data
1659
- const hint = suggestions?.length ? ` Did you mean: ${suggestions.join(", ")}?` : ""
1660
- Bus.publish(Session.Event.Error, {
1661
- sessionID: input.sessionID,
1662
- error: new NamedError.Unknown({ message: `Model not found: ${providerID}/${modelID}.${hint}` }).toObject(),
1663
- })
1664
- }
1665
- throw e
1666
- }
1667
- const agent = await Agent.get(agentName)
1668
- if (!agent) {
1669
- const available = await Agent.list().then((agents) => agents.filter((a) => !a.hidden).map((a) => a.name))
1670
- const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
1671
- const error = new NamedError.Unknown({ message: `Agent not found: "${agentName}".${hint}` })
1672
- Bus.publish(Session.Event.Error, {
1673
- sessionID: input.sessionID,
1674
- error: error.toObject(),
1675
- })
1676
- throw error
1677
- }
1678
-
1679
- const templateParts = await resolvePromptParts(template)
1680
- const isSubtask = (agent.mode === "subagent" && command.subtask !== false) || command.subtask === true
1681
- const parts = isSubtask
1682
- ? [
1683
- {
1684
- type: "subtask" as const,
1685
- agent: agent.name,
1686
- description: command.description ?? "",
1687
- command: input.command,
1688
- model: {
1689
- providerID: taskModel.providerID,
1690
- modelID: taskModel.modelID,
1691
- },
1692
- // TODO: how can we make task tool accept a more complex input?
1693
- prompt: templateParts.find((y) => y.type === "text")?.text ?? "",
1694
- },
1695
- ]
1696
- : [...templateParts, ...(input.parts ?? [])]
1697
-
1698
- const userAgent = isSubtask ? (input.agent ?? (await Agent.defaultAgent())) : agentName
1699
- const userModel = isSubtask
1700
- ? input.model
1701
- ? Provider.parseModel(input.model)
1702
- : await lastModel(input.sessionID)
1703
- : taskModel
1704
-
1705
- await Plugin.trigger(
1706
- "command.execute.before",
1707
- {
1708
- command: input.command,
1709
- sessionID: input.sessionID,
1710
- arguments: input.arguments,
1711
- },
1712
- { parts },
1713
- )
1714
-
1715
- const result = (await prompt({
1716
- sessionID: input.sessionID,
1717
- messageID: input.messageID,
1718
- model: userModel,
1719
- agent: userAgent,
1720
- parts,
1721
- variant: input.variant,
1722
- })) as MessageV2.WithParts
1723
-
1724
- Bus.publish(Command.Event.Executed, {
1725
- name: input.command,
1726
- sessionID: input.sessionID,
1727
- arguments: input.arguments,
1728
- messageID: result.info.id,
1729
- })
1730
-
1731
- return result
1732
- }
1733
-
1734
- async function ensureTitle(input: {
1735
- session: Session.Info
1736
- history: MessageV2.WithParts[]
1737
- providerID: string
1738
- modelID: string
1739
- }) {
1740
- if (input.session.parentID) return
1741
- if (!Session.isDefaultTitle(input.session.title)) return
1742
-
1743
- // Find first non-synthetic user message
1744
- const firstRealUserIdx = input.history.findIndex(
1745
- (m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic),
1746
- )
1747
- if (firstRealUserIdx === -1) return
1748
-
1749
- const isFirst =
1750
- input.history.filter((m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic))
1751
- .length === 1
1752
- if (!isFirst) return
1753
-
1754
- // Gather all messages up to and including the first real user message for context
1755
- // This includes any shell/subtask executions that preceded the user's first prompt
1756
- const contextMessages = input.history.slice(0, firstRealUserIdx + 1)
1757
- const firstRealUser = contextMessages[firstRealUserIdx]
1758
-
1759
- // For subtask-only messages (from command invocations), extract the prompt directly
1760
- // since toModelMessage converts subtask parts to generic "The following tool was executed by the user"
1761
- const subtaskParts = firstRealUser.parts.filter((p) => p.type === "subtask") as MessageV2.SubtaskPart[]
1762
- const hasOnlySubtaskParts = subtaskParts.length > 0 && firstRealUser.parts.every((p) => p.type === "subtask")
1763
-
1764
- const agent = await Agent.get("title")
1765
- if (!agent) return
1766
- const result = await LLM.stream({
1767
- agent,
1768
- user: firstRealUser.info as MessageV2.User,
1769
- system: [],
1770
- small: true,
1771
- tools: {},
1772
- model: await iife(async () => {
1773
- if (agent.model) return await Provider.getModel(agent.model.providerID, agent.model.modelID)
1774
- return (
1775
- (await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
1776
- )
1777
- }),
1778
- abort: new AbortController().signal,
1779
- sessionID: input.session.id,
1780
- retries: 2,
1781
- messages: [
1782
- {
1783
- role: "user",
1784
- content: "Generate a title for this conversation:\n",
1785
- },
1786
- ...(hasOnlySubtaskParts
1787
- ? [{ role: "user" as const, content: subtaskParts.map((p) => p.prompt).join("\n") }]
1788
- : MessageV2.toModelMessage(contextMessages)),
1789
- ],
1790
- })
1791
- const text = await result.text.catch((err) => log.error("failed to generate title", { error: err }))
1792
- if (text)
1793
- return Session.update(input.session.id, (draft) => {
1794
- const cleaned = text
1795
- .replace(/<think>[\s\S]*?<\/think>\s*/g, "")
1796
- .split("\n")
1797
- .map((line) => line.trim())
1798
- .find((line) => line.length > 0)
1799
- if (!cleaned) return
1800
-
1801
- const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
1802
- draft.title = title
1803
- })
1804
- }
1805
- }