chad-code 1.3.1 → 1.3.2

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