@nghyane/arcane 0.1.12 → 0.1.14

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 (333) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/package.json +21 -70
  3. package/scripts/format-prompts.ts +1 -3
  4. package/src/cli/args.ts +2 -7
  5. package/src/cli/config-cli.ts +1 -1
  6. package/src/cli/plugin-cli.ts +1 -1
  7. package/src/cli/setup-cli.ts +1 -1
  8. package/src/cli/update-cli.ts +1 -1
  9. package/src/cli/web-search-cli.ts +1 -1
  10. package/src/cli.ts +0 -1
  11. package/src/commands/config.ts +1 -1
  12. package/src/commands/grep.ts +1 -1
  13. package/src/commands/jupyter.ts +1 -1
  14. package/src/commands/plugin.ts +1 -1
  15. package/src/commands/setup.ts +1 -1
  16. package/src/commands/shell.ts +1 -1
  17. package/src/commands/ssh.ts +1 -1
  18. package/src/commands/stats.ts +1 -1
  19. package/src/commands/update.ts +1 -1
  20. package/src/config/model-registry.ts +3 -4
  21. package/src/config/model-resolver.ts +36 -9
  22. package/src/config/prompt-templates.ts +1 -9
  23. package/src/config/settings-schema.ts +32 -88
  24. package/src/config/settings.ts +3 -4
  25. package/src/debug/index.ts +1 -1
  26. package/src/debug/log-formatting.ts +1 -1
  27. package/src/debug/log-viewer.ts +2 -2
  28. package/src/discovery/helpers.ts +13 -3
  29. package/src/exa/company.ts +2 -7
  30. package/src/exa/index.ts +1 -35
  31. package/src/exa/linkedin.ts +2 -7
  32. package/src/exa/mcp-client.ts +21 -11
  33. package/src/exa/render.ts +30 -190
  34. package/src/exa/researcher.ts +2 -12
  35. package/src/exa/search.ts +5 -25
  36. package/src/exa/types.ts +3 -3
  37. package/src/exec/bash-executor.ts +2 -1
  38. package/src/exec/non-interactive-env.ts +43 -0
  39. package/src/export/html/index.ts +1 -1
  40. package/src/extensibility/custom-tools/loader.ts +1 -1
  41. package/src/extensibility/custom-tools/types.ts +5 -1
  42. package/src/extensibility/custom-tools/wrapper.ts +1 -1
  43. package/src/extensibility/extensions/runner.ts +1 -1
  44. package/src/extensibility/extensions/types.ts +1 -1
  45. package/src/extensibility/extensions/wrapper.ts +7 -15
  46. package/src/extensibility/hooks/runner.ts +1 -1
  47. package/src/extensibility/hooks/types.ts +1 -1
  48. package/src/extensibility/plugins/doctor.ts +1 -1
  49. package/src/index.ts +13 -13
  50. package/src/lsp/index.ts +77 -24
  51. package/src/lsp/render.ts +34 -583
  52. package/src/lsp/types.ts +3 -3
  53. package/src/lsp/utils.ts +1 -1
  54. package/src/main.ts +1 -1
  55. package/src/mcp/tool-bridge.ts +1 -24
  56. package/src/modes/components/assistant-message.ts +7 -7
  57. package/src/modes/components/bash-execution.ts +48 -113
  58. package/src/modes/components/bordered-loader.ts +1 -1
  59. package/src/modes/components/branch-summary-message.ts +13 -10
  60. package/src/modes/components/compaction-summary-message.ts +14 -13
  61. package/src/modes/components/context-group.ts +106 -0
  62. package/src/modes/components/custom-message.ts +4 -5
  63. package/src/modes/components/diff.ts +2 -2
  64. package/src/modes/components/dynamic-border.ts +1 -1
  65. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  66. package/src/modes/components/extensions/extension-list.ts +1 -1
  67. package/src/modes/components/extensions/inspector-panel.ts +8 -3
  68. package/src/modes/components/footer.ts +2 -2
  69. package/src/modes/components/history-search.ts +1 -1
  70. package/src/modes/components/hook-editor.ts +1 -1
  71. package/src/modes/components/hook-input.ts +1 -1
  72. package/src/modes/components/hook-message.ts +4 -5
  73. package/src/modes/components/hook-selector.ts +1 -1
  74. package/src/modes/components/index.ts +0 -2
  75. package/src/modes/components/keybinding-hints.ts +1 -1
  76. package/src/modes/components/login-dialog.ts +1 -1
  77. package/src/modes/components/mcp-add-wizard.ts +1 -1
  78. package/src/modes/components/model-selector.ts +1 -1
  79. package/src/modes/components/oauth-selector.ts +1 -1
  80. package/src/modes/components/plugin-settings.ts +1 -1
  81. package/src/modes/components/python-execution.ts +49 -92
  82. package/src/modes/components/queue-mode-selector.ts +1 -1
  83. package/src/modes/components/session-selector.ts +1 -1
  84. package/src/modes/components/settings-defs.ts +5 -10
  85. package/src/modes/components/settings-selector.ts +1 -1
  86. package/src/modes/components/show-images-selector.ts +1 -1
  87. package/src/modes/components/skill-message.ts +4 -4
  88. package/src/modes/components/status-line/segments.ts +2 -2
  89. package/src/modes/components/status-line/separators.ts +1 -1
  90. package/src/modes/components/status-line-segment-editor.ts +1 -1
  91. package/src/modes/components/status-line.ts +1 -1
  92. package/src/modes/components/theme-selector.ts +1 -1
  93. package/src/modes/components/thinking-selector.ts +1 -1
  94. package/src/modes/components/todo-display.ts +2 -4
  95. package/src/modes/components/todo-reminder.ts +4 -4
  96. package/src/modes/components/tool-execution.ts +118 -440
  97. package/src/modes/components/tool-image-display.ts +107 -0
  98. package/src/modes/components/tree-selector.ts +2 -2
  99. package/src/modes/components/ttsr-notification.ts +4 -17
  100. package/src/modes/components/user-message-selector.ts +1 -1
  101. package/src/modes/components/user-message.ts +9 -10
  102. package/src/modes/components/welcome.ts +1 -1
  103. package/src/modes/controllers/command-controller.ts +1 -1
  104. package/src/modes/controllers/event-controller.ts +58 -187
  105. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  106. package/src/modes/controllers/input-controller.ts +3 -1
  107. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  108. package/src/modes/controllers/selector-controller.ts +3 -26
  109. package/src/modes/controllers/ssh-command-controller.ts +1 -1
  110. package/src/modes/interactive-mode.ts +3 -7
  111. package/src/modes/print-mode.ts +5 -5
  112. package/src/modes/rpc/rpc-mode.ts +1 -1
  113. package/src/modes/types.ts +1 -2
  114. package/src/modes/utils/ui-helpers.ts +34 -32
  115. package/src/patch/edit-tool.ts +742 -0
  116. package/src/patch/index.ts +32 -898
  117. package/src/patch/schemas.ts +208 -0
  118. package/src/patch/shared.ts +83 -151
  119. package/src/prompts/agents/explore.md +22 -37
  120. package/src/prompts/agents/frontmatter.md +1 -1
  121. package/src/prompts/agents/init.md +2 -2
  122. package/src/prompts/agents/librarian.md +30 -21
  123. package/src/prompts/agents/oracle.md +9 -2
  124. package/src/prompts/agents/reviewer.md +15 -49
  125. package/src/prompts/agents/task.md +17 -9
  126. package/src/prompts/compaction/branch-summary-context.md +1 -1
  127. package/src/prompts/compaction/branch-summary-preamble.md +1 -1
  128. package/src/prompts/compaction/branch-summary.md +4 -1
  129. package/src/prompts/compaction/compaction-short-summary.md +1 -1
  130. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  131. package/src/prompts/compaction/compaction-summary.md +4 -1
  132. package/src/prompts/compaction/compaction-turn-prefix.md +1 -1
  133. package/src/prompts/compaction/compaction-update-summary.md +1 -1
  134. package/src/prompts/memories/consolidation.md +1 -1
  135. package/src/prompts/memories/read_path.md +1 -1
  136. package/src/prompts/memories/stage_one_input.md +1 -1
  137. package/src/prompts/memories/stage_one_system.md +1 -1
  138. package/src/prompts/review-request.md +1 -1
  139. package/src/prompts/system/agent-creation-architect.md +1 -1
  140. package/src/prompts/system/agent-creation-user.md +1 -1
  141. package/src/prompts/system/custom-system-prompt.md +1 -1
  142. package/src/prompts/system/file-operations.md +1 -1
  143. package/src/prompts/system/subagent-system-prompt.md +2 -2
  144. package/src/prompts/system/summarization-system.md +1 -1
  145. package/src/prompts/system/system-prompt.md +163 -178
  146. package/src/prompts/system/title-system.md +1 -1
  147. package/src/prompts/system/ttsr-interrupt.md +1 -1
  148. package/src/prompts/system/verification-reminder.md +6 -0
  149. package/src/prompts/system/web-search.md +1 -1
  150. package/src/sdk.ts +0 -9
  151. package/src/session/agent-session.ts +244 -1459
  152. package/src/session/auth-storage.ts +5 -0
  153. package/src/session/model-controller.ts +406 -0
  154. package/src/session/retry-utils.ts +71 -0
  155. package/src/session/session-manager.ts +22 -186
  156. package/src/session/session-types.ts +312 -0
  157. package/src/session/stats.ts +387 -0
  158. package/src/session/streaming-edit.ts +258 -0
  159. package/src/session/ttsr.ts +213 -0
  160. package/src/slash-commands/builtin-registry.ts +0 -8
  161. package/src/ssh/connection-manager.ts +1 -0
  162. package/src/stt/recorder.ts +2 -2
  163. package/src/system-prompt.ts +1 -14
  164. package/src/task/agents.ts +7 -33
  165. package/src/task/executor.ts +50 -438
  166. package/src/task/index.ts +104 -71
  167. package/src/task/progress-tracker.ts +390 -0
  168. package/src/task/render.ts +371 -187
  169. package/src/task/subprocess-tool-registry.ts +1 -1
  170. package/src/task/types.ts +14 -47
  171. package/src/tools/ask.ts +31 -42
  172. package/src/tools/bash-interactive.ts +4 -47
  173. package/src/tools/bash-interceptor.ts +2 -2
  174. package/src/tools/bash-normalize.ts +1 -1
  175. package/src/tools/bash-skill-urls.ts +2 -2
  176. package/src/tools/bash.ts +87 -136
  177. package/src/tools/browser.ts +54 -84
  178. package/src/tools/create-tools.ts +186 -0
  179. package/src/tools/default-renderer.ts +104 -0
  180. package/src/tools/explore.ts +11 -10
  181. package/src/tools/fetch.ts +24 -114
  182. package/src/tools/find.ts +48 -132
  183. package/src/tools/gemini-image.ts +5 -15
  184. package/src/tools/github.ts +450 -0
  185. package/src/tools/grep.ts +43 -179
  186. package/src/tools/index.ts +35 -198
  187. package/src/tools/json-tree.ts +3 -3
  188. package/src/tools/librarian.ts +18 -18
  189. package/src/tools/list-limit.ts +2 -2
  190. package/src/tools/notebook.ts +35 -87
  191. package/src/tools/oracle.ts +25 -25
  192. package/src/tools/output-meta.ts +89 -4
  193. package/src/tools/output-utils.ts +2 -2
  194. package/src/tools/python.ts +86 -637
  195. package/src/tools/read.ts +36 -119
  196. package/src/tools/reviewer-tool.ts +19 -21
  197. package/src/tools/search-code.ts +128 -0
  198. package/src/tools/ssh.ts +67 -126
  199. package/src/tools/subagent-tool.ts +197 -123
  200. package/src/tools/todo-write.ts +15 -31
  201. package/src/tools/tool-errors.ts +0 -30
  202. package/src/tools/undo-edit.ts +30 -67
  203. package/src/tools/write.ts +78 -127
  204. package/src/tui/code-cell.ts +4 -4
  205. package/src/tui/file-list.ts +2 -2
  206. package/src/tui/output-block.ts +1 -1
  207. package/src/tui/status-line.ts +1 -1
  208. package/src/tui/tree-list.ts +2 -2
  209. package/src/tui/types.ts +1 -1
  210. package/src/tui/utils.ts +1 -1
  211. package/src/{tools → ui}/render-utils.ts +87 -126
  212. package/src/utils/external-editor.ts +4 -4
  213. package/src/utils/file-mentions.ts +1 -1
  214. package/src/utils/index.ts +30 -0
  215. package/src/utils/tools-manager.ts +9 -19
  216. package/src/web/github-client.ts +290 -0
  217. package/src/web/scrapers/github.ts +11 -62
  218. package/src/web/search/auth.ts +1 -3
  219. package/src/web/search/index.ts +85 -49
  220. package/src/web/search/provider.ts +11 -16
  221. package/src/web/search/providers/grep.ts +160 -0
  222. package/src/web/search/render.ts +48 -235
  223. package/src/web/search/types.ts +1 -1
  224. package/src/commands/commit.ts +0 -36
  225. package/src/commit/agentic/agent.ts +0 -311
  226. package/src/commit/agentic/fallback.ts +0 -96
  227. package/src/commit/agentic/index.ts +0 -359
  228. package/src/commit/agentic/prompts/analyze-file.md +0 -22
  229. package/src/commit/agentic/prompts/session-user.md +0 -25
  230. package/src/commit/agentic/prompts/split-confirm.md +0 -1
  231. package/src/commit/agentic/prompts/system.md +0 -38
  232. package/src/commit/agentic/state.ts +0 -69
  233. package/src/commit/agentic/tools/analyze-file.ts +0 -118
  234. package/src/commit/agentic/tools/git-file-diff.ts +0 -194
  235. package/src/commit/agentic/tools/git-hunk.ts +0 -50
  236. package/src/commit/agentic/tools/git-overview.ts +0 -84
  237. package/src/commit/agentic/tools/index.ts +0 -56
  238. package/src/commit/agentic/tools/propose-changelog.ts +0 -128
  239. package/src/commit/agentic/tools/propose-commit.ts +0 -154
  240. package/src/commit/agentic/tools/recent-commits.ts +0 -81
  241. package/src/commit/agentic/tools/split-commit.ts +0 -280
  242. package/src/commit/agentic/topo-sort.ts +0 -44
  243. package/src/commit/agentic/trivial.ts +0 -51
  244. package/src/commit/agentic/validation.ts +0 -200
  245. package/src/commit/analysis/conventional.ts +0 -165
  246. package/src/commit/analysis/index.ts +0 -4
  247. package/src/commit/analysis/scope.ts +0 -242
  248. package/src/commit/analysis/summary.ts +0 -112
  249. package/src/commit/analysis/validation.ts +0 -66
  250. package/src/commit/changelog/detect.ts +0 -37
  251. package/src/commit/changelog/generate.ts +0 -110
  252. package/src/commit/changelog/index.ts +0 -234
  253. package/src/commit/changelog/parse.ts +0 -44
  254. package/src/commit/cli.ts +0 -93
  255. package/src/commit/git/diff.ts +0 -148
  256. package/src/commit/git/errors.ts +0 -9
  257. package/src/commit/git/index.ts +0 -211
  258. package/src/commit/git/operations.ts +0 -54
  259. package/src/commit/index.ts +0 -5
  260. package/src/commit/map-reduce/index.ts +0 -64
  261. package/src/commit/map-reduce/map-phase.ts +0 -178
  262. package/src/commit/map-reduce/reduce-phase.ts +0 -145
  263. package/src/commit/map-reduce/utils.ts +0 -9
  264. package/src/commit/message.ts +0 -11
  265. package/src/commit/model-selection.ts +0 -69
  266. package/src/commit/pipeline.ts +0 -243
  267. package/src/commit/prompts/analysis-system.md +0 -148
  268. package/src/commit/prompts/analysis-user.md +0 -38
  269. package/src/commit/prompts/changelog-system.md +0 -50
  270. package/src/commit/prompts/changelog-user.md +0 -18
  271. package/src/commit/prompts/file-observer-system.md +0 -24
  272. package/src/commit/prompts/file-observer-user.md +0 -8
  273. package/src/commit/prompts/reduce-system.md +0 -50
  274. package/src/commit/prompts/reduce-user.md +0 -17
  275. package/src/commit/prompts/summary-retry.md +0 -3
  276. package/src/commit/prompts/summary-system.md +0 -38
  277. package/src/commit/prompts/summary-user.md +0 -13
  278. package/src/commit/prompts/types-description.md +0 -2
  279. package/src/commit/types.ts +0 -109
  280. package/src/commit/utils/exclusions.ts +0 -42
  281. package/src/mcp/render.ts +0 -123
  282. package/src/modes/components/agent-dashboard.ts +0 -1130
  283. package/src/modes/components/codemode-group.ts +0 -369
  284. package/src/modes/components/read-tool-group.ts +0 -119
  285. package/src/modes/components/visual-truncate.ts +0 -63
  286. package/src/prompts/system/subagent-user-prompt.md +0 -8
  287. package/src/prompts/tools/ask.md +0 -44
  288. package/src/prompts/tools/bash.md +0 -24
  289. package/src/prompts/tools/browser.md +0 -33
  290. package/src/prompts/tools/calculator.md +0 -12
  291. package/src/prompts/tools/explore.md +0 -29
  292. package/src/prompts/tools/fetch.md +0 -16
  293. package/src/prompts/tools/find.md +0 -18
  294. package/src/prompts/tools/gemini-image.md +0 -23
  295. package/src/prompts/tools/grep.md +0 -28
  296. package/src/prompts/tools/hashline.md +0 -232
  297. package/src/prompts/tools/librarian.md +0 -24
  298. package/src/prompts/tools/lsp.md +0 -28
  299. package/src/prompts/tools/oracle.md +0 -26
  300. package/src/prompts/tools/patch.md +0 -74
  301. package/src/prompts/tools/python.md +0 -66
  302. package/src/prompts/tools/read.md +0 -36
  303. package/src/prompts/tools/replace.md +0 -38
  304. package/src/prompts/tools/reviewer.md +0 -41
  305. package/src/prompts/tools/ssh.md +0 -51
  306. package/src/prompts/tools/task-summary.md +0 -28
  307. package/src/prompts/tools/task.md +0 -146
  308. package/src/prompts/tools/todo-write.md +0 -65
  309. package/src/prompts/tools/undo-edit.md +0 -7
  310. package/src/prompts/tools/web-search.md +0 -19
  311. package/src/prompts/tools/write.md +0 -18
  312. package/src/task/batch.ts +0 -102
  313. package/src/task/discovery.ts +0 -126
  314. package/src/task/parallel.ts +0 -84
  315. package/src/task/template.ts +0 -32
  316. package/src/tools/calculator.ts +0 -537
  317. package/src/tools/jtd-to-typescript.ts +0 -198
  318. package/src/tools/renderers.ts +0 -60
  319. package/src/tools/tool-result.ts +0 -86
  320. /package/src/{modes/theme → theme}/dark.json +0 -0
  321. /package/src/{modes/theme → theme}/defaults/dark-catppuccin.json +0 -0
  322. /package/src/{modes/theme → theme}/defaults/dark-dracula.json +0 -0
  323. /package/src/{modes/theme → theme}/defaults/dark-gruvbox.json +0 -0
  324. /package/src/{modes/theme → theme}/defaults/dark-solarized.json +0 -0
  325. /package/src/{modes/theme → theme}/defaults/dark-tokyo-night.json +0 -0
  326. /package/src/{modes/theme → theme}/defaults/index.ts +0 -0
  327. /package/src/{modes/theme → theme}/defaults/light-catppuccin.json +0 -0
  328. /package/src/{modes/theme → theme}/defaults/light-github.json +0 -0
  329. /package/src/{modes/theme → theme}/defaults/light-solarized.json +0 -0
  330. /package/src/{modes/theme → theme}/light.json +0 -0
  331. /package/src/{modes/theme → theme}/mermaid-cache.ts +0 -0
  332. /package/src/{modes/theme → theme}/theme-schema.json +0 -0
  333. /package/src/{modes/theme → theme}/theme.ts +0 -0
@@ -8,48 +8,6 @@
8
8
  *
9
9
  * The mode is determined by the `edit.mode` setting.
10
10
  */
11
- import * as fs from "node:fs/promises";
12
- import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@nghyane/arcane-agent";
13
- import { StringEnum } from "@nghyane/arcane-ai";
14
- import { type Static, Type } from "@sinclair/typebox";
15
- import { renderPromptTemplate } from "../config/prompt-templates";
16
- import {
17
- createLspWritethrough,
18
- type FileDiagnosticsResult,
19
- flushLspWritethroughBatch,
20
- type WritethroughCallback,
21
- writethroughNoop,
22
- } from "../lsp";
23
- import hashlineDescription from "../prompts/tools/hashline.md" with { type: "text" };
24
- import patchDescription from "../prompts/tools/patch.md" with { type: "text" };
25
- import replaceDescription from "../prompts/tools/replace.md" with { type: "text" };
26
- import type { ToolSession } from "../tools";
27
- import {
28
- invalidateFsScanAfterDelete,
29
- invalidateFsScanAfterRename,
30
- invalidateFsScanAfterWrite,
31
- } from "../tools/fs-cache-invalidation";
32
- import { outputMeta } from "../tools/output-meta";
33
- import { resolveToCwd } from "../tools/path-utils";
34
- import { saveForUndo } from "../tools/undo-history";
35
- import { applyPatch } from "./applicator";
36
- import { generateDiffString, generateUnifiedDiffString, replaceText } from "./diff";
37
- import { findMatch } from "./fuzzy";
38
- import {
39
- applyHashlineEdits,
40
- computeLineHash,
41
- type HashlineEdit,
42
- type LineTag,
43
- parseTag,
44
- type ReplaceTextEdit,
45
- } from "./hashline";
46
- import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "./normalize";
47
- import { buildNormativeUpdateInput } from "./normative";
48
- import { type EditToolDetails, getLspBatchRequest } from "./shared";
49
- // Internal imports
50
- import type { FileSystem, Operation, PatchInput } from "./types";
51
- import { EditMatchError } from "./types";
52
-
53
11
  // ═══════════════════════════════════════════════════════════════════════════
54
12
  // Re-exports
55
13
  // ═══════════════════════════════════════════════════════════════════════════
@@ -65,9 +23,16 @@ export {
65
23
  generateUnifiedDiffString,
66
24
  replaceText,
67
25
  } from "./diff";
68
-
26
+ // Edit tool
27
+ export { EditTool } from "./edit-tool";
69
28
  // Fuzzy matching
70
- export { DEFAULT_FUZZY_THRESHOLD, findContextLine, findMatch as findEditMatch, findMatch, seekSequence } from "./fuzzy";
29
+ export {
30
+ DEFAULT_FUZZY_THRESHOLD,
31
+ findContextLine,
32
+ findMatch as findEditMatch,
33
+ findMatch,
34
+ seekSequence,
35
+ } from "./fuzzy";
71
36
  // Hashline
72
37
  export {
73
38
  applyHashlineEdits,
@@ -80,12 +45,32 @@ export {
80
45
  validateLineRef,
81
46
  } from "./hashline";
82
47
  // Normalization
83
- export { adjustIndentation, detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "./normalize";
48
+ export {
49
+ adjustIndentation,
50
+ detectLineEnding,
51
+ normalizeToLF,
52
+ restoreLineEndings,
53
+ stripBom,
54
+ } from "./normalize";
84
55
  // Parsing
85
- export { normalizeCreateContent, normalizeDiff, parseHunks as parseDiffHunks } from "./parser";
56
+ export {
57
+ normalizeCreateContent,
58
+ normalizeDiff,
59
+ parseHunks as parseDiffHunks,
60
+ } from "./parser";
61
+ // Schemas & types
62
+ export {
63
+ DEFAULT_EDIT_MODE,
64
+ type EditMode,
65
+ type HashlineParams,
66
+ type HashlineToolEdit,
67
+ normalizeEditMode,
68
+ type PatchParams,
69
+ type ReplaceParams,
70
+ } from "./schemas";
86
71
  export type { EditRenderContext, EditToolDetails } from "./shared";
87
72
  // Rendering
88
- export { editToolRenderer, getLspBatchRequest } from "./shared";
73
+ export { getLspBatchRequest } from "./shared";
89
74
  export type {
90
75
  ApplyPatchOptions,
91
76
  ApplyPatchResult,
@@ -111,854 +96,3 @@ export type {
111
96
  // Types
112
97
  // Legacy aliases for backwards compatibility
113
98
  export { ApplyPatchError, EditMatchError, ParseError } from "./types";
114
-
115
- // ═══════════════════════════════════════════════════════════════════════════
116
- // Schemas
117
- // ═══════════════════════════════════════════════════════════════════════════
118
-
119
- const replaceEditSchema = Type.Object({
120
- path: Type.String({ description: "File path (relative or absolute)" }),
121
- old_text: Type.String({ description: "Text to find (fuzzy whitespace matching enabled)" }),
122
- new_text: Type.String({ description: "Replacement text" }),
123
- all: Type.Optional(Type.Boolean({ description: "Replace all occurrences (default: unique match required)" })),
124
- });
125
-
126
- const patchEditSchema = Type.Object({
127
- path: Type.String({ description: "File path" }),
128
- op: Type.Optional(
129
- StringEnum(["create", "delete", "update"], {
130
- description: "Operation (default: update)",
131
- }),
132
- ),
133
- rename: Type.Optional(Type.String({ description: "New path for move" })),
134
- diff: Type.Optional(Type.String({ description: "Diff hunks (update) or full content (create)" })),
135
- });
136
-
137
- export type ReplaceParams = Static<typeof replaceEditSchema>;
138
- export type PatchParams = Static<typeof patchEditSchema>;
139
-
140
- /** Pattern matching hashline display format: `LINE#ID:CONTENT` */
141
- const HASHLINE_PREFIX_RE = /^\s*(?:>>>|>>)?\s*\d+#[0-9a-zA-Z]{1,16}:/;
142
-
143
- /** Pattern matching a unified-diff `+` prefix (but not `++`) */
144
- const DIFF_PLUS_RE = /^[+-](?![+-])/;
145
-
146
- /**
147
- * Strip hashline display prefixes and diff `+` markers from replacement lines.
148
- *
149
- * Models frequently copy the `LINE#ID ` prefix from read output into their
150
- * replacement content, or include unified-diff `+` prefixes. Both corrupt the
151
- * output file. This strips them heuristically before application.
152
- */
153
- function stripNewLinePrefixes(lines: string[]): string[] {
154
- // Detect whether the *majority* of non-empty lines carry a prefix —
155
- // if only one line out of many has a match it's likely real content.
156
- let hashPrefixCount = 0;
157
- let diffPlusCount = 0;
158
- let nonEmpty = 0;
159
- for (const l of lines) {
160
- if (l.length === 0) continue;
161
- nonEmpty++;
162
- if (HASHLINE_PREFIX_RE.test(l)) hashPrefixCount++;
163
- if (DIFF_PLUS_RE.test(l)) diffPlusCount++;
164
- }
165
- if (nonEmpty === 0) return lines;
166
-
167
- const stripHash = hashPrefixCount > 0 && hashPrefixCount >= nonEmpty * 0.5;
168
- const stripPlus = !stripHash && nonEmpty >= 2 && diffPlusCount > 0 && diffPlusCount >= nonEmpty * 0.5;
169
-
170
- if (!stripHash && !stripPlus) return lines;
171
-
172
- return lines.map(l => {
173
- if (stripHash) return l.replace(HASHLINE_PREFIX_RE, "");
174
- if (stripPlus) return l.replace(DIFF_PLUS_RE, "");
175
- return l;
176
- });
177
- }
178
-
179
- const hashlineReplaceContentFormat = (kind: string) =>
180
- Type.Union([
181
- Type.Null(),
182
- Type.Array(Type.String(), { description: `${kind} lines` }),
183
- Type.String({ description: `${kind} line` }),
184
- ]);
185
-
186
- const hashlineInsertContentFormat = (kind: string) =>
187
- Type.Union([
188
- Type.Array(Type.String(), { description: `${kind} lines`, minItems: 1 }),
189
- Type.String({ description: `${kind} line`, minLength: 1 }),
190
- ]);
191
-
192
- const hashlineTagFormat = (what: string) =>
193
- Type.String({
194
- description: `Tag identifying the ${what} — format "N#XX" (e.g. "5#PM"), copied verbatim from read output`,
195
- });
196
-
197
- function hashlineParseContent(edit: string | string[] | null): string[] {
198
- if (edit === null) return [];
199
- if (Array.isArray(edit)) return edit;
200
- const lines = stripNewLinePrefixes(edit.split("\n"));
201
- if (lines.length === 0) return [];
202
- if (lines.length > 1 && lines[lines.length - 1].trim() === "") return lines.slice(0, -1);
203
- return lines;
204
- }
205
-
206
- function hashlineParseContentString(edit: string | string[] | null): string {
207
- if (edit === null) return "";
208
- if (Array.isArray(edit)) return edit.join("\n");
209
- return edit;
210
- }
211
-
212
- const hashlineTargetEditSchema = Type.Object(
213
- {
214
- op: Type.Literal("set"),
215
- tag: hashlineTagFormat("line being replaced"),
216
- content: hashlineReplaceContentFormat("Replacement"),
217
- },
218
- { additionalProperties: false },
219
- );
220
-
221
- const hashlineAppendEditSchema = Type.Object(
222
- {
223
- op: Type.Literal("append"),
224
- after: Type.Optional(hashlineTagFormat("line after which to append")),
225
- content: hashlineInsertContentFormat("Appended"),
226
- },
227
- { additionalProperties: false },
228
- );
229
-
230
- const hashlinePrependEditSchema = Type.Object(
231
- {
232
- op: Type.Literal("prepend"),
233
- before: Type.Optional(hashlineTagFormat("line before which to prepend")),
234
- content: hashlineInsertContentFormat("Prepended"),
235
- },
236
- { additionalProperties: false },
237
- );
238
-
239
- const hashlineRangeEditSchema = Type.Object(
240
- {
241
- op: Type.Literal("replace"),
242
- first: hashlineTagFormat("first line"),
243
- last: hashlineTagFormat("last line"),
244
- content: hashlineReplaceContentFormat("Replacement"),
245
- },
246
- { additionalProperties: false },
247
- );
248
-
249
- const hashlineInsertEditSchema = Type.Object(
250
- {
251
- op: Type.Literal("insert"),
252
- before: Type.Optional(hashlineTagFormat("line before which to insert")),
253
- after: Type.Optional(hashlineTagFormat("line after which to insert")),
254
- content: hashlineInsertContentFormat("Inserted"),
255
- },
256
- { additionalProperties: false },
257
- );
258
-
259
- const hashlineReplaceTextEditSchema = Type.Object(
260
- {
261
- op: Type.Literal("replaceText"),
262
- old_text: Type.String({ description: "Text to find", minLength: 1 }),
263
- new_text: hashlineReplaceContentFormat("Replacement"),
264
- all: Type.Optional(Type.Boolean({ description: "Replace all occurrences" })),
265
- },
266
- { additionalProperties: false },
267
- );
268
-
269
- const HL_REPLACE_ENABLED = Bun.env.ARCANE_HL_REPLACETXT === "1";
270
-
271
- const hashlineEditSpecSchema = Type.Union([
272
- hashlineTargetEditSchema,
273
- hashlineRangeEditSchema,
274
- hashlineAppendEditSchema,
275
- hashlinePrependEditSchema,
276
- hashlineInsertEditSchema,
277
- ...(HL_REPLACE_ENABLED ? [hashlineReplaceTextEditSchema] : []),
278
- ]);
279
-
280
- const hashlineEditSchema = Type.Object(
281
- {
282
- path: Type.String({ description: "File path (relative or absolute)" }),
283
- edits: Type.Array(hashlineEditSpecSchema, {
284
- description: "Changes to apply to the file at `path`",
285
- minItems: 0,
286
- }),
287
- delete: Type.Optional(Type.Boolean({ description: "Delete the file when true" })),
288
- rename: Type.Optional(Type.String({ description: "New path if moving" })),
289
- },
290
- { additionalProperties: false },
291
- );
292
-
293
- export type HashlineToolEdit = Static<typeof hashlineEditSpecSchema>;
294
- export type HashlineParams = Static<typeof hashlineEditSchema>;
295
-
296
- // ═══════════════════════════════════════════════════════════════════════════
297
- // LSP FileSystem for patch mode
298
- // ═══════════════════════════════════════════════════════════════════════════
299
-
300
- class LspFileSystem implements FileSystem {
301
- #lastDiagnostics: FileDiagnosticsResult | undefined;
302
- #fileCache: Record<string, Bun.BunFile> = {};
303
-
304
- constructor(
305
- private readonly writethrough: (
306
- dst: string,
307
- content: string,
308
- signal?: AbortSignal,
309
- file?: import("bun").BunFile,
310
- batch?: { id: string; flush: boolean },
311
- ) => Promise<FileDiagnosticsResult | undefined>,
312
- private readonly signal?: AbortSignal,
313
- private readonly batchRequest?: { id: string; flush: boolean },
314
- ) {}
315
-
316
- #getFile(path: string): Bun.BunFile {
317
- if (this.#fileCache[path]) {
318
- return this.#fileCache[path];
319
- }
320
- const file = Bun.file(path);
321
- this.#fileCache[path] = file;
322
- return file;
323
- }
324
-
325
- async exists(path: string): Promise<boolean> {
326
- return this.#getFile(path).exists();
327
- }
328
-
329
- async read(path: string): Promise<string> {
330
- return this.#getFile(path).text();
331
- }
332
-
333
- async readBinary(path: string): Promise<Uint8Array> {
334
- const buffer = await this.#getFile(path).arrayBuffer();
335
- return new Uint8Array(buffer);
336
- }
337
-
338
- async write(path: string, content: string): Promise<void> {
339
- const file = this.#getFile(path);
340
- const result = await this.writethrough(path, content, this.signal, file, this.batchRequest);
341
- if (result) {
342
- this.#lastDiagnostics = result;
343
- }
344
- }
345
-
346
- async delete(path: string): Promise<void> {
347
- await this.#getFile(path).unlink();
348
- }
349
-
350
- async mkdir(path: string): Promise<void> {
351
- await fs.mkdir(path, { recursive: true });
352
- }
353
-
354
- getDiagnostics(): FileDiagnosticsResult | undefined {
355
- return this.#lastDiagnostics;
356
- }
357
- }
358
-
359
- function mergeDiagnosticsWithWarnings(
360
- diagnostics: FileDiagnosticsResult | undefined,
361
- warnings: string[],
362
- ): FileDiagnosticsResult | undefined {
363
- if (warnings.length === 0) return diagnostics;
364
- const warningMessages = warnings.map(warning => `patch: ${warning}`);
365
- if (!diagnostics) {
366
- return {
367
- server: "patch",
368
- messages: warningMessages,
369
- summary: `Patch warnings: ${warnings.length}`,
370
- errored: false,
371
- };
372
- }
373
- return {
374
- ...diagnostics,
375
- messages: [...warningMessages, ...diagnostics.messages],
376
- summary: `${diagnostics.summary}; Patch warnings: ${warnings.length}`,
377
- };
378
- }
379
-
380
- // ═══════════════════════════════════════════════════════════════════════════
381
- // Tool Class
382
- // ═══════════════════════════════════════════════════════════════════════════
383
-
384
- type TInput = typeof replaceEditSchema | typeof patchEditSchema | typeof hashlineEditSchema;
385
-
386
- export type EditMode = "replace" | "patch" | "hashline";
387
-
388
- export const DEFAULT_EDIT_MODE: EditMode = "patch";
389
-
390
- export function normalizeEditMode(mode?: string | null): EditMode | null {
391
- switch (mode) {
392
- case "replace":
393
- return "replace";
394
- case "patch":
395
- return "patch";
396
- case "hashline":
397
- return "hashline";
398
- default:
399
- return null;
400
- }
401
- }
402
-
403
- /**
404
- * Edit tool implementation.
405
- *
406
- * Creates replace-mode, patch-mode, or hashline-mode behavior based on session settings.
407
- */
408
- export class EditTool implements AgentTool<TInput> {
409
- readonly name = "edit";
410
- readonly label = "Edit";
411
- readonly nonAbortable = true;
412
- readonly concurrency = "exclusive";
413
-
414
- readonly #allowFuzzy: boolean;
415
- readonly #fuzzyThreshold: number;
416
- readonly #writethrough: WritethroughCallback;
417
- readonly #editMode?: EditMode | null;
418
-
419
- constructor(private readonly session: ToolSession) {
420
- const {
421
- ARCANE_EDIT_FUZZY: editFuzzy = "auto",
422
- ARCANE_EDIT_FUZZY_THRESHOLD: editFuzzyThreshold = "auto",
423
- ARCANE_EDIT_VARIANT: envEditVariant = "auto",
424
- } = Bun.env;
425
-
426
- if (envEditVariant && envEditVariant !== "auto") {
427
- const editMode = normalizeEditMode(envEditVariant);
428
- if (!editMode) {
429
- throw new Error(`Invalid ARCANE_EDIT_VARIANT: ${envEditVariant}`);
430
- }
431
- this.#editMode = editMode;
432
- }
433
-
434
- switch (editFuzzy) {
435
- case "true":
436
- case "1":
437
- this.#allowFuzzy = true;
438
- break;
439
- case "false":
440
- case "0":
441
- this.#allowFuzzy = false;
442
- break;
443
- case "auto":
444
- this.#allowFuzzy = session.settings.get("edit.fuzzyMatch");
445
- break;
446
- default:
447
- throw new Error(`Invalid ARCANE_EDIT_FUZZY: ${editFuzzy}`);
448
- }
449
- switch (editFuzzyThreshold) {
450
- case "auto":
451
- this.#fuzzyThreshold = session.settings.get("edit.fuzzyThreshold");
452
- break;
453
- default:
454
- this.#fuzzyThreshold = parseFloat(editFuzzyThreshold);
455
- if (Number.isNaN(this.#fuzzyThreshold) || this.#fuzzyThreshold < 0 || this.#fuzzyThreshold > 1) {
456
- throw new Error(`Invalid ARCANE_EDIT_FUZZY_THRESHOLD: ${editFuzzyThreshold}`);
457
- }
458
- break;
459
- }
460
-
461
- const enableLsp = session.enableLsp ?? true;
462
- const enableDiagnostics = enableLsp && session.settings.get("lsp.diagnosticsOnEdit");
463
- const enableFormat = enableLsp && session.settings.get("lsp.formatOnWrite");
464
- this.#writethrough = enableLsp
465
- ? createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics })
466
- : writethroughNoop;
467
- }
468
-
469
- /**
470
- * Determine edit mode dynamically based on current model.
471
- * This is re-evaluated on each access so tool definitions stay current when model changes.
472
- */
473
- get mode(): EditMode {
474
- if (this.#editMode) return this.#editMode;
475
- const activeModel = this.session.getActiveModelString?.();
476
- const editVariant =
477
- this.session.settings.getEditVariantForModel(activeModel) ??
478
- normalizeEditMode(this.session.settings.get("edit.mode"));
479
- return editVariant ?? DEFAULT_EDIT_MODE;
480
- }
481
-
482
- /**
483
- * Dynamic description based on current edit mode (which depends on current model).
484
- */
485
- get description(): string {
486
- switch (this.mode) {
487
- case "patch":
488
- return renderPromptTemplate(patchDescription);
489
- case "hashline":
490
- return renderPromptTemplate(hashlineDescription, { allowReplaceText: HL_REPLACE_ENABLED });
491
- default:
492
- return renderPromptTemplate(replaceDescription);
493
- }
494
- }
495
-
496
- /**
497
- * Dynamic parameters schema based on current edit mode (which depends on current model).
498
- */
499
- get parameters(): TInput {
500
- switch (this.mode) {
501
- case "patch":
502
- return patchEditSchema;
503
- case "hashline":
504
- return hashlineEditSchema;
505
- default:
506
- return replaceEditSchema;
507
- }
508
- }
509
-
510
- async execute(
511
- _toolCallId: string,
512
- params: ReplaceParams | PatchParams | HashlineParams,
513
- signal?: AbortSignal,
514
- _onUpdate?: AgentToolUpdateCallback<EditToolDetails, TInput>,
515
- context?: AgentToolContext,
516
- ): Promise<AgentToolResult<EditToolDetails, TInput>> {
517
- const batchRequest = getLspBatchRequest(context?.toolCall);
518
-
519
- // ─────────────────────────────────────────────────────────────────
520
- // Hashline mode execution
521
- // ─────────────────────────────────────────────────────────────────
522
- if (this.mode === "hashline") {
523
- const { path, edits, delete: deleteFile, rename } = params as HashlineParams;
524
-
525
- if (path.endsWith(".ipynb") && edits?.length > 0) {
526
- throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
527
- }
528
-
529
- const absolutePath = resolveToCwd(path, this.session.cwd);
530
- const resolvedRename = rename ? resolveToCwd(rename, this.session.cwd) : undefined;
531
- const file = Bun.file(absolutePath);
532
-
533
- if (deleteFile) {
534
- if (await file.exists()) {
535
- await file.unlink();
536
- }
537
- invalidateFsScanAfterDelete(absolutePath);
538
- return {
539
- content: [{ type: "text", text: `Deleted ${path}` }],
540
- details: {
541
- diff: "",
542
- op: "delete",
543
- meta: outputMeta().get(),
544
- },
545
- };
546
- }
547
-
548
- if (!(await file.exists())) {
549
- const content: string[] = [];
550
- for (const edit of edits) {
551
- switch (edit.op) {
552
- case "append": {
553
- if (edit.after) {
554
- throw new Error(`File not found: ${path}`);
555
- }
556
- content.push(...hashlineParseContent(edit.content));
557
- break;
558
- }
559
- case "prepend": {
560
- if (edit.before) {
561
- throw new Error(`File not found: ${path}`);
562
- }
563
- content.unshift(...hashlineParseContent(edit.content));
564
- break;
565
- }
566
- default: {
567
- throw new Error(`File not found: ${path}`);
568
- }
569
- }
570
- }
571
- await file.write(content.join("\n"));
572
- return {
573
- content: [{ type: "text", text: `Created ${path}` }],
574
- details: {
575
- diff: "",
576
- op: "create",
577
- meta: outputMeta().get(),
578
- },
579
- };
580
- }
581
-
582
- const anchorEdits: HashlineEdit[] = [];
583
- const replaceEdits: ReplaceTextEdit[] = [];
584
- for (const edit of edits) {
585
- switch (edit.op) {
586
- case "set": {
587
- const { tag, content } = edit;
588
- anchorEdits.push({ op: "set", tag: parseTag(tag), content: hashlineParseContent(content) });
589
- break;
590
- }
591
- case "replace": {
592
- const { first, last, content } = edit;
593
- anchorEdits.push({
594
- op: "replace",
595
- first: parseTag(first),
596
- last: parseTag(last),
597
- content: hashlineParseContent(content),
598
- });
599
- break;
600
- }
601
- case "append": {
602
- const { after, content } = edit;
603
- anchorEdits.push({
604
- op: "append",
605
- ...(after ? { after: parseTag(after) } : {}),
606
- content: hashlineParseContent(content),
607
- });
608
- break;
609
- }
610
- case "prepend": {
611
- const { before, content } = edit;
612
- anchorEdits.push({
613
- op: "prepend",
614
- ...(before ? { before: parseTag(before) } : {}),
615
- content: hashlineParseContent(content),
616
- });
617
- break;
618
- }
619
- case "insert": {
620
- const { before, after, content } = edit;
621
- if (before && !after) {
622
- anchorEdits.push({
623
- op: "prepend",
624
- before: parseTag(before),
625
- content: hashlineParseContent(content),
626
- });
627
- } else if (after && !before) {
628
- anchorEdits.push({
629
- op: "append",
630
- after: parseTag(after),
631
- content: hashlineParseContent(content),
632
- });
633
- } else if (before && after) {
634
- anchorEdits.push({
635
- op: "insert",
636
- before: parseTag(before),
637
- after: parseTag(after),
638
- content: hashlineParseContent(content),
639
- });
640
- } else {
641
- throw new Error(`Insert must have both before and after tags.`);
642
- }
643
- break;
644
- }
645
- case "replaceText": {
646
- const { old_text, new_text, all } = edit;
647
- replaceEdits.push({
648
- op: "replaceText",
649
- old_text: old_text,
650
- new_text: hashlineParseContentString(new_text),
651
- all: all ?? false,
652
- });
653
- break;
654
- }
655
- default:
656
- throw new Error(`Invalid edit operation: ${JSON.stringify(edit)}`);
657
- }
658
- }
659
-
660
- const rawContent = await file.text();
661
- const { bom, text: content } = stripBom(rawContent);
662
- const originalEnding = detectLineEnding(content);
663
- const originalNormalized = normalizeToLF(content);
664
- let normalizedContent = originalNormalized;
665
-
666
- // Apply anchor-based edits first (set, set_range, insert)
667
- const anchorResult = applyHashlineEdits(normalizedContent, anchorEdits);
668
- normalizedContent = anchorResult.content;
669
-
670
- // Apply content-replace edits (substr-style fuzzy replace)
671
- for (const r of replaceEdits) {
672
- if (r.old_text.length === 0) {
673
- throw new Error("old_text must not be empty.");
674
- }
675
- const rep = replaceText(normalizedContent, r.old_text, r.new_text, {
676
- fuzzy: this.#allowFuzzy,
677
- all: r.all ?? false,
678
- threshold: this.#fuzzyThreshold,
679
- });
680
- normalizedContent = rep.content;
681
- }
682
-
683
- const result = {
684
- content: normalizedContent,
685
- firstChangedLine: anchorResult.firstChangedLine,
686
- warnings: anchorResult.warnings,
687
- noopEdits: anchorResult.noopEdits,
688
- };
689
- if (originalNormalized === result.content && !rename) {
690
- let diagnostic = `No changes made to ${path}. The edits produced identical content.`;
691
- if (result.noopEdits && result.noopEdits.length > 0) {
692
- const details = result.noopEdits
693
- .map(
694
- e =>
695
- `Edit ${e.editIndex}: replacement for ${e.loc} is identical to current content:\n ${e.loc}| ${e.currentContent}`,
696
- )
697
- .join("\n");
698
- diagnostic += `\n${details}`;
699
- diagnostic +=
700
- "\nYour content must differ from what the file already contains. Re-read the file to see the current state.";
701
- } else {
702
- // Edits were not literally identical but heuristics normalized them back
703
- const lines = result.content.split("\n");
704
- const targetLines: string[] = [];
705
- const refs: LineTag[] = [];
706
- for (const edit of anchorEdits) {
707
- refs.length = 0;
708
- switch (edit.op) {
709
- case "set":
710
- refs.push(edit.tag);
711
- break;
712
- case "replace":
713
- refs.push(edit.first, edit.last);
714
- break;
715
- case "append":
716
- if (edit.after) refs.push(edit.after);
717
- break;
718
- case "prepend":
719
- if (edit.before) refs.push(edit.before);
720
- break;
721
- case "insert":
722
- refs.push(edit.after, edit.before);
723
- break;
724
- default:
725
- break;
726
- }
727
-
728
- for (const ref of refs) {
729
- try {
730
- if (ref.line >= 1 && ref.line <= lines.length) {
731
- const lineContent = lines[ref.line - 1];
732
- const hash = computeLineHash(ref.line, lineContent);
733
- targetLines.push(`${ref.line}#${hash}:${lineContent}`);
734
- }
735
- } catch {
736
- /* skip malformed refs */
737
- }
738
- }
739
- }
740
- if (targetLines.length > 0) {
741
- const preview = [...new Set(targetLines)].slice(0, 5).join("\n");
742
- diagnostic += `\nThe file currently contains these lines:\n${preview}\nYour edits were normalized back to the original content (whitespace-only differences are preserved as-is). Ensure your replacement changes actual code, not just formatting.`;
743
- }
744
- }
745
- throw new Error(diagnostic);
746
- }
747
-
748
- const finalContent = bom + restoreLineEndings(result.content, originalEnding);
749
- const writePath = resolvedRename ?? absolutePath;
750
- saveForUndo(absolutePath, rawContent);
751
- const diagnostics = await this.#writethrough(
752
- writePath,
753
- finalContent,
754
- signal,
755
- Bun.file(writePath),
756
- batchRequest,
757
- );
758
- if (resolvedRename && resolvedRename !== absolutePath) {
759
- await file.unlink();
760
- invalidateFsScanAfterRename(absolutePath, resolvedRename);
761
- } else {
762
- invalidateFsScanAfterWrite(absolutePath);
763
- }
764
- const diffResult = generateDiffString(originalNormalized, result.content);
765
-
766
- const normative = buildNormativeUpdateInput({
767
- path,
768
- ...(rename ? { rename } : {}),
769
- oldContent: rawContent,
770
- newContent: finalContent,
771
- });
772
-
773
- const meta = outputMeta()
774
- .diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
775
- .get();
776
-
777
- const resultText = rename ? `Updated and moved ${path} to ${rename}` : `Updated ${path}`;
778
- return {
779
- content: [
780
- {
781
- type: "text",
782
- text: `${resultText}${result.warnings?.length ? `\n\nWarnings:\n${result.warnings.join("\n")}` : ""}`,
783
- },
784
- ],
785
- details: {
786
- diff: diffResult.diff,
787
- firstChangedLine: result.firstChangedLine ?? diffResult.firstChangedLine,
788
- diagnostics,
789
- op: "update",
790
- rename,
791
- meta,
792
- },
793
- $normative: normative,
794
- };
795
- }
796
-
797
- // ─────────────────────────────────────────────────────────────────
798
- // Patch mode execution
799
- // ─────────────────────────────────────────────────────────────────
800
- if (this.mode === "patch") {
801
- const { path, op: rawOp, rename, diff } = params as PatchParams;
802
-
803
- // Normalize unrecognized operations to "update"
804
- const op: Operation = rawOp === "create" || rawOp === "delete" ? rawOp : "update";
805
-
806
- const resolvedPath = resolveToCwd(path, this.session.cwd);
807
- const resolvedRename = rename ? resolveToCwd(rename, this.session.cwd) : undefined;
808
-
809
- if (path.endsWith(".ipynb")) {
810
- throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
811
- }
812
- if (rename?.endsWith(".ipynb")) {
813
- throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
814
- }
815
-
816
- const input: PatchInput = { path: resolvedPath, op, rename: resolvedRename, diff };
817
- const fs = new LspFileSystem(this.#writethrough, signal, batchRequest);
818
- const result = await applyPatch(input, {
819
- cwd: this.session.cwd,
820
- fs,
821
- fuzzyThreshold: this.#fuzzyThreshold,
822
- allowFuzzy: this.#allowFuzzy,
823
- });
824
- if (result.change.oldContent !== undefined) {
825
- saveForUndo(resolvedPath, result.change.oldContent);
826
- }
827
- if (resolvedRename) {
828
- invalidateFsScanAfterRename(resolvedPath, resolvedRename);
829
- } else if (result.change.type === "delete") {
830
- invalidateFsScanAfterDelete(resolvedPath);
831
- } else {
832
- invalidateFsScanAfterWrite(resolvedPath);
833
- }
834
- const effRename = result.change.newPath ? rename : undefined;
835
-
836
- // Generate diff for display
837
- let diffResult = { diff: "", firstChangedLine: undefined as number | undefined };
838
- if (result.change.type === "update" && result.change.oldContent && result.change.newContent) {
839
- const normalizedOld = normalizeToLF(stripBom(result.change.oldContent).text);
840
- const normalizedNew = normalizeToLF(stripBom(result.change.newContent).text);
841
- diffResult = generateUnifiedDiffString(normalizedOld, normalizedNew);
842
- }
843
-
844
- let resultText: string;
845
- switch (result.change.type) {
846
- case "create":
847
- resultText = `Created ${path}`;
848
- break;
849
- case "delete":
850
- resultText = `Deleted ${path}`;
851
- break;
852
- case "update":
853
- resultText = effRename ? `Updated and moved ${path} to ${effRename}` : `Updated ${path}`;
854
- break;
855
- }
856
-
857
- let diagnostics = fs.getDiagnostics();
858
- if (op === "delete" && batchRequest?.flush) {
859
- const flushedDiagnostics = await flushLspWritethroughBatch(batchRequest.id, this.session.cwd, signal);
860
- diagnostics ??= flushedDiagnostics;
861
- }
862
- const patchWarnings = result.warnings ?? [];
863
- const mergedDiagnostics = mergeDiagnosticsWithWarnings(diagnostics, patchWarnings);
864
-
865
- const meta = outputMeta()
866
- .diagnostics(mergedDiagnostics?.summary ?? "", mergedDiagnostics?.messages ?? [])
867
- .get();
868
-
869
- return {
870
- content: [{ type: "text", text: resultText }],
871
- details: {
872
- diff: diffResult.diff,
873
- firstChangedLine: diffResult.firstChangedLine,
874
- diagnostics: mergedDiagnostics,
875
- op,
876
- rename: effRename,
877
- meta,
878
- },
879
- };
880
- }
881
-
882
- // ─────────────────────────────────────────────────────────────────
883
- // Replace mode execution
884
- // ─────────────────────────────────────────────────────────────────
885
- const { path, old_text, new_text, all } = params as ReplaceParams;
886
-
887
- if (path.endsWith(".ipynb")) {
888
- throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
889
- }
890
-
891
- if (old_text.length === 0) {
892
- throw new Error("old_text must not be empty.");
893
- }
894
-
895
- const absolutePath = resolveToCwd(path, this.session.cwd);
896
- const file = Bun.file(absolutePath);
897
-
898
- if (!(await file.exists())) {
899
- throw new Error(`File not found: ${path}`);
900
- }
901
-
902
- const rawContent = await file.text();
903
- const { bom, text: content } = stripBom(rawContent);
904
- const originalEnding = detectLineEnding(content);
905
- const normalizedContent = normalizeToLF(content);
906
- const normalizedOldText = normalizeToLF(old_text);
907
- const normalizedNewText = normalizeToLF(new_text);
908
-
909
- const result = replaceText(normalizedContent, normalizedOldText, normalizedNewText, {
910
- fuzzy: this.#allowFuzzy,
911
- all: all ?? false,
912
- threshold: this.#fuzzyThreshold,
913
- });
914
-
915
- if (result.count === 0) {
916
- // Get error details
917
- const matchOutcome = findMatch(normalizedContent, normalizedOldText, {
918
- allowFuzzy: this.#allowFuzzy,
919
- threshold: this.#fuzzyThreshold,
920
- });
921
-
922
- if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
923
- const previews = matchOutcome.occurrencePreviews?.join("\n\n") ?? "";
924
- const moreMsg = matchOutcome.occurrences > 5 ? ` (showing first 5 of ${matchOutcome.occurrences})` : "";
925
- throw new Error(
926
- `Found ${matchOutcome.occurrences} occurrences in ${path}${moreMsg}:\n\n${previews}\n\n` +
927
- `Add more context lines to disambiguate.`,
928
- );
929
- }
930
-
931
- throw new EditMatchError(path, normalizedOldText, matchOutcome.closest, {
932
- allowFuzzy: this.#allowFuzzy,
933
- threshold: this.#fuzzyThreshold,
934
- fuzzyMatches: matchOutcome.fuzzyMatches,
935
- });
936
- }
937
-
938
- if (normalizedContent === result.content) {
939
- throw new Error(
940
- `No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`,
941
- );
942
- }
943
-
944
- const finalContent = bom + restoreLineEndings(result.content, originalEnding);
945
- saveForUndo(absolutePath, rawContent);
946
- const diagnostics = await this.#writethrough(absolutePath, finalContent, signal, file, batchRequest);
947
- invalidateFsScanAfterWrite(absolutePath);
948
- const diffResult = generateDiffString(normalizedContent, result.content);
949
-
950
- const resultText =
951
- result.count > 1
952
- ? `Successfully replaced ${result.count} occurrences in ${path}.`
953
- : `Successfully replaced text in ${path}.`;
954
-
955
- const meta = outputMeta()
956
- .diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
957
- .get();
958
-
959
- return {
960
- content: [{ type: "text", text: resultText }],
961
- details: { diff: diffResult.diff, firstChangedLine: diffResult.firstChangedLine, diagnostics, meta },
962
- };
963
- }
964
- }