nikcli 0.0.6

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