@nghyane/arcane 0.1.0

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 (738) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/README.md +12 -0
  3. package/examples/README.md +21 -0
  4. package/examples/custom-tools/README.md +109 -0
  5. package/examples/custom-tools/hello/index.ts +20 -0
  6. package/examples/custom-tools/todo/index.ts +206 -0
  7. package/examples/extensions/README.md +143 -0
  8. package/examples/extensions/api-demo.ts +89 -0
  9. package/examples/extensions/chalk-logger.ts +25 -0
  10. package/examples/extensions/hello.ts +32 -0
  11. package/examples/extensions/pirate.ts +43 -0
  12. package/examples/extensions/plan-mode.ts +550 -0
  13. package/examples/extensions/reload-runtime.ts +37 -0
  14. package/examples/extensions/todo.ts +296 -0
  15. package/examples/extensions/tools.ts +144 -0
  16. package/examples/extensions/with-deps/index.ts +35 -0
  17. package/examples/extensions/with-deps/package-lock.json +31 -0
  18. package/examples/extensions/with-deps/package.json +16 -0
  19. package/examples/hooks/README.md +56 -0
  20. package/examples/hooks/auto-commit-on-exit.ts +48 -0
  21. package/examples/hooks/confirm-destructive.ts +58 -0
  22. package/examples/hooks/custom-compaction.ts +116 -0
  23. package/examples/hooks/dirty-repo-guard.ts +51 -0
  24. package/examples/hooks/file-trigger.ts +40 -0
  25. package/examples/hooks/git-checkpoint.ts +52 -0
  26. package/examples/hooks/handoff.ts +150 -0
  27. package/examples/hooks/permission-gate.ts +33 -0
  28. package/examples/hooks/protected-paths.ts +29 -0
  29. package/examples/hooks/qna.ts +119 -0
  30. package/examples/hooks/status-line.ts +39 -0
  31. package/examples/sdk/01-minimal.ts +21 -0
  32. package/examples/sdk/02-custom-model.ts +49 -0
  33. package/examples/sdk/03-custom-prompt.ts +43 -0
  34. package/examples/sdk/04-skills.ts +43 -0
  35. package/examples/sdk/06-extensions.ts +80 -0
  36. package/examples/sdk/06-hooks.ts +61 -0
  37. package/examples/sdk/07-context-files.ts +35 -0
  38. package/examples/sdk/08-prompt-templates.ts +36 -0
  39. package/examples/sdk/08-slash-commands.ts +41 -0
  40. package/examples/sdk/09-api-keys-and-oauth.ts +54 -0
  41. package/examples/sdk/11-sessions.ts +47 -0
  42. package/examples/sdk/README.md +150 -0
  43. package/package.json +464 -0
  44. package/scripts/format-prompts.ts +184 -0
  45. package/scripts/generate-docs-index.ts +40 -0
  46. package/scripts/generate-template.ts +32 -0
  47. package/src/bun-imports.d.ts +22 -0
  48. package/src/capability/context-file.ts +39 -0
  49. package/src/capability/extension-module.ts +33 -0
  50. package/src/capability/extension.ts +47 -0
  51. package/src/capability/fs.ts +89 -0
  52. package/src/capability/hook.ts +39 -0
  53. package/src/capability/index.ts +432 -0
  54. package/src/capability/instruction.ts +36 -0
  55. package/src/capability/mcp.ts +60 -0
  56. package/src/capability/prompt.ts +34 -0
  57. package/src/capability/rule.ts +223 -0
  58. package/src/capability/settings.ts +34 -0
  59. package/src/capability/skill.ts +48 -0
  60. package/src/capability/slash-command.ts +39 -0
  61. package/src/capability/ssh.ts +41 -0
  62. package/src/capability/system-prompt.ts +34 -0
  63. package/src/capability/tool.ts +37 -0
  64. package/src/capability/types.ts +156 -0
  65. package/src/cli/args.ts +259 -0
  66. package/src/cli/config-cli.ts +357 -0
  67. package/src/cli/file-processor.ts +124 -0
  68. package/src/cli/grep-cli.ts +152 -0
  69. package/src/cli/jupyter-cli.ts +106 -0
  70. package/src/cli/list-models.ts +103 -0
  71. package/src/cli/plugin-cli.ts +661 -0
  72. package/src/cli/session-picker.ts +42 -0
  73. package/src/cli/setup-cli.ts +376 -0
  74. package/src/cli/shell-cli.ts +174 -0
  75. package/src/cli/ssh-cli.ts +179 -0
  76. package/src/cli/stats-cli.ts +197 -0
  77. package/src/cli/update-cli.ts +286 -0
  78. package/src/cli/web-search-cli.ts +143 -0
  79. package/src/cli.ts +65 -0
  80. package/src/commands/commit.ts +36 -0
  81. package/src/commands/config.ts +51 -0
  82. package/src/commands/grep.ts +41 -0
  83. package/src/commands/jupyter.ts +32 -0
  84. package/src/commands/launch.ts +139 -0
  85. package/src/commands/plugin.ts +70 -0
  86. package/src/commands/setup.ts +42 -0
  87. package/src/commands/shell.ts +29 -0
  88. package/src/commands/ssh.ts +60 -0
  89. package/src/commands/stats.ts +29 -0
  90. package/src/commands/update.ts +21 -0
  91. package/src/commands/web-search.ts +42 -0
  92. package/src/commit/agentic/agent.ts +311 -0
  93. package/src/commit/agentic/fallback.ts +96 -0
  94. package/src/commit/agentic/index.ts +359 -0
  95. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  96. package/src/commit/agentic/prompts/session-user.md +25 -0
  97. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  98. package/src/commit/agentic/prompts/system.md +38 -0
  99. package/src/commit/agentic/state.ts +69 -0
  100. package/src/commit/agentic/tools/analyze-file.ts +118 -0
  101. package/src/commit/agentic/tools/git-file-diff.ts +194 -0
  102. package/src/commit/agentic/tools/git-hunk.ts +50 -0
  103. package/src/commit/agentic/tools/git-overview.ts +84 -0
  104. package/src/commit/agentic/tools/index.ts +56 -0
  105. package/src/commit/agentic/tools/propose-changelog.ts +128 -0
  106. package/src/commit/agentic/tools/propose-commit.ts +154 -0
  107. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  108. package/src/commit/agentic/tools/split-commit.ts +280 -0
  109. package/src/commit/agentic/topo-sort.ts +44 -0
  110. package/src/commit/agentic/trivial.ts +51 -0
  111. package/src/commit/agentic/validation.ts +200 -0
  112. package/src/commit/analysis/conventional.ts +165 -0
  113. package/src/commit/analysis/index.ts +4 -0
  114. package/src/commit/analysis/scope.ts +242 -0
  115. package/src/commit/analysis/summary.ts +112 -0
  116. package/src/commit/analysis/validation.ts +66 -0
  117. package/src/commit/changelog/detect.ts +37 -0
  118. package/src/commit/changelog/generate.ts +110 -0
  119. package/src/commit/changelog/index.ts +234 -0
  120. package/src/commit/changelog/parse.ts +44 -0
  121. package/src/commit/cli.ts +93 -0
  122. package/src/commit/git/diff.ts +148 -0
  123. package/src/commit/git/errors.ts +9 -0
  124. package/src/commit/git/index.ts +211 -0
  125. package/src/commit/git/operations.ts +54 -0
  126. package/src/commit/index.ts +5 -0
  127. package/src/commit/map-reduce/index.ts +64 -0
  128. package/src/commit/map-reduce/map-phase.ts +178 -0
  129. package/src/commit/map-reduce/reduce-phase.ts +145 -0
  130. package/src/commit/map-reduce/utils.ts +9 -0
  131. package/src/commit/message.ts +11 -0
  132. package/src/commit/model-selection.ts +69 -0
  133. package/src/commit/pipeline.ts +243 -0
  134. package/src/commit/prompts/analysis-system.md +148 -0
  135. package/src/commit/prompts/analysis-user.md +38 -0
  136. package/src/commit/prompts/changelog-system.md +50 -0
  137. package/src/commit/prompts/changelog-user.md +18 -0
  138. package/src/commit/prompts/file-observer-system.md +24 -0
  139. package/src/commit/prompts/file-observer-user.md +8 -0
  140. package/src/commit/prompts/reduce-system.md +50 -0
  141. package/src/commit/prompts/reduce-user.md +17 -0
  142. package/src/commit/prompts/summary-retry.md +3 -0
  143. package/src/commit/prompts/summary-system.md +38 -0
  144. package/src/commit/prompts/summary-user.md +13 -0
  145. package/src/commit/prompts/types-description.md +2 -0
  146. package/src/commit/types.ts +109 -0
  147. package/src/commit/utils/exclusions.ts +42 -0
  148. package/src/config/file-lock.ts +121 -0
  149. package/src/config/keybindings.ts +280 -0
  150. package/src/config/model-registry.ts +1140 -0
  151. package/src/config/model-resolver.ts +812 -0
  152. package/src/config/prompt-templates.ts +526 -0
  153. package/src/config/resolve-config-value.ts +92 -0
  154. package/src/config/settings-schema.ts +1236 -0
  155. package/src/config/settings.ts +706 -0
  156. package/src/config.ts +414 -0
  157. package/src/cursor.ts +239 -0
  158. package/src/debug/index.ts +431 -0
  159. package/src/debug/log-formatting.ts +60 -0
  160. package/src/debug/log-viewer.ts +903 -0
  161. package/src/debug/profiler.ts +158 -0
  162. package/src/debug/report-bundle.ts +366 -0
  163. package/src/debug/system-info.ts +112 -0
  164. package/src/discovery/agents-md.ts +68 -0
  165. package/src/discovery/agents.ts +199 -0
  166. package/src/discovery/builtin.ts +815 -0
  167. package/src/discovery/claude-plugins.ts +205 -0
  168. package/src/discovery/claude.ts +506 -0
  169. package/src/discovery/cline.ts +83 -0
  170. package/src/discovery/codex.ts +532 -0
  171. package/src/discovery/cursor.ts +218 -0
  172. package/src/discovery/gemini.ts +395 -0
  173. package/src/discovery/github.ts +117 -0
  174. package/src/discovery/helpers.ts +698 -0
  175. package/src/discovery/index.ts +89 -0
  176. package/src/discovery/mcp-json.ts +156 -0
  177. package/src/discovery/opencode.ts +394 -0
  178. package/src/discovery/ssh.ts +160 -0
  179. package/src/discovery/vscode.ts +103 -0
  180. package/src/discovery/windsurf.ts +145 -0
  181. package/src/exa/company.ts +57 -0
  182. package/src/exa/index.ts +62 -0
  183. package/src/exa/linkedin.ts +57 -0
  184. package/src/exa/mcp-client.ts +289 -0
  185. package/src/exa/render.ts +244 -0
  186. package/src/exa/researcher.ts +89 -0
  187. package/src/exa/search.ts +330 -0
  188. package/src/exa/types.ts +166 -0
  189. package/src/exa/websets.ts +247 -0
  190. package/src/exec/bash-executor.ts +184 -0
  191. package/src/exec/exec.ts +53 -0
  192. package/src/export/custom-share.ts +65 -0
  193. package/src/export/html/index.ts +162 -0
  194. package/src/export/html/template.css +889 -0
  195. package/src/export/html/template.generated.ts +2 -0
  196. package/src/export/html/template.html +45 -0
  197. package/src/export/html/template.js +1329 -0
  198. package/src/export/html/template.macro.ts +24 -0
  199. package/src/export/html/vendor/highlight.min.js +1213 -0
  200. package/src/export/html/vendor/marked.min.js +6 -0
  201. package/src/export/ttsr.ts +434 -0
  202. package/src/extensibility/custom-commands/bundled/review/index.ts +433 -0
  203. package/src/extensibility/custom-commands/index.ts +15 -0
  204. package/src/extensibility/custom-commands/loader.ts +231 -0
  205. package/src/extensibility/custom-commands/types.ts +111 -0
  206. package/src/extensibility/custom-tools/index.ts +22 -0
  207. package/src/extensibility/custom-tools/loader.ts +235 -0
  208. package/src/extensibility/custom-tools/types.ts +226 -0
  209. package/src/extensibility/custom-tools/wrapper.ts +45 -0
  210. package/src/extensibility/extensions/index.ts +136 -0
  211. package/src/extensibility/extensions/loader.ts +520 -0
  212. package/src/extensibility/extensions/runner.ts +774 -0
  213. package/src/extensibility/extensions/types.ts +1293 -0
  214. package/src/extensibility/extensions/wrapper.ts +188 -0
  215. package/src/extensibility/hooks/index.ts +16 -0
  216. package/src/extensibility/hooks/loader.ts +273 -0
  217. package/src/extensibility/hooks/runner.ts +441 -0
  218. package/src/extensibility/hooks/tool-wrapper.ts +106 -0
  219. package/src/extensibility/hooks/types.ts +817 -0
  220. package/src/extensibility/plugins/doctor.ts +65 -0
  221. package/src/extensibility/plugins/git-url.ts +281 -0
  222. package/src/extensibility/plugins/index.ts +33 -0
  223. package/src/extensibility/plugins/installer.ts +192 -0
  224. package/src/extensibility/plugins/loader.ts +338 -0
  225. package/src/extensibility/plugins/manager.ts +716 -0
  226. package/src/extensibility/plugins/parser.ts +105 -0
  227. package/src/extensibility/plugins/types.ts +190 -0
  228. package/src/extensibility/skills.ts +385 -0
  229. package/src/extensibility/slash-commands.ts +287 -0
  230. package/src/extensibility/tool-proxy.ts +25 -0
  231. package/src/index.ts +275 -0
  232. package/src/internal-urls/agent-protocol.ts +136 -0
  233. package/src/internal-urls/artifact-protocol.ts +97 -0
  234. package/src/internal-urls/docs-index.generated.ts +54 -0
  235. package/src/internal-urls/docs-protocol.ts +84 -0
  236. package/src/internal-urls/index.ts +31 -0
  237. package/src/internal-urls/json-query.ts +126 -0
  238. package/src/internal-urls/memory-protocol.ts +133 -0
  239. package/src/internal-urls/router.ts +70 -0
  240. package/src/internal-urls/rule-protocol.ts +55 -0
  241. package/src/internal-urls/skill-protocol.ts +111 -0
  242. package/src/internal-urls/types.ts +52 -0
  243. package/src/ipy/executor.ts +556 -0
  244. package/src/ipy/gateway-coordinator.ts +426 -0
  245. package/src/ipy/kernel.ts +892 -0
  246. package/src/ipy/modules.ts +109 -0
  247. package/src/ipy/prelude.py +831 -0
  248. package/src/ipy/prelude.ts +3 -0
  249. package/src/ipy/runtime.ts +222 -0
  250. package/src/lsp/client.ts +867 -0
  251. package/src/lsp/clients/biome-client.ts +202 -0
  252. package/src/lsp/clients/index.ts +50 -0
  253. package/src/lsp/clients/lsp-linter-client.ts +93 -0
  254. package/src/lsp/clients/swiftlint-client.ts +120 -0
  255. package/src/lsp/config.ts +397 -0
  256. package/src/lsp/defaults.json +464 -0
  257. package/src/lsp/edits.ts +109 -0
  258. package/src/lsp/index.ts +1268 -0
  259. package/src/lsp/lspmux.ts +250 -0
  260. package/src/lsp/render.ts +689 -0
  261. package/src/lsp/types.ts +414 -0
  262. package/src/lsp/utils.ts +549 -0
  263. package/src/main.ts +773 -0
  264. package/src/mcp/client.ts +239 -0
  265. package/src/mcp/config-writer.ts +215 -0
  266. package/src/mcp/config.ts +363 -0
  267. package/src/mcp/index.ts +55 -0
  268. package/src/mcp/json-rpc.ts +84 -0
  269. package/src/mcp/loader.ts +124 -0
  270. package/src/mcp/manager.ts +490 -0
  271. package/src/mcp/oauth-discovery.ts +274 -0
  272. package/src/mcp/oauth-flow.ts +229 -0
  273. package/src/mcp/render.ts +123 -0
  274. package/src/mcp/tool-bridge.ts +372 -0
  275. package/src/mcp/tool-cache.ts +121 -0
  276. package/src/mcp/transports/http.ts +332 -0
  277. package/src/mcp/transports/index.ts +6 -0
  278. package/src/mcp/transports/stdio.ts +281 -0
  279. package/src/mcp/types.ts +248 -0
  280. package/src/memories/index.ts +1099 -0
  281. package/src/memories/storage.ts +563 -0
  282. package/src/modes/components/agent-dashboard.ts +1130 -0
  283. package/src/modes/components/assistant-message.ts +144 -0
  284. package/src/modes/components/bash-execution.ts +218 -0
  285. package/src/modes/components/bordered-loader.ts +41 -0
  286. package/src/modes/components/branch-summary-message.ts +45 -0
  287. package/src/modes/components/codemode-group.ts +369 -0
  288. package/src/modes/components/compaction-summary-message.ts +51 -0
  289. package/src/modes/components/countdown-timer.ts +46 -0
  290. package/src/modes/components/custom-editor.ts +181 -0
  291. package/src/modes/components/custom-message.ts +91 -0
  292. package/src/modes/components/diff.ts +186 -0
  293. package/src/modes/components/dynamic-border.ts +25 -0
  294. package/src/modes/components/extensions/extension-dashboard.ts +325 -0
  295. package/src/modes/components/extensions/extension-list.ts +484 -0
  296. package/src/modes/components/extensions/index.ts +9 -0
  297. package/src/modes/components/extensions/inspector-panel.ts +321 -0
  298. package/src/modes/components/extensions/state-manager.ts +586 -0
  299. package/src/modes/components/extensions/types.ts +191 -0
  300. package/src/modes/components/footer.ts +315 -0
  301. package/src/modes/components/history-search.ts +157 -0
  302. package/src/modes/components/hook-editor.ts +101 -0
  303. package/src/modes/components/hook-input.ts +72 -0
  304. package/src/modes/components/hook-message.ts +100 -0
  305. package/src/modes/components/hook-selector.ts +155 -0
  306. package/src/modes/components/index.ts +41 -0
  307. package/src/modes/components/keybinding-hints.ts +65 -0
  308. package/src/modes/components/login-dialog.ts +164 -0
  309. package/src/modes/components/mcp-add-wizard.ts +1295 -0
  310. package/src/modes/components/model-selector.ts +625 -0
  311. package/src/modes/components/oauth-selector.ts +210 -0
  312. package/src/modes/components/plugin-settings.ts +477 -0
  313. package/src/modes/components/python-execution.ts +196 -0
  314. package/src/modes/components/queue-mode-selector.ts +56 -0
  315. package/src/modes/components/read-tool-group.ts +119 -0
  316. package/src/modes/components/session-selector.ts +242 -0
  317. package/src/modes/components/settings-defs.ts +340 -0
  318. package/src/modes/components/settings-selector.ts +529 -0
  319. package/src/modes/components/show-images-selector.ts +45 -0
  320. package/src/modes/components/skill-message.ts +90 -0
  321. package/src/modes/components/status-line/index.ts +4 -0
  322. package/src/modes/components/status-line/presets.ts +94 -0
  323. package/src/modes/components/status-line/segments.ts +352 -0
  324. package/src/modes/components/status-line/separators.ts +55 -0
  325. package/src/modes/components/status-line/types.ts +75 -0
  326. package/src/modes/components/status-line-segment-editor.ts +354 -0
  327. package/src/modes/components/status-line.ts +421 -0
  328. package/src/modes/components/theme-selector.ts +63 -0
  329. package/src/modes/components/thinking-selector.ts +64 -0
  330. package/src/modes/components/todo-display.ts +115 -0
  331. package/src/modes/components/todo-reminder.ts +40 -0
  332. package/src/modes/components/tool-execution.ts +703 -0
  333. package/src/modes/components/tree-selector.ts +904 -0
  334. package/src/modes/components/ttsr-notification.ts +80 -0
  335. package/src/modes/components/user-message-selector.ts +146 -0
  336. package/src/modes/components/user-message.ts +22 -0
  337. package/src/modes/components/visual-truncate.ts +63 -0
  338. package/src/modes/components/welcome.ts +247 -0
  339. package/src/modes/controllers/command-controller.ts +1120 -0
  340. package/src/modes/controllers/event-controller.ts +479 -0
  341. package/src/modes/controllers/extension-ui-controller.ts +778 -0
  342. package/src/modes/controllers/input-controller.ts +671 -0
  343. package/src/modes/controllers/mcp-command-controller.ts +1315 -0
  344. package/src/modes/controllers/selector-controller.ts +712 -0
  345. package/src/modes/controllers/ssh-command-controller.ts +452 -0
  346. package/src/modes/index.ts +15 -0
  347. package/src/modes/interactive-mode.ts +1027 -0
  348. package/src/modes/print-mode.ts +191 -0
  349. package/src/modes/rpc/rpc-client.ts +583 -0
  350. package/src/modes/rpc/rpc-mode.ts +700 -0
  351. package/src/modes/rpc/rpc-types.ts +236 -0
  352. package/src/modes/theme/dark.json +95 -0
  353. package/src/modes/theme/defaults/alabaster.json +93 -0
  354. package/src/modes/theme/defaults/amethyst.json +96 -0
  355. package/src/modes/theme/defaults/anthracite.json +93 -0
  356. package/src/modes/theme/defaults/basalt.json +91 -0
  357. package/src/modes/theme/defaults/birch.json +95 -0
  358. package/src/modes/theme/defaults/dark-abyss.json +91 -0
  359. package/src/modes/theme/defaults/dark-arctic.json +104 -0
  360. package/src/modes/theme/defaults/dark-aurora.json +95 -0
  361. package/src/modes/theme/defaults/dark-catppuccin.json +107 -0
  362. package/src/modes/theme/defaults/dark-cavern.json +91 -0
  363. package/src/modes/theme/defaults/dark-copper.json +95 -0
  364. package/src/modes/theme/defaults/dark-cosmos.json +90 -0
  365. package/src/modes/theme/defaults/dark-cyberpunk.json +102 -0
  366. package/src/modes/theme/defaults/dark-dracula.json +98 -0
  367. package/src/modes/theme/defaults/dark-eclipse.json +91 -0
  368. package/src/modes/theme/defaults/dark-ember.json +95 -0
  369. package/src/modes/theme/defaults/dark-equinox.json +90 -0
  370. package/src/modes/theme/defaults/dark-forest.json +96 -0
  371. package/src/modes/theme/defaults/dark-github.json +105 -0
  372. package/src/modes/theme/defaults/dark-gruvbox.json +112 -0
  373. package/src/modes/theme/defaults/dark-lavender.json +95 -0
  374. package/src/modes/theme/defaults/dark-lunar.json +89 -0
  375. package/src/modes/theme/defaults/dark-midnight.json +95 -0
  376. package/src/modes/theme/defaults/dark-monochrome.json +94 -0
  377. package/src/modes/theme/defaults/dark-monokai.json +98 -0
  378. package/src/modes/theme/defaults/dark-nebula.json +90 -0
  379. package/src/modes/theme/defaults/dark-nord.json +97 -0
  380. package/src/modes/theme/defaults/dark-ocean.json +101 -0
  381. package/src/modes/theme/defaults/dark-one.json +100 -0
  382. package/src/modes/theme/defaults/dark-rainforest.json +91 -0
  383. package/src/modes/theme/defaults/dark-reef.json +91 -0
  384. package/src/modes/theme/defaults/dark-retro.json +92 -0
  385. package/src/modes/theme/defaults/dark-rose-pine.json +96 -0
  386. package/src/modes/theme/defaults/dark-sakura.json +95 -0
  387. package/src/modes/theme/defaults/dark-slate.json +95 -0
  388. package/src/modes/theme/defaults/dark-solarized.json +97 -0
  389. package/src/modes/theme/defaults/dark-solstice.json +90 -0
  390. package/src/modes/theme/defaults/dark-starfall.json +91 -0
  391. package/src/modes/theme/defaults/dark-sunset.json +99 -0
  392. package/src/modes/theme/defaults/dark-swamp.json +90 -0
  393. package/src/modes/theme/defaults/dark-synthwave.json +103 -0
  394. package/src/modes/theme/defaults/dark-taiga.json +91 -0
  395. package/src/modes/theme/defaults/dark-terminal.json +95 -0
  396. package/src/modes/theme/defaults/dark-tokyo-night.json +101 -0
  397. package/src/modes/theme/defaults/dark-tundra.json +91 -0
  398. package/src/modes/theme/defaults/dark-twilight.json +91 -0
  399. package/src/modes/theme/defaults/dark-volcanic.json +91 -0
  400. package/src/modes/theme/defaults/graphite.json +92 -0
  401. package/src/modes/theme/defaults/index.ts +195 -0
  402. package/src/modes/theme/defaults/light-arctic.json +107 -0
  403. package/src/modes/theme/defaults/light-aurora-day.json +91 -0
  404. package/src/modes/theme/defaults/light-canyon.json +91 -0
  405. package/src/modes/theme/defaults/light-catppuccin.json +106 -0
  406. package/src/modes/theme/defaults/light-cirrus.json +90 -0
  407. package/src/modes/theme/defaults/light-coral.json +95 -0
  408. package/src/modes/theme/defaults/light-cyberpunk.json +96 -0
  409. package/src/modes/theme/defaults/light-dawn.json +90 -0
  410. package/src/modes/theme/defaults/light-dunes.json +91 -0
  411. package/src/modes/theme/defaults/light-eucalyptus.json +95 -0
  412. package/src/modes/theme/defaults/light-forest.json +100 -0
  413. package/src/modes/theme/defaults/light-frost.json +95 -0
  414. package/src/modes/theme/defaults/light-github.json +115 -0
  415. package/src/modes/theme/defaults/light-glacier.json +91 -0
  416. package/src/modes/theme/defaults/light-gruvbox.json +108 -0
  417. package/src/modes/theme/defaults/light-haze.json +90 -0
  418. package/src/modes/theme/defaults/light-honeycomb.json +95 -0
  419. package/src/modes/theme/defaults/light-lagoon.json +91 -0
  420. package/src/modes/theme/defaults/light-lavender.json +95 -0
  421. package/src/modes/theme/defaults/light-meadow.json +91 -0
  422. package/src/modes/theme/defaults/light-mint.json +95 -0
  423. package/src/modes/theme/defaults/light-monochrome.json +101 -0
  424. package/src/modes/theme/defaults/light-ocean.json +99 -0
  425. package/src/modes/theme/defaults/light-one.json +99 -0
  426. package/src/modes/theme/defaults/light-opal.json +91 -0
  427. package/src/modes/theme/defaults/light-orchard.json +91 -0
  428. package/src/modes/theme/defaults/light-paper.json +95 -0
  429. package/src/modes/theme/defaults/light-prism.json +90 -0
  430. package/src/modes/theme/defaults/light-retro.json +98 -0
  431. package/src/modes/theme/defaults/light-sand.json +95 -0
  432. package/src/modes/theme/defaults/light-savanna.json +91 -0
  433. package/src/modes/theme/defaults/light-solarized.json +102 -0
  434. package/src/modes/theme/defaults/light-soleil.json +90 -0
  435. package/src/modes/theme/defaults/light-sunset.json +99 -0
  436. package/src/modes/theme/defaults/light-synthwave.json +98 -0
  437. package/src/modes/theme/defaults/light-tokyo-night.json +111 -0
  438. package/src/modes/theme/defaults/light-wetland.json +91 -0
  439. package/src/modes/theme/defaults/light-zenith.json +89 -0
  440. package/src/modes/theme/defaults/limestone.json +94 -0
  441. package/src/modes/theme/defaults/mahogany.json +97 -0
  442. package/src/modes/theme/defaults/marble.json +93 -0
  443. package/src/modes/theme/defaults/obsidian.json +91 -0
  444. package/src/modes/theme/defaults/onyx.json +91 -0
  445. package/src/modes/theme/defaults/pearl.json +93 -0
  446. package/src/modes/theme/defaults/porcelain.json +91 -0
  447. package/src/modes/theme/defaults/quartz.json +96 -0
  448. package/src/modes/theme/defaults/sandstone.json +95 -0
  449. package/src/modes/theme/defaults/titanium.json +90 -0
  450. package/src/modes/theme/light.json +93 -0
  451. package/src/modes/theme/mermaid-cache.ts +111 -0
  452. package/src/modes/theme/theme-schema.json +429 -0
  453. package/src/modes/theme/theme.ts +2333 -0
  454. package/src/modes/types.ts +216 -0
  455. package/src/modes/utils/ui-helpers.ts +529 -0
  456. package/src/patch/applicator.ts +1482 -0
  457. package/src/patch/diff.ts +425 -0
  458. package/src/patch/fuzzy.ts +784 -0
  459. package/src/patch/hashline.ts +972 -0
  460. package/src/patch/index.ts +964 -0
  461. package/src/patch/normalize.ts +397 -0
  462. package/src/patch/normative.ts +72 -0
  463. package/src/patch/parser.ts +532 -0
  464. package/src/patch/shared.ts +400 -0
  465. package/src/patch/types.ts +292 -0
  466. package/src/priority.json +35 -0
  467. package/src/prompts/agents/explore.md +48 -0
  468. package/src/prompts/agents/frontmatter.md +9 -0
  469. package/src/prompts/agents/init.md +36 -0
  470. package/src/prompts/agents/librarian.md +53 -0
  471. package/src/prompts/agents/oracle.md +51 -0
  472. package/src/prompts/agents/reviewer.md +70 -0
  473. package/src/prompts/agents/task.md +14 -0
  474. package/src/prompts/compaction/branch-summary-context.md +5 -0
  475. package/src/prompts/compaction/branch-summary-preamble.md +2 -0
  476. package/src/prompts/compaction/branch-summary.md +30 -0
  477. package/src/prompts/compaction/compaction-short-summary.md +9 -0
  478. package/src/prompts/compaction/compaction-summary-context.md +5 -0
  479. package/src/prompts/compaction/compaction-summary.md +38 -0
  480. package/src/prompts/compaction/compaction-turn-prefix.md +17 -0
  481. package/src/prompts/compaction/compaction-update-summary.md +45 -0
  482. package/src/prompts/memories/consolidation.md +30 -0
  483. package/src/prompts/memories/read_path.md +11 -0
  484. package/src/prompts/memories/stage_one_input.md +6 -0
  485. package/src/prompts/memories/stage_one_system.md +21 -0
  486. package/src/prompts/review-request.md +64 -0
  487. package/src/prompts/system/agent-creation-architect.md +65 -0
  488. package/src/prompts/system/agent-creation-user.md +6 -0
  489. package/src/prompts/system/custom-system-prompt.md +68 -0
  490. package/src/prompts/system/file-operations.md +10 -0
  491. package/src/prompts/system/subagent-submit-reminder.md +11 -0
  492. package/src/prompts/system/subagent-system-prompt.md +31 -0
  493. package/src/prompts/system/subagent-user-prompt.md +8 -0
  494. package/src/prompts/system/summarization-system.md +3 -0
  495. package/src/prompts/system/system-prompt.md +300 -0
  496. package/src/prompts/system/title-system.md +2 -0
  497. package/src/prompts/system/ttsr-interrupt.md +7 -0
  498. package/src/prompts/system/web-search.md +28 -0
  499. package/src/prompts/tools/ask.md +44 -0
  500. package/src/prompts/tools/bash.md +24 -0
  501. package/src/prompts/tools/browser.md +33 -0
  502. package/src/prompts/tools/calculator.md +12 -0
  503. package/src/prompts/tools/explore.md +29 -0
  504. package/src/prompts/tools/fetch.md +16 -0
  505. package/src/prompts/tools/find.md +18 -0
  506. package/src/prompts/tools/gemini-image.md +23 -0
  507. package/src/prompts/tools/grep.md +28 -0
  508. package/src/prompts/tools/hashline.md +232 -0
  509. package/src/prompts/tools/librarian.md +24 -0
  510. package/src/prompts/tools/lsp.md +28 -0
  511. package/src/prompts/tools/oracle.md +26 -0
  512. package/src/prompts/tools/patch.md +74 -0
  513. package/src/prompts/tools/python.md +66 -0
  514. package/src/prompts/tools/read.md +36 -0
  515. package/src/prompts/tools/replace.md +38 -0
  516. package/src/prompts/tools/reviewer.md +41 -0
  517. package/src/prompts/tools/ssh.md +51 -0
  518. package/src/prompts/tools/task-summary.md +28 -0
  519. package/src/prompts/tools/task.md +275 -0
  520. package/src/prompts/tools/todo-write.md +65 -0
  521. package/src/prompts/tools/undo-edit.md +7 -0
  522. package/src/prompts/tools/web-search.md +19 -0
  523. package/src/prompts/tools/write.md +18 -0
  524. package/src/sdk.ts +1287 -0
  525. package/src/secrets/index.ts +116 -0
  526. package/src/secrets/obfuscator.ts +269 -0
  527. package/src/secrets/regex.ts +21 -0
  528. package/src/session/agent-session.ts +4669 -0
  529. package/src/session/agent-storage.ts +621 -0
  530. package/src/session/artifacts.ts +132 -0
  531. package/src/session/auth-storage.ts +1433 -0
  532. package/src/session/blob-store.ts +103 -0
  533. package/src/session/compaction/branch-summarization.ts +315 -0
  534. package/src/session/compaction/compaction.ts +864 -0
  535. package/src/session/compaction/index.ts +7 -0
  536. package/src/session/compaction/pruning.ts +91 -0
  537. package/src/session/compaction/utils.ts +171 -0
  538. package/src/session/history-storage.ts +170 -0
  539. package/src/session/messages.ts +317 -0
  540. package/src/session/session-manager.ts +2276 -0
  541. package/src/session/session-storage.ts +342 -0
  542. package/src/session/streaming-output.ts +565 -0
  543. package/src/slash-commands/builtin-registry.ts +439 -0
  544. package/src/ssh/config-writer.ts +183 -0
  545. package/src/ssh/connection-manager.ts +444 -0
  546. package/src/ssh/ssh-executor.ts +127 -0
  547. package/src/ssh/sshfs-mount.ts +135 -0
  548. package/src/stt/downloader.ts +71 -0
  549. package/src/stt/index.ts +3 -0
  550. package/src/stt/recorder.ts +351 -0
  551. package/src/stt/setup.ts +52 -0
  552. package/src/stt/stt-controller.ts +160 -0
  553. package/src/stt/transcribe.py +70 -0
  554. package/src/stt/transcriber.ts +91 -0
  555. package/src/system-prompt.ts +685 -0
  556. package/src/task/agents.ts +155 -0
  557. package/src/task/batch.ts +102 -0
  558. package/src/task/commands.ts +134 -0
  559. package/src/task/discovery.ts +126 -0
  560. package/src/task/executor.ts +908 -0
  561. package/src/task/index.ts +223 -0
  562. package/src/task/output-manager.ts +107 -0
  563. package/src/task/parallel.ts +84 -0
  564. package/src/task/render.ts +326 -0
  565. package/src/task/subprocess-tool-registry.ts +88 -0
  566. package/src/task/template.ts +32 -0
  567. package/src/task/types.ts +144 -0
  568. package/src/tools/ask.ts +523 -0
  569. package/src/tools/bash-interactive.ts +419 -0
  570. package/src/tools/bash-interceptor.ts +105 -0
  571. package/src/tools/bash-normalize.ts +107 -0
  572. package/src/tools/bash-skill-urls.ts +177 -0
  573. package/src/tools/bash.ts +347 -0
  574. package/src/tools/browser.ts +1374 -0
  575. package/src/tools/calculator.ts +537 -0
  576. package/src/tools/context.ts +39 -0
  577. package/src/tools/explore.ts +23 -0
  578. package/src/tools/fetch.ts +1091 -0
  579. package/src/tools/find.ts +540 -0
  580. package/src/tools/fs-cache-invalidation.ts +28 -0
  581. package/src/tools/gemini-image.ts +907 -0
  582. package/src/tools/grep.ts +489 -0
  583. package/src/tools/index.ts +337 -0
  584. package/src/tools/json-tree.ts +231 -0
  585. package/src/tools/jtd-to-json-schema.ts +247 -0
  586. package/src/tools/jtd-to-typescript.ts +198 -0
  587. package/src/tools/librarian.ts +33 -0
  588. package/src/tools/list-limit.ts +40 -0
  589. package/src/tools/notebook.ts +287 -0
  590. package/src/tools/oracle.ts +40 -0
  591. package/src/tools/output-meta.ts +459 -0
  592. package/src/tools/output-utils.ts +63 -0
  593. package/src/tools/path-utils.ts +116 -0
  594. package/src/tools/puppeteer/00_stealth_tampering.txt +63 -0
  595. package/src/tools/puppeteer/01_stealth_activity.txt +20 -0
  596. package/src/tools/puppeteer/02_stealth_hairline.txt +11 -0
  597. package/src/tools/puppeteer/03_stealth_botd.txt +384 -0
  598. package/src/tools/puppeteer/04_stealth_iframe.txt +81 -0
  599. package/src/tools/puppeteer/05_stealth_webgl.txt +75 -0
  600. package/src/tools/puppeteer/06_stealth_screen.txt +72 -0
  601. package/src/tools/puppeteer/07_stealth_fonts.txt +97 -0
  602. package/src/tools/puppeteer/08_stealth_audio.txt +51 -0
  603. package/src/tools/puppeteer/09_stealth_locale.txt +46 -0
  604. package/src/tools/puppeteer/10_stealth_plugins.txt +206 -0
  605. package/src/tools/puppeteer/11_stealth_hardware.txt +8 -0
  606. package/src/tools/puppeteer/12_stealth_codecs.txt +40 -0
  607. package/src/tools/puppeteer/13_stealth_worker.txt +74 -0
  608. package/src/tools/python.ts +1118 -0
  609. package/src/tools/read.ts +1193 -0
  610. package/src/tools/render-utils.ts +680 -0
  611. package/src/tools/renderers.ts +60 -0
  612. package/src/tools/reviewer-tool.ts +41 -0
  613. package/src/tools/ssh.ts +326 -0
  614. package/src/tools/subagent-tool.ts +169 -0
  615. package/src/tools/submit-result.ts +152 -0
  616. package/src/tools/todo-write.ts +255 -0
  617. package/src/tools/tool-errors.ts +92 -0
  618. package/src/tools/tool-result.ts +86 -0
  619. package/src/tools/undo-edit.ts +145 -0
  620. package/src/tools/undo-history.ts +22 -0
  621. package/src/tools/write.ts +274 -0
  622. package/src/tui/code-cell.ts +108 -0
  623. package/src/tui/file-list.ts +47 -0
  624. package/src/tui/index.ts +11 -0
  625. package/src/tui/output-block.ts +144 -0
  626. package/src/tui/status-line.ts +39 -0
  627. package/src/tui/tree-list.ts +53 -0
  628. package/src/tui/types.ts +16 -0
  629. package/src/tui/utils.ts +116 -0
  630. package/src/utils/changelog.ts +98 -0
  631. package/src/utils/event-bus.ts +33 -0
  632. package/src/utils/external-editor.ts +59 -0
  633. package/src/utils/file-display-mode.ts +36 -0
  634. package/src/utils/file-mentions.ts +384 -0
  635. package/src/utils/frontmatter.ts +101 -0
  636. package/src/utils/fuzzy.ts +108 -0
  637. package/src/utils/ignore-files.ts +119 -0
  638. package/src/utils/image-convert.ts +27 -0
  639. package/src/utils/image-resize.ts +236 -0
  640. package/src/utils/mime.ts +30 -0
  641. package/src/utils/open.ts +20 -0
  642. package/src/utils/shell-snapshot.ts +199 -0
  643. package/src/utils/timings.ts +26 -0
  644. package/src/utils/title-generator.ts +167 -0
  645. package/src/utils/tools-manager.ts +362 -0
  646. package/src/web/scrapers/artifacthub.ts +215 -0
  647. package/src/web/scrapers/arxiv.ts +88 -0
  648. package/src/web/scrapers/aur.ts +175 -0
  649. package/src/web/scrapers/biorxiv.ts +141 -0
  650. package/src/web/scrapers/bluesky.ts +284 -0
  651. package/src/web/scrapers/brew.ts +177 -0
  652. package/src/web/scrapers/cheatsh.ts +78 -0
  653. package/src/web/scrapers/chocolatey.ts +158 -0
  654. package/src/web/scrapers/choosealicense.ts +110 -0
  655. package/src/web/scrapers/cisa-kev.ts +100 -0
  656. package/src/web/scrapers/clojars.ts +180 -0
  657. package/src/web/scrapers/coingecko.ts +184 -0
  658. package/src/web/scrapers/crates-io.ts +128 -0
  659. package/src/web/scrapers/crossref.ts +149 -0
  660. package/src/web/scrapers/devto.ts +177 -0
  661. package/src/web/scrapers/discogs.ts +307 -0
  662. package/src/web/scrapers/discourse.ts +221 -0
  663. package/src/web/scrapers/dockerhub.ts +160 -0
  664. package/src/web/scrapers/fdroid.ts +158 -0
  665. package/src/web/scrapers/firefox-addons.ts +214 -0
  666. package/src/web/scrapers/flathub.ts +239 -0
  667. package/src/web/scrapers/github-gist.ts +68 -0
  668. package/src/web/scrapers/github.ts +490 -0
  669. package/src/web/scrapers/gitlab.ts +456 -0
  670. package/src/web/scrapers/go-pkg.ts +275 -0
  671. package/src/web/scrapers/hackage.ts +94 -0
  672. package/src/web/scrapers/hackernews.ts +208 -0
  673. package/src/web/scrapers/hex.ts +121 -0
  674. package/src/web/scrapers/huggingface.ts +385 -0
  675. package/src/web/scrapers/iacr.ts +86 -0
  676. package/src/web/scrapers/index.ts +249 -0
  677. package/src/web/scrapers/jetbrains-marketplace.ts +169 -0
  678. package/src/web/scrapers/lemmy.ts +220 -0
  679. package/src/web/scrapers/lobsters.ts +186 -0
  680. package/src/web/scrapers/mastodon.ts +310 -0
  681. package/src/web/scrapers/maven.ts +152 -0
  682. package/src/web/scrapers/mdn.ts +172 -0
  683. package/src/web/scrapers/metacpan.ts +253 -0
  684. package/src/web/scrapers/musicbrainz.ts +272 -0
  685. package/src/web/scrapers/npm.ts +114 -0
  686. package/src/web/scrapers/nuget.ts +205 -0
  687. package/src/web/scrapers/nvd.ts +243 -0
  688. package/src/web/scrapers/ollama.ts +265 -0
  689. package/src/web/scrapers/open-vsx.ts +119 -0
  690. package/src/web/scrapers/opencorporates.ts +275 -0
  691. package/src/web/scrapers/openlibrary.ts +319 -0
  692. package/src/web/scrapers/orcid.ts +298 -0
  693. package/src/web/scrapers/osv.ts +192 -0
  694. package/src/web/scrapers/packagist.ts +174 -0
  695. package/src/web/scrapers/pub-dev.ts +185 -0
  696. package/src/web/scrapers/pubmed.ts +177 -0
  697. package/src/web/scrapers/pypi.ts +129 -0
  698. package/src/web/scrapers/rawg.ts +124 -0
  699. package/src/web/scrapers/readthedocs.ts +125 -0
  700. package/src/web/scrapers/reddit.ts +104 -0
  701. package/src/web/scrapers/repology.ts +262 -0
  702. package/src/web/scrapers/rfc.ts +209 -0
  703. package/src/web/scrapers/rubygems.ts +117 -0
  704. package/src/web/scrapers/searchcode.ts +217 -0
  705. package/src/web/scrapers/sec-edgar.ts +274 -0
  706. package/src/web/scrapers/semantic-scholar.ts +190 -0
  707. package/src/web/scrapers/snapcraft.ts +200 -0
  708. package/src/web/scrapers/sourcegraph.ts +373 -0
  709. package/src/web/scrapers/spdx.ts +121 -0
  710. package/src/web/scrapers/spotify.ts +217 -0
  711. package/src/web/scrapers/stackoverflow.ts +124 -0
  712. package/src/web/scrapers/terraform.ts +304 -0
  713. package/src/web/scrapers/tldr.ts +51 -0
  714. package/src/web/scrapers/twitter.ts +97 -0
  715. package/src/web/scrapers/types.ts +200 -0
  716. package/src/web/scrapers/utils.ts +142 -0
  717. package/src/web/scrapers/vimeo.ts +152 -0
  718. package/src/web/scrapers/vscode-marketplace.ts +195 -0
  719. package/src/web/scrapers/w3c.ts +163 -0
  720. package/src/web/scrapers/wikidata.ts +357 -0
  721. package/src/web/scrapers/wikipedia.ts +95 -0
  722. package/src/web/scrapers/youtube.ts +312 -0
  723. package/src/web/search/auth.ts +178 -0
  724. package/src/web/search/index.ts +598 -0
  725. package/src/web/search/provider.ts +77 -0
  726. package/src/web/search/providers/anthropic.ts +284 -0
  727. package/src/web/search/providers/base.ts +22 -0
  728. package/src/web/search/providers/brave.ts +165 -0
  729. package/src/web/search/providers/codex.ts +377 -0
  730. package/src/web/search/providers/exa.ts +158 -0
  731. package/src/web/search/providers/gemini.ts +437 -0
  732. package/src/web/search/providers/jina.ts +99 -0
  733. package/src/web/search/providers/kimi.ts +196 -0
  734. package/src/web/search/providers/perplexity.ts +546 -0
  735. package/src/web/search/providers/synthetic.ts +136 -0
  736. package/src/web/search/providers/zai.ts +352 -0
  737. package/src/web/search/render.ts +299 -0
  738. package/src/web/search/types.ts +437 -0
@@ -0,0 +1,972 @@
1
+ /**
2
+ * Hashline edit mode — a line-addressable edit format using content hashes.
3
+ *
4
+ * Each line in a file is identified by its 1-indexed line number and a short
5
+ * hexadecimal hash derived from the normalized line content (xxHash32, truncated to 2
6
+ * hex chars).
7
+ * The combined `LINE#ID` reference acts as both an address and a staleness check:
8
+ * if the file has changed since the caller last read it, hash mismatches are caught
9
+ * before any mutation occurs.
10
+ *
11
+ * Displayed format: `LINENUM#HASH:CONTENT`
12
+ * Reference format: `"LINENUM#HASH"` (e.g. `"5#aa"`)
13
+ */
14
+
15
+ import type { HashMismatch } from "./types";
16
+
17
+ export type LineTag = { line: number; hash: string };
18
+ export type HashlineEdit =
19
+ | { op: "set"; tag: LineTag; content: string[] }
20
+ | { op: "replace"; first: LineTag; last: LineTag; content: string[] }
21
+ | { op: "append"; after?: LineTag; content: string[] }
22
+ | { op: "prepend"; before?: LineTag; content: string[] }
23
+ | { op: "insert"; after: LineTag; before: LineTag; content: string[] };
24
+ export type ReplaceTextEdit = { op: "replaceText"; old_text: string; new_text: string; all?: boolean };
25
+ export type EditSpec = HashlineEdit | ReplaceTextEdit;
26
+
27
+ /**
28
+ * Compare two strings ignoring all whitespace differences.
29
+ *
30
+ * Returns true when the non-whitespace characters are identical — meaning
31
+ * the only differences are in spaces, tabs, or other whitespace.
32
+ */
33
+ function equalsIgnoringWhitespace(a: string, b: string): boolean {
34
+ // Fast path: identical strings
35
+ if (a === b) return true;
36
+ // Compare with all whitespace removed
37
+ return a.replace(/\s+/g, "") === b.replace(/\s+/g, "");
38
+ }
39
+
40
+ function stripAllWhitespace(s: string): string {
41
+ return s.replace(/\s+/g, "");
42
+ }
43
+
44
+ function stripTrailingContinuationTokens(s: string): string {
45
+ // Heuristic: models often merge a continuation line into the prior line
46
+ // while also changing the trailing operator (e.g. `&&` → `||`).
47
+ // Strip common trailing continuation tokens so we can still detect merges.
48
+ return s.replace(/(?:&&|\|\||\?\?|\?|:|=|,|\+|-|\*|\/|\.|\()\s*$/u, "");
49
+ }
50
+
51
+ function stripMergeOperatorChars(s: string): string {
52
+ // Used for merge detection when the model changes a logical operator like
53
+ // `||` → `??` while also merging adjacent lines.
54
+ return s.replace(/[|&?]/g, "");
55
+ }
56
+
57
+ function leadingWhitespace(s: string): string {
58
+ const match = s.match(/^\s*/);
59
+ return match ? match[0] : "";
60
+ }
61
+
62
+ function restoreLeadingIndent(templateLine: string, line: string): string {
63
+ if (line.length === 0) return line;
64
+ const templateIndent = leadingWhitespace(templateLine);
65
+ if (templateIndent.length === 0) return line;
66
+ const indent = leadingWhitespace(line);
67
+ if (indent.length > 0) return line;
68
+ return templateIndent + line;
69
+ }
70
+
71
+ function restoreIndentForPairedReplacement(oldLines: string[], newLines: string[]): string[] {
72
+ if (oldLines.length !== newLines.length) return newLines;
73
+ let changed = false;
74
+ const out = new Array<string>(newLines.length);
75
+ for (let i = 0; i < newLines.length; i++) {
76
+ const restored = restoreLeadingIndent(oldLines[i], newLines[i]);
77
+ out[i] = restored;
78
+ if (restored !== newLines[i]) changed = true;
79
+ }
80
+ return changed ? out : newLines;
81
+ }
82
+
83
+ /**
84
+ * Undo pure formatting rewrites where the model reflows a single logical line
85
+ * into multiple lines (or similar), but the token stream is identical.
86
+ */
87
+ function restoreOldWrappedLines(oldLines: string[], newLines: string[]): string[] {
88
+ if (oldLines.length === 0 || newLines.length < 2) return newLines;
89
+
90
+ const canonToOld = new Map<string, { line: string; count: number }>();
91
+ for (const line of oldLines) {
92
+ const canon = stripAllWhitespace(line);
93
+ const bucket = canonToOld.get(canon);
94
+ if (bucket) bucket.count++;
95
+ else canonToOld.set(canon, { line, count: 1 });
96
+ }
97
+
98
+ const candidates: { start: number; len: number; replacement: string; canon: string }[] = [];
99
+ for (let start = 0; start < newLines.length; start++) {
100
+ for (let len = 2; len <= 10 && start + len <= newLines.length; len++) {
101
+ const canonSpan = stripAllWhitespace(newLines.slice(start, start + len).join(""));
102
+ const old = canonToOld.get(canonSpan);
103
+ if (old && old.count === 1 && canonSpan.length >= 6) {
104
+ candidates.push({ start, len, replacement: old.line, canon: canonSpan });
105
+ }
106
+ }
107
+ }
108
+ if (candidates.length === 0) return newLines;
109
+
110
+ // Keep only spans whose canonical match is unique in the new output.
111
+ const canonCounts = new Map<string, number>();
112
+ for (const c of candidates) {
113
+ canonCounts.set(c.canon, (canonCounts.get(c.canon) ?? 0) + 1);
114
+ }
115
+ const uniqueCandidates = candidates.filter(c => (canonCounts.get(c.canon) ?? 0) === 1);
116
+ if (uniqueCandidates.length === 0) return newLines;
117
+
118
+ // Apply replacements back-to-front so indices remain stable.
119
+ uniqueCandidates.sort((a, b) => b.start - a.start);
120
+ const out = [...newLines];
121
+ for (const c of uniqueCandidates) {
122
+ out.splice(c.start, c.len, c.replacement);
123
+ }
124
+ return out;
125
+ }
126
+
127
+ function stripInsertAnchorEchoAfter(anchorLine: string, dstLines: string[]): string[] {
128
+ if (dstLines.length <= 1) return dstLines;
129
+ if (equalsIgnoringWhitespace(dstLines[0], anchorLine)) {
130
+ return dstLines.slice(1);
131
+ }
132
+ return dstLines;
133
+ }
134
+
135
+ function stripInsertAnchorEchoBefore(anchorLine: string, dstLines: string[]): string[] {
136
+ if (dstLines.length <= 1) return dstLines;
137
+ if (equalsIgnoringWhitespace(dstLines[dstLines.length - 1], anchorLine)) {
138
+ return dstLines.slice(0, -1);
139
+ }
140
+ return dstLines;
141
+ }
142
+
143
+ function stripInsertBoundaryEcho(afterLine: string, beforeLine: string, dstLines: string[]): string[] {
144
+ let out = dstLines;
145
+ if (out.length > 1 && equalsIgnoringWhitespace(out[0], afterLine)) {
146
+ out = out.slice(1);
147
+ }
148
+ if (out.length > 1 && equalsIgnoringWhitespace(out[out.length - 1], beforeLine)) {
149
+ out = out.slice(0, -1);
150
+ }
151
+ return out;
152
+ }
153
+
154
+ function stripRangeBoundaryEcho(fileLines: string[], startLine: number, endLine: number, dstLines: string[]): string[] {
155
+ // Only strip when the model replaced with multiple lines and grew the edit.
156
+ // This avoids turning a single-line replacement into a deletion.
157
+ const count = endLine - startLine + 1;
158
+ if (dstLines.length <= 1 || dstLines.length <= count) return dstLines;
159
+
160
+ let out = dstLines;
161
+ const beforeIdx = startLine - 2;
162
+ if (beforeIdx >= 0 && equalsIgnoringWhitespace(out[0], fileLines[beforeIdx])) {
163
+ out = out.slice(1);
164
+ }
165
+
166
+ const afterIdx = endLine;
167
+ if (
168
+ afterIdx < fileLines.length &&
169
+ out.length > 0 &&
170
+ equalsIgnoringWhitespace(out[out.length - 1], fileLines[afterIdx])
171
+ ) {
172
+ out = out.slice(0, -1);
173
+ }
174
+
175
+ return out;
176
+ }
177
+
178
+ const NIBBLE_STR = "ZPMQVRWSNKTXJBYH";
179
+
180
+ const DICT = Array.from({ length: 256 }, (_, i) => {
181
+ const h = i >>> 4;
182
+ const l = i & 0x0f;
183
+ return `${NIBBLE_STR[h]}${NIBBLE_STR[l]}`;
184
+ });
185
+
186
+ /**
187
+ * Compute a short hexadecimal hash of a single line.
188
+ *
189
+ * Uses xxHash32 on a whitespace-normalized line, truncated to {@link HASH_LEN}
190
+ * hex characters. The `idx` parameter is accepted for compatibility with older
191
+ * call sites, but is not currently mixed into the hash.
192
+ * The line input should not include a trailing newline.
193
+ */
194
+ export function computeLineHash(idx: number, line: string): string {
195
+ if (line.endsWith("\r")) {
196
+ line = line.slice(0, -1);
197
+ }
198
+ line = line.replace(/\s+/g, "");
199
+ void idx; // Might use line, but for now, let's not.
200
+ return DICT[Bun.hash.xxHash32(line) & 0xff];
201
+ }
202
+
203
+ /**
204
+ * Formats a tag given the line number and content.
205
+ */
206
+ export function formatLineTag(line: number, content: string): string {
207
+ return `${line}#${computeLineHash(line, content)}`;
208
+ }
209
+
210
+ /**
211
+ * Format file content with hashline prefixes for display.
212
+ *
213
+ * Each line becomes `LINENUM#HASH:CONTENT` where LINENUM is 1-indexed.
214
+ *
215
+ * @param content - Raw file content string
216
+ * @param startLine - First line number (1-indexed, defaults to 1)
217
+ * @returns Formatted string with one hashline-prefixed line per input line
218
+ *
219
+ * @example
220
+ * ```
221
+ * formatHashLines("function hi() {\n return;\n}")
222
+ * // "1#HH:function hi() {\n2#HH: return;\n3#HH:}"
223
+ * ```
224
+ */
225
+ export function formatHashLines(content: string, startLine = 1): string {
226
+ const lines = content.split("\n");
227
+ return lines
228
+ .map((line, i) => {
229
+ const num = startLine + i;
230
+ return `${formatLineTag(num, line)}:${line}`;
231
+ })
232
+ .join("\n");
233
+ }
234
+
235
+ // ═══════════════════════════════════════════════════════════════════════════
236
+ // Hashline streaming formatter
237
+ // ═══════════════════════════════════════════════════════════════════════════
238
+
239
+ export interface HashlineStreamOptions {
240
+ /** First line number to use when formatting (1-indexed). */
241
+ startLine?: number;
242
+ /** Maximum formatted lines per yielded chunk (default: 200). */
243
+ maxChunkLines?: number;
244
+ /** Maximum UTF-8 bytes per yielded chunk (default: 64 KiB). */
245
+ maxChunkBytes?: number;
246
+ }
247
+
248
+ function isReadableStream(value: unknown): value is ReadableStream<Uint8Array> {
249
+ return (
250
+ typeof value === "object" &&
251
+ value !== null &&
252
+ "getReader" in value &&
253
+ typeof (value as { getReader?: unknown }).getReader === "function"
254
+ );
255
+ }
256
+
257
+ async function* bytesFromReadableStream(stream: ReadableStream<Uint8Array>): AsyncGenerator<Uint8Array> {
258
+ const reader = stream.getReader();
259
+ try {
260
+ while (true) {
261
+ const { done, value } = await reader.read();
262
+ if (done) return;
263
+ if (value) yield value;
264
+ }
265
+ } finally {
266
+ reader.releaseLock();
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Stream hashline-formatted output from a UTF-8 byte source.
272
+ *
273
+ * This is intended for large files where callers want incremental output
274
+ * (e.g. while reading from a file handle) rather than allocating a single
275
+ * large string.
276
+ */
277
+ export async function* streamHashLinesFromUtf8(
278
+ source: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
279
+ options: HashlineStreamOptions = {},
280
+ ): AsyncGenerator<string> {
281
+ const startLine = options.startLine ?? 1;
282
+ const maxChunkLines = options.maxChunkLines ?? 200;
283
+ const maxChunkBytes = options.maxChunkBytes ?? 64 * 1024;
284
+ const decoder = new TextDecoder("utf-8");
285
+ const chunks = isReadableStream(source) ? bytesFromReadableStream(source) : source;
286
+ let lineNum = startLine;
287
+ let pending = "";
288
+ let sawAnyText = false;
289
+ let endedWithNewline = false;
290
+ let outLines: string[] = [];
291
+ let outBytes = 0;
292
+
293
+ const flush = (): string | undefined => {
294
+ if (outLines.length === 0) return undefined;
295
+ const chunk = outLines.join("\n");
296
+ outLines = [];
297
+ outBytes = 0;
298
+ return chunk;
299
+ };
300
+
301
+ const pushLine = (line: string): string[] => {
302
+ const formatted = `${lineNum}#${computeLineHash(lineNum, line)}:${line}`;
303
+ lineNum++;
304
+
305
+ const chunksToYield: string[] = [];
306
+ const sepBytes = outLines.length === 0 ? 0 : 1; // "\n"
307
+ const lineBytes = Buffer.byteLength(formatted, "utf-8");
308
+
309
+ if (
310
+ outLines.length > 0 &&
311
+ (outLines.length >= maxChunkLines || outBytes + sepBytes + lineBytes > maxChunkBytes)
312
+ ) {
313
+ const flushed = flush();
314
+ if (flushed) chunksToYield.push(flushed);
315
+ }
316
+
317
+ outLines.push(formatted);
318
+ outBytes += (outLines.length === 1 ? 0 : 1) + lineBytes;
319
+
320
+ if (outLines.length >= maxChunkLines || outBytes >= maxChunkBytes) {
321
+ const flushed = flush();
322
+ if (flushed) chunksToYield.push(flushed);
323
+ }
324
+
325
+ return chunksToYield;
326
+ };
327
+
328
+ const consumeText = (text: string): string[] => {
329
+ if (text.length === 0) return [];
330
+ sawAnyText = true;
331
+ pending += text;
332
+ const chunksToYield: string[] = [];
333
+ while (true) {
334
+ const idx = pending.indexOf("\n");
335
+ if (idx === -1) break;
336
+ const line = pending.slice(0, idx);
337
+ pending = pending.slice(idx + 1);
338
+ endedWithNewline = true;
339
+ chunksToYield.push(...pushLine(line));
340
+ }
341
+ if (pending.length > 0) endedWithNewline = false;
342
+ return chunksToYield;
343
+ };
344
+ for await (const chunk of chunks) {
345
+ for (const out of consumeText(decoder.decode(chunk, { stream: true }))) {
346
+ yield out;
347
+ }
348
+ }
349
+
350
+ for (const out of consumeText(decoder.decode())) {
351
+ yield out;
352
+ }
353
+ if (!sawAnyText) {
354
+ // Mirror `"".split("\n")` behavior: one empty line.
355
+ for (const out of pushLine("")) {
356
+ yield out;
357
+ }
358
+ } else if (pending.length > 0 || endedWithNewline) {
359
+ // Emit the final line (may be empty if the file ended with a newline).
360
+ for (const out of pushLine(pending)) {
361
+ yield out;
362
+ }
363
+ }
364
+
365
+ const last = flush();
366
+ if (last) yield last;
367
+ }
368
+
369
+ /**
370
+ * Stream hashline-formatted output from an (async) iterable of lines.
371
+ *
372
+ * Each yielded chunk is a `\n`-joined string of one or more formatted lines.
373
+ */
374
+ export async function* streamHashLinesFromLines(
375
+ lines: Iterable<string> | AsyncIterable<string>,
376
+ options: HashlineStreamOptions = {},
377
+ ): AsyncGenerator<string> {
378
+ const startLine = options.startLine ?? 1;
379
+ const maxChunkLines = options.maxChunkLines ?? 200;
380
+ const maxChunkBytes = options.maxChunkBytes ?? 64 * 1024;
381
+
382
+ let lineNum = startLine;
383
+ let outLines: string[] = [];
384
+ let outBytes = 0;
385
+ let sawAnyLine = false;
386
+ const flush = (): string | undefined => {
387
+ if (outLines.length === 0) return undefined;
388
+ const chunk = outLines.join("\n");
389
+ outLines = [];
390
+ outBytes = 0;
391
+ return chunk;
392
+ };
393
+
394
+ const pushLine = (line: string): string[] => {
395
+ sawAnyLine = true;
396
+ const formatted = `${lineNum}#${computeLineHash(lineNum, line)}:${line}`;
397
+ lineNum++;
398
+
399
+ const chunksToYield: string[] = [];
400
+ const sepBytes = outLines.length === 0 ? 0 : 1;
401
+ const lineBytes = Buffer.byteLength(formatted, "utf-8");
402
+
403
+ if (
404
+ outLines.length > 0 &&
405
+ (outLines.length >= maxChunkLines || outBytes + sepBytes + lineBytes > maxChunkBytes)
406
+ ) {
407
+ const flushed = flush();
408
+ if (flushed) chunksToYield.push(flushed);
409
+ }
410
+
411
+ outLines.push(formatted);
412
+ outBytes += (outLines.length === 1 ? 0 : 1) + lineBytes;
413
+
414
+ if (outLines.length >= maxChunkLines || outBytes >= maxChunkBytes) {
415
+ const flushed = flush();
416
+ if (flushed) chunksToYield.push(flushed);
417
+ }
418
+
419
+ return chunksToYield;
420
+ };
421
+
422
+ const asyncIterator = (lines as AsyncIterable<string>)[Symbol.asyncIterator];
423
+ if (typeof asyncIterator === "function") {
424
+ for await (const line of lines as AsyncIterable<string>) {
425
+ for (const out of pushLine(line)) {
426
+ yield out;
427
+ }
428
+ }
429
+ } else {
430
+ for (const line of lines as Iterable<string>) {
431
+ for (const out of pushLine(line)) {
432
+ yield out;
433
+ }
434
+ }
435
+ }
436
+ if (!sawAnyLine) {
437
+ // Mirror `"".split("\n")` behavior: one empty line.
438
+ for (const out of pushLine("")) {
439
+ yield out;
440
+ }
441
+ }
442
+
443
+ const last = flush();
444
+ if (last) yield last;
445
+ }
446
+
447
+ /**
448
+ * Parse a line reference string like `"5#abcd"` into structured form.
449
+ *
450
+ * @throws Error if the format is invalid (not `NUMBER#HEXHASH`)
451
+ */
452
+ export function parseTag(ref: string): { line: number; hash: string } {
453
+ // This regex captures:
454
+ // 1. optional leading ">+" and whitespace
455
+ // 2. line number (1+ digits)
456
+ // 3. "#" with optional surrounding spaces
457
+ // 4. hash (2 hex chars)
458
+ // 5. optional trailing display suffix (":..." or " ...")
459
+ const match = ref.match(/^\s*[>+-]*\s*(\d+)\s*#\s*([ZPMQVRWSNKTXJBYH]{2})/);
460
+ if (!match) {
461
+ throw new Error(
462
+ `Invalid line reference "${ref}". Expected format "N#XX" (e.g. "5#PM") — copy the tag verbatim from read output.`,
463
+ );
464
+ }
465
+ const line = Number.parseInt(match[1], 10);
466
+ if (line < 1) {
467
+ throw new Error(`Line number must be >= 1, got ${line} in "${ref}".`);
468
+ }
469
+ return { line, hash: match[2] };
470
+ }
471
+
472
+ // ═══════════════════════════════════════════════════════════════════════════
473
+ // Hash Mismatch Error
474
+ // ═══════════════════════════════════════════════════════════════════════════
475
+
476
+ /** Number of context lines shown above/below each mismatched line */
477
+ const MISMATCH_CONTEXT = 2;
478
+
479
+ /**
480
+ * Error thrown when one or more hashline references have stale hashes.
481
+ *
482
+ * Displays grep-style output with `>>>` markers on mismatched lines,
483
+ * showing the correct `LINE#ID` so the caller can fix all refs at once.
484
+ */
485
+ export class HashlineMismatchError extends Error {
486
+ readonly remaps: ReadonlyMap<string, string>;
487
+ constructor(
488
+ public readonly mismatches: HashMismatch[],
489
+ public readonly fileLines: string[],
490
+ ) {
491
+ super(HashlineMismatchError.formatMessage(mismatches, fileLines));
492
+ this.name = "HashlineMismatchError";
493
+ const remaps = new Map<string, string>();
494
+ for (const m of mismatches) {
495
+ const actual = computeLineHash(m.line, fileLines[m.line - 1]);
496
+ remaps.set(`${m.line}#${m.expected}`, `${m.line}#${actual}`);
497
+ }
498
+ this.remaps = remaps;
499
+ }
500
+
501
+ static formatMessage(mismatches: HashMismatch[], fileLines: string[]): string {
502
+ const mismatchSet = new Map<number, HashMismatch>();
503
+ for (const m of mismatches) {
504
+ mismatchSet.set(m.line, m);
505
+ }
506
+
507
+ // Collect line ranges to display (mismatch lines + context)
508
+ const displayLines = new Set<number>();
509
+ for (const m of mismatches) {
510
+ const lo = Math.max(1, m.line - MISMATCH_CONTEXT);
511
+ const hi = Math.min(fileLines.length, m.line + MISMATCH_CONTEXT);
512
+ for (let i = lo; i <= hi; i++) {
513
+ displayLines.add(i);
514
+ }
515
+ }
516
+
517
+ const sorted = [...displayLines].sort((a, b) => a - b);
518
+ const lines: string[] = [];
519
+
520
+ lines.push(
521
+ `${mismatches.length} line${mismatches.length > 1 ? "s have" : " has"} changed since last read. Use the updated LINE#ID references shown below (>>> marks changed lines).`,
522
+ );
523
+ lines.push("");
524
+
525
+ let prevLine = -1;
526
+ for (const lineNum of sorted) {
527
+ // Gap separator between non-contiguous regions
528
+ if (prevLine !== -1 && lineNum > prevLine + 1) {
529
+ lines.push(" ...");
530
+ }
531
+ prevLine = lineNum;
532
+
533
+ const content = fileLines[lineNum - 1];
534
+ const hash = computeLineHash(lineNum, content);
535
+ const prefix = `${lineNum}#${hash}`;
536
+
537
+ if (mismatchSet.has(lineNum)) {
538
+ lines.push(`>>> ${prefix}:${content}`);
539
+ } else {
540
+ lines.push(` ${prefix}:${content}`);
541
+ }
542
+ }
543
+ return lines.join("\n");
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Validate that a line reference points to an existing line with a matching hash.
549
+ *
550
+ * @param ref - Parsed line reference (1-indexed line number + expected hash)
551
+ * @param fileLines - Array of file lines (0-indexed)
552
+ * @throws HashlineMismatchError if the hash doesn't match (includes correct hashes in context)
553
+ * @throws Error if the line is out of range
554
+ */
555
+ export function validateLineRef(ref: { line: number; hash: string }, fileLines: string[]): void {
556
+ if (ref.line < 1 || ref.line > fileLines.length) {
557
+ throw new Error(`Line ${ref.line} does not exist (file has ${fileLines.length} lines)`);
558
+ }
559
+ const actualHash = computeLineHash(ref.line, fileLines[ref.line - 1]);
560
+ if (actualHash !== ref.hash) {
561
+ throw new HashlineMismatchError([{ line: ref.line, expected: ref.hash, actual: actualHash }], fileLines);
562
+ }
563
+ }
564
+
565
+ // ═══════════════════════════════════════════════════════════════════════════
566
+ // Edit Application
567
+ // ═══════════════════════════════════════════════════════════════════════════
568
+
569
+ /**
570
+ * Apply an array of hashline edits to file content.
571
+ *
572
+ * Each edit operation identifies target lines directly (`set`, `set_range`,
573
+ * `insert`). Line references are resolved via {@link parseTag}
574
+ * and hashes validated before any mutation.
575
+ *
576
+ * Edits are sorted bottom-up (highest effective line first) so earlier
577
+ * splices don't invalidate later line numbers.
578
+ *
579
+ * @returns The modified content and the 1-indexed first changed line number
580
+ */
581
+ export function applyHashlineEdits(
582
+ content: string,
583
+ edits: HashlineEdit[],
584
+ ): {
585
+ content: string;
586
+ firstChangedLine: number | undefined;
587
+ warnings?: string[];
588
+ noopEdits?: Array<{ editIndex: number; loc: string; currentContent: string }>;
589
+ } {
590
+ if (edits.length === 0) {
591
+ return { content, firstChangedLine: undefined };
592
+ }
593
+
594
+ const fileLines = content.split("\n");
595
+ const hadFinalNewline = content.endsWith("\n");
596
+ const originalFileLines = [...fileLines];
597
+ let firstChangedLine: number | undefined;
598
+ const noopEdits: Array<{ editIndex: number; loc: string; currentContent: string }> = [];
599
+
600
+ const autocorrect = Bun.env.ARCANE_HL_AUTOCORRECT === "1";
601
+
602
+ function collectExplicitlyTouchedLines(): Set<number> {
603
+ const touched = new Set<number>();
604
+ for (const edit of edits) {
605
+ switch (edit.op) {
606
+ case "set":
607
+ touched.add(edit.tag.line);
608
+ break;
609
+ case "replace":
610
+ for (let ln = edit.first.line; ln <= edit.last.line; ln++) touched.add(ln);
611
+ break;
612
+ case "append":
613
+ if (edit.after) {
614
+ touched.add(edit.after.line);
615
+ }
616
+ break;
617
+ case "prepend":
618
+ if (edit.before) {
619
+ touched.add(edit.before.line);
620
+ }
621
+ break;
622
+ case "insert":
623
+ touched.add(edit.after.line);
624
+ touched.add(edit.before.line);
625
+ break;
626
+ }
627
+ }
628
+ return touched;
629
+ }
630
+
631
+ const explicitlyTouchedLines = collectExplicitlyTouchedLines();
632
+ // Pre-validate: collect all hash mismatches before mutating
633
+ const mismatches: HashMismatch[] = [];
634
+ function validateRef(ref: { line: number; hash: string }): boolean {
635
+ if (ref.line < 1 || ref.line > fileLines.length) {
636
+ throw new Error(`Line ${ref.line} does not exist (file has ${fileLines.length} lines)`);
637
+ }
638
+ const actualHash = computeLineHash(ref.line, fileLines[ref.line - 1]);
639
+ if (actualHash === ref.hash) {
640
+ return true;
641
+ }
642
+ mismatches.push({ line: ref.line, expected: ref.hash, actual: actualHash });
643
+ return false;
644
+ }
645
+ for (const edit of edits) {
646
+ switch (edit.op) {
647
+ case "set": {
648
+ if (!validateRef(edit.tag)) continue;
649
+ break;
650
+ }
651
+ case "append": {
652
+ if (edit.content.length === 0) {
653
+ throw new Error('Insert-after edit (src "N#HH..") requires non-empty dst');
654
+ }
655
+ if (edit.after && !validateRef(edit.after)) continue;
656
+ break;
657
+ }
658
+ case "prepend": {
659
+ if (edit.content.length === 0) {
660
+ throw new Error('Insert-before edit (src "N#HH..") requires non-empty dst');
661
+ }
662
+ if (edit.before && !validateRef(edit.before)) continue;
663
+ break;
664
+ }
665
+ case "insert": {
666
+ if (edit.content.length === 0) {
667
+ throw new Error('Insert-between edit (src "A#HH.. B#HH..") requires non-empty dst');
668
+ }
669
+ if (edit.before.line <= edit.after.line) {
670
+ throw new Error(`insert requires after (${edit.after.line}) < before (${edit.before.line})`);
671
+ }
672
+ const afterValid = validateRef(edit.after);
673
+ const beforeValid = validateRef(edit.before);
674
+ if (!afterValid || !beforeValid) continue;
675
+ break;
676
+ }
677
+ case "replace": {
678
+ if (edit.first.line > edit.last.line) {
679
+ throw new Error(`Range start line ${edit.first.line} must be <= end line ${edit.last.line}`);
680
+ }
681
+
682
+ const startValid = validateRef(edit.first);
683
+ const endValid = validateRef(edit.last);
684
+ if (!startValid || !endValid) continue;
685
+ break;
686
+ }
687
+ }
688
+ }
689
+ if (mismatches.length > 0) {
690
+ throw new HashlineMismatchError(mismatches, fileLines);
691
+ }
692
+ // Deduplicate identical edits targeting the same line(s)
693
+ const seenEditKeys = new Map<string, number>();
694
+ const dedupIndices = new Set<number>();
695
+ for (let i = 0; i < edits.length; i++) {
696
+ const edit = edits[i];
697
+ let lineKey: string;
698
+ switch (edit.op) {
699
+ case "set":
700
+ lineKey = `s:${edit.tag.line}`;
701
+ break;
702
+ case "replace":
703
+ lineKey = `r:${edit.first.line}:${edit.last.line}`;
704
+ break;
705
+ case "append":
706
+ if (edit.after) {
707
+ lineKey = `i:${edit.after.line}`;
708
+ break;
709
+ }
710
+ lineKey = "ieof";
711
+ break;
712
+ case "prepend":
713
+ if (edit.before) {
714
+ lineKey = `ib:${edit.before.line}`;
715
+ break;
716
+ }
717
+ lineKey = "ibef";
718
+ break;
719
+ case "insert":
720
+ lineKey = `ix:${edit.after.line}:${edit.before.line}`;
721
+ break;
722
+ }
723
+ const dstKey = `${lineKey}:${edit.content.join("\n")}`;
724
+ if (seenEditKeys.has(dstKey)) {
725
+ dedupIndices.add(i);
726
+ } else {
727
+ seenEditKeys.set(dstKey, i);
728
+ }
729
+ }
730
+ if (dedupIndices.size > 0) {
731
+ for (let i = edits.length - 1; i >= 0; i--) {
732
+ if (dedupIndices.has(i)) edits.splice(i, 1);
733
+ }
734
+ }
735
+
736
+ // Compute sort key (descending) — bottom-up application
737
+ const annotated = edits.map((edit, idx) => {
738
+ let sortLine: number;
739
+ let precedence: number;
740
+ switch (edit.op) {
741
+ case "set":
742
+ sortLine = edit.tag.line;
743
+ precedence = 0;
744
+ break;
745
+ case "replace":
746
+ sortLine = edit.last.line;
747
+ precedence = 0;
748
+ break;
749
+ case "append":
750
+ sortLine = edit.after ? edit.after.line : fileLines.length + 1;
751
+ precedence = 1;
752
+ break;
753
+ case "prepend":
754
+ sortLine = edit.before ? edit.before.line : 0;
755
+ precedence = 2;
756
+ break;
757
+ case "insert":
758
+ sortLine = edit.before.line;
759
+ precedence = 3;
760
+ break;
761
+ }
762
+ return { edit, idx, sortLine, precedence };
763
+ });
764
+
765
+ annotated.sort((a, b) => b.sortLine - a.sortLine || a.precedence - b.precedence || a.idx - b.idx);
766
+
767
+ // Apply edits bottom-up
768
+ for (const { edit, idx } of annotated) {
769
+ switch (edit.op) {
770
+ case "set": {
771
+ const merged = autocorrect ? maybeExpandSingleLineMerge(edit.tag.line, edit.content) : null;
772
+ if (merged) {
773
+ const origLines = originalFileLines.slice(
774
+ merged.startLine - 1,
775
+ merged.startLine - 1 + merged.deleteCount,
776
+ );
777
+ let nextLines = merged.newLines;
778
+ nextLines = restoreIndentForPairedReplacement([origLines[0] ?? ""], nextLines);
779
+
780
+ if (origLines.every((line, i) => line === nextLines[i])) {
781
+ noopEdits.push({
782
+ editIndex: idx,
783
+ loc: `${edit.tag.line}#${edit.tag.hash}`,
784
+ currentContent: origLines.join("\n"),
785
+ });
786
+ break;
787
+ }
788
+ fileLines.splice(merged.startLine - 1, merged.deleteCount, ...nextLines);
789
+ trackFirstChanged(merged.startLine);
790
+ break;
791
+ }
792
+
793
+ const count = 1;
794
+ const origLines = originalFileLines.slice(edit.tag.line - 1, edit.tag.line);
795
+ let stripped = autocorrect
796
+ ? stripRangeBoundaryEcho(originalFileLines, edit.tag.line, edit.tag.line, edit.content)
797
+ : edit.content;
798
+ stripped = autocorrect ? restoreOldWrappedLines(origLines, stripped) : stripped;
799
+ const newLines = autocorrect ? restoreIndentForPairedReplacement(origLines, stripped) : stripped;
800
+ if (origLines.every((line, i) => line === newLines[i])) {
801
+ noopEdits.push({
802
+ editIndex: idx,
803
+ loc: `${edit.tag.line}#${edit.tag.hash}`,
804
+ currentContent: origLines.join("\n"),
805
+ });
806
+ break;
807
+ }
808
+ fileLines.splice(edit.tag.line - 1, count, ...newLines);
809
+ trackFirstChanged(edit.tag.line);
810
+ break;
811
+ }
812
+ case "replace": {
813
+ const count = edit.last.line - edit.first.line + 1;
814
+ const origLines = originalFileLines.slice(edit.first.line - 1, edit.first.line - 1 + count);
815
+ let stripped = autocorrect
816
+ ? stripRangeBoundaryEcho(originalFileLines, edit.first.line, edit.last.line, edit.content)
817
+ : edit.content;
818
+ stripped = autocorrect ? restoreOldWrappedLines(origLines, stripped) : stripped;
819
+ const newLines = autocorrect ? restoreIndentForPairedReplacement(origLines, stripped) : stripped;
820
+ if (autocorrect && origLines.every((line, i) => line === newLines[i])) {
821
+ noopEdits.push({
822
+ editIndex: idx,
823
+ loc: `${edit.first.line}#${edit.first.hash}`,
824
+ currentContent: origLines.join("\n"),
825
+ });
826
+ break;
827
+ }
828
+ fileLines.splice(edit.first.line - 1, count, ...newLines);
829
+ trackFirstChanged(edit.first.line);
830
+ break;
831
+ }
832
+ case "append": {
833
+ const inserted = edit.after
834
+ ? autocorrect
835
+ ? stripInsertAnchorEchoAfter(originalFileLines[edit.after.line - 1], edit.content)
836
+ : edit.content
837
+ : edit.content;
838
+ if (inserted.length === 0) {
839
+ noopEdits.push({
840
+ editIndex: idx,
841
+ loc: edit.after ? `${edit.after.line}#${edit.after.hash}` : "EOF",
842
+ currentContent: edit.after ? originalFileLines[edit.after.line - 1] : "",
843
+ });
844
+ break;
845
+ }
846
+ if (edit.after) {
847
+ fileLines.splice(edit.after.line, 0, ...inserted);
848
+ trackFirstChanged(edit.after.line + 1);
849
+ } else {
850
+ if (fileLines.length === 1 && fileLines[0] === "") {
851
+ fileLines.splice(0, 1, ...inserted);
852
+ trackFirstChanged(1);
853
+ } else {
854
+ fileLines.splice(fileLines.length, 0, ...inserted);
855
+ trackFirstChanged(fileLines.length - inserted.length + 1);
856
+ }
857
+ }
858
+ break;
859
+ }
860
+ case "prepend": {
861
+ const inserted = edit.before
862
+ ? autocorrect
863
+ ? stripInsertAnchorEchoBefore(originalFileLines[edit.before.line - 1], edit.content)
864
+ : edit.content
865
+ : edit.content;
866
+ if (inserted.length === 0) {
867
+ noopEdits.push({
868
+ editIndex: idx,
869
+ loc: edit.before ? `${edit.before.line}#${edit.before.hash}` : "BOF",
870
+ currentContent: edit.before ? originalFileLines[edit.before.line - 1] : "",
871
+ });
872
+ break;
873
+ }
874
+ if (edit.before) {
875
+ fileLines.splice(edit.before.line - 1, 0, ...inserted);
876
+ trackFirstChanged(edit.before.line);
877
+ } else {
878
+ if (fileLines.length === 1 && fileLines[0] === "") {
879
+ fileLines.splice(0, 1, ...inserted);
880
+ } else {
881
+ fileLines.splice(0, 0, ...inserted);
882
+ }
883
+ trackFirstChanged(1);
884
+ }
885
+ break;
886
+ }
887
+ case "insert": {
888
+ const afterLine = originalFileLines[edit.after.line - 1];
889
+ const beforeLine = originalFileLines[edit.before.line - 1];
890
+ const inserted = autocorrect ? stripInsertBoundaryEcho(afterLine, beforeLine, edit.content) : edit.content;
891
+ if (inserted.length === 0) {
892
+ noopEdits.push({
893
+ editIndex: idx,
894
+ loc: `${edit.after.line}#${edit.after.hash}..${edit.before.line}#${edit.before.hash}`,
895
+ currentContent: `${afterLine}\n${beforeLine}`,
896
+ });
897
+ break;
898
+ }
899
+ fileLines.splice(edit.before.line - 1, 0, ...inserted);
900
+ trackFirstChanged(edit.before.line);
901
+ break;
902
+ }
903
+ }
904
+ }
905
+
906
+ let finalContent = fileLines.join("\n");
907
+ // Preserve trailing newline behavior of original content
908
+ if (hadFinalNewline && !finalContent.endsWith("\n")) {
909
+ finalContent += "\n";
910
+ } else if (!hadFinalNewline && finalContent.endsWith("\n")) {
911
+ finalContent = finalContent.slice(0, -1);
912
+ }
913
+
914
+ return {
915
+ content: finalContent,
916
+ firstChangedLine,
917
+ ...(noopEdits.length > 0 ? { noopEdits } : {}),
918
+ };
919
+
920
+ function trackFirstChanged(line: number): void {
921
+ if (firstChangedLine === undefined || line < firstChangedLine) {
922
+ firstChangedLine = line;
923
+ }
924
+ }
925
+
926
+ function maybeExpandSingleLineMerge(
927
+ line: number,
928
+ content: string[],
929
+ ): { startLine: number; deleteCount: number; newLines: string[] } | null {
930
+ if (content.length !== 1) return null;
931
+ if (line < 1 || line > fileLines.length) return null;
932
+
933
+ const newLine = content[0];
934
+ const newCanon = stripAllWhitespace(newLine);
935
+ const newCanonForMergeOps = stripMergeOperatorChars(newCanon);
936
+ if (newCanon.length === 0) return null;
937
+
938
+ const orig = fileLines[line - 1];
939
+ const origCanon = stripAllWhitespace(orig);
940
+ const origCanonForMatch = stripTrailingContinuationTokens(origCanon);
941
+ const origCanonForMergeOps = stripMergeOperatorChars(origCanon);
942
+ const origLooksLikeContinuation = origCanonForMatch.length < origCanon.length;
943
+ if (origCanon.length === 0) return null;
944
+ const nextIdx = line;
945
+ const prevIdx = line - 2;
946
+ // Case A: dst absorbed the next continuation line.
947
+ if (origLooksLikeContinuation && nextIdx < fileLines.length && !explicitlyTouchedLines.has(line + 1)) {
948
+ const next = fileLines[nextIdx];
949
+ const nextCanon = stripAllWhitespace(next);
950
+ const a = newCanon.indexOf(origCanonForMatch);
951
+ const b = newCanon.indexOf(nextCanon);
952
+ if (a !== -1 && b !== -1 && a < b && newCanon.length <= origCanon.length + nextCanon.length + 32) {
953
+ return { startLine: line, deleteCount: 2, newLines: [newLine] };
954
+ }
955
+ }
956
+ // Case B: dst absorbed the previous declaration/continuation line.
957
+ if (prevIdx >= 0 && !explicitlyTouchedLines.has(line - 1)) {
958
+ const prev = fileLines[prevIdx];
959
+ const prevCanon = stripAllWhitespace(prev);
960
+ const prevCanonForMatch = stripTrailingContinuationTokens(prevCanon);
961
+ const prevLooksLikeContinuation = prevCanonForMatch.length < prevCanon.length;
962
+ if (!prevLooksLikeContinuation) return null;
963
+ const a = newCanonForMergeOps.indexOf(stripMergeOperatorChars(prevCanonForMatch));
964
+ const b = newCanonForMergeOps.indexOf(origCanonForMergeOps);
965
+ if (a !== -1 && b !== -1 && a < b && newCanon.length <= prevCanon.length + origCanon.length + 32) {
966
+ return { startLine: line - 1, deleteCount: 2, newLines: [newLine] };
967
+ }
968
+ }
969
+
970
+ return null;
971
+ }
972
+ }