bincode-cli 1.0.1

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