pim-agent 0.0.1 → 0.9.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 (261) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -7
  3. package/bin/pim.ts +151 -0
  4. package/package.json +82 -4
  5. package/packages/core/src/attachments/AttachmentStore.ts +99 -0
  6. package/packages/core/src/attachments/Attachments.ts +69 -0
  7. package/packages/core/src/extensions/CoreExtensions.ts +51 -0
  8. package/packages/core/src/extensions/apply-patch/coordinator.ts +53 -0
  9. package/packages/core/src/extensions/apply-patch/executor.ts +550 -0
  10. package/packages/core/src/extensions/apply-patch/index.ts +69 -0
  11. package/packages/core/src/extensions/apply-patch/matcher.ts +47 -0
  12. package/packages/core/src/extensions/apply-patch/model.ts +39 -0
  13. package/packages/core/src/extensions/apply-patch/parser.ts +379 -0
  14. package/packages/core/src/extensions/apply-patch/render.ts +216 -0
  15. package/packages/core/src/extensions/apply-patch/schema.ts +37 -0
  16. package/packages/core/src/extensions/apply-patch/types.ts +30 -0
  17. package/packages/core/src/extensions/bash/capture.ts +86 -0
  18. package/packages/core/src/extensions/bash/format.ts +110 -0
  19. package/packages/core/src/extensions/bash/image.ts +31 -0
  20. package/packages/core/src/extensions/bash/index.ts +88 -0
  21. package/packages/core/src/extensions/bash/render.ts +31 -0
  22. package/packages/core/src/extensions/bash/run.ts +221 -0
  23. package/packages/core/src/extensions/bash/schema.ts +64 -0
  24. package/packages/core/src/extensions/edit/edit.ts +366 -0
  25. package/packages/core/src/extensions/edit/index.ts +36 -0
  26. package/packages/core/src/extensions/edit/render.ts +16 -0
  27. package/packages/core/src/extensions/edit/schema.ts +37 -0
  28. package/packages/core/src/extensions/glob/glob.ts +34 -0
  29. package/packages/core/src/extensions/glob/index.ts +83 -0
  30. package/packages/core/src/extensions/glob/render.ts +72 -0
  31. package/packages/core/src/extensions/glob/schema.ts +66 -0
  32. package/packages/core/src/extensions/grep/grep.ts +256 -0
  33. package/packages/core/src/extensions/grep/index.ts +114 -0
  34. package/packages/core/src/extensions/grep/render.ts +271 -0
  35. package/packages/core/src/extensions/grep/schema.ts +92 -0
  36. package/packages/core/src/extensions/read/ImageMemory.ts +42 -0
  37. package/packages/core/src/extensions/read/image.ts +57 -0
  38. package/packages/core/src/extensions/read/index.ts +88 -0
  39. package/packages/core/src/extensions/read/read.ts +191 -0
  40. package/packages/core/src/extensions/read/render.ts +58 -0
  41. package/packages/core/src/extensions/read/schema.ts +49 -0
  42. package/packages/core/src/extensions/subagent/index.ts +33 -0
  43. package/packages/core/src/extensions/subagent/render.ts +127 -0
  44. package/packages/core/src/extensions/subagent/schema.ts +9 -0
  45. package/packages/core/src/extensions/subagent/subagent.ts +419 -0
  46. package/packages/core/src/extensions/system-prompt/index.ts +28 -0
  47. package/packages/core/src/extensions/system-prompt/prompt.ts +228 -0
  48. package/packages/core/src/extensions/todo/index.ts +123 -0
  49. package/packages/core/src/extensions/todo/render.ts +156 -0
  50. package/packages/core/src/extensions/todo/schema.ts +24 -0
  51. package/packages/core/src/extensions/todo/todo.ts +185 -0
  52. package/packages/core/src/extensions/web-fetch/JinaReaderClient.ts +187 -0
  53. package/packages/core/src/extensions/web-fetch/WebViewFetchClient.ts +183 -0
  54. package/packages/core/src/extensions/web-fetch/WebViewMarkdownSnapshot.ts +512 -0
  55. package/packages/core/src/extensions/web-fetch/fetch.ts +265 -0
  56. package/packages/core/src/extensions/web-fetch/image.ts +165 -0
  57. package/packages/core/src/extensions/web-fetch/index.ts +83 -0
  58. package/packages/core/src/extensions/web-fetch/render.ts +100 -0
  59. package/packages/core/src/extensions/web-fetch/schema.ts +46 -0
  60. package/packages/core/src/extensions/web-search/ExaMcpClient.ts +251 -0
  61. package/packages/core/src/extensions/web-search/SearchBreaker.ts +147 -0
  62. package/packages/core/src/extensions/web-search/SearchChain.ts +77 -0
  63. package/packages/core/src/extensions/web-search/index.ts +90 -0
  64. package/packages/core/src/extensions/web-search/providers/DuckDuckGoProvider.ts +195 -0
  65. package/packages/core/src/extensions/web-search/providers/ExaProvider.ts +54 -0
  66. package/packages/core/src/extensions/web-search/providers/FirecrawlProvider.ts +144 -0
  67. package/packages/core/src/extensions/web-search/providers/SearchProvider.ts +118 -0
  68. package/packages/core/src/extensions/web-search/render.ts +49 -0
  69. package/packages/core/src/extensions/web-search/schema.ts +29 -0
  70. package/packages/core/src/extensions/web-search/search.ts +23 -0
  71. package/packages/core/src/extensions/write/index.ts +58 -0
  72. package/packages/core/src/extensions/write/render.ts +16 -0
  73. package/packages/core/src/extensions/write/schema.ts +14 -0
  74. package/packages/core/src/extensions/write/write.ts +87 -0
  75. package/packages/core/src/picker/FilePickerSuggestionEngine.ts +14 -0
  76. package/packages/core/src/picker/InProcessFilePickerSuggestionEngine.ts +52 -0
  77. package/packages/core/src/picker/PickerItem.ts +6 -0
  78. package/packages/core/src/picker/PickerService.ts +110 -0
  79. package/packages/core/src/picker/RemoteFilePickerSuggestionEngine.ts +56 -0
  80. package/packages/core/src/picker/WorkerFilePickerSuggestionEngine.ts +268 -0
  81. package/packages/core/src/picker/catalog.ts +220 -0
  82. package/packages/core/src/picker/commandRanker.ts +17 -0
  83. package/packages/core/src/picker/filePickerWorker.ts +66 -0
  84. package/packages/core/src/picker/filePickerWorkerMessages.ts +39 -0
  85. package/packages/core/src/picker/ranker.ts +245 -0
  86. package/packages/core/src/picker/token.ts +70 -0
  87. package/packages/core/src/session/AgentRuntime.ts +85 -0
  88. package/packages/core/src/session/EventLog.ts +213 -0
  89. package/packages/core/src/session/MessageText.ts +21 -0
  90. package/packages/core/src/session/ReadCursors.ts +94 -0
  91. package/packages/core/src/session/SessionCache.ts +68 -0
  92. package/packages/core/src/session/SessionHost.ts +876 -0
  93. package/packages/core/src/session/SessionLease.ts +279 -0
  94. package/packages/core/src/session/SessionRegistry.ts +169 -0
  95. package/packages/core/src/session/WriteMark.ts +37 -0
  96. package/packages/core/src/shared/Cli.ts +25 -0
  97. package/packages/core/src/shared/DaemonUnit.ts +13 -0
  98. package/packages/core/src/shared/DiffLines.ts +304 -0
  99. package/packages/core/src/shared/DiffRenderer.ts +283 -0
  100. package/packages/core/src/shared/DiffView.ts +29 -0
  101. package/packages/core/src/shared/Directories.ts +61 -0
  102. package/packages/core/src/shared/EditMatcher.ts +767 -0
  103. package/packages/core/src/shared/Errors.ts +9 -0
  104. package/packages/core/src/shared/ExtensionToggles.ts +130 -0
  105. package/packages/core/src/shared/FileEnumerator.ts +361 -0
  106. package/packages/core/src/shared/FileScanner.ts +57 -0
  107. package/packages/core/src/shared/FileWatch.ts +107 -0
  108. package/packages/core/src/shared/Format.ts +74 -0
  109. package/packages/core/src/shared/Fs.ts +91 -0
  110. package/packages/core/src/shared/FsErrors.ts +67 -0
  111. package/packages/core/src/shared/FuzzyMatcher.ts +71 -0
  112. package/packages/core/src/shared/Git.ts +89 -0
  113. package/packages/core/src/shared/GlobExclusions.ts +13 -0
  114. package/packages/core/src/shared/Http.ts +14 -0
  115. package/packages/core/src/shared/ImageMime.ts +36 -0
  116. package/packages/core/src/shared/Images.ts +357 -0
  117. package/packages/core/src/shared/Json.ts +19 -0
  118. package/packages/core/src/shared/Languages.ts +95 -0
  119. package/packages/core/src/shared/Levenshtein.ts +30 -0
  120. package/packages/core/src/shared/Lines.ts +79 -0
  121. package/packages/core/src/shared/McpClient.ts +444 -0
  122. package/packages/core/src/shared/OutputBudget.ts +84 -0
  123. package/packages/core/src/shared/PatchSummary.ts +40 -0
  124. package/packages/core/src/shared/Paths.ts +80 -0
  125. package/packages/core/src/shared/PimSettings.ts +128 -0
  126. package/packages/core/src/shared/PimVersion.ts +93 -0
  127. package/packages/core/src/shared/Pool.ts +26 -0
  128. package/packages/core/src/shared/Proc.ts +30 -0
  129. package/packages/core/src/shared/RateLimiter.ts +50 -0
  130. package/packages/core/src/shared/Renderer.ts +371 -0
  131. package/packages/core/src/shared/SafePath.ts +17 -0
  132. package/packages/core/src/shared/SearchRender.ts +82 -0
  133. package/packages/core/src/shared/SendFile.ts +23 -0
  134. package/packages/core/src/shared/SpillCache.ts +78 -0
  135. package/packages/core/src/shared/SubagentLogs.ts +102 -0
  136. package/packages/core/src/shared/Supervisor.ts +460 -0
  137. package/packages/core/src/shared/Sweeper.ts +32 -0
  138. package/packages/core/src/shared/Tools.ts +801 -0
  139. package/packages/core/src/shared/Updater.ts +193 -0
  140. package/packages/core/src/view/AnsiPainter.ts +227 -0
  141. package/packages/core/src/view/BodyRenderer.ts +102 -0
  142. package/packages/core/src/view/DiffBlocks.ts +52 -0
  143. package/packages/core/src/view/DiffLayout.ts +69 -0
  144. package/packages/core/src/view/MarkdownPainter.ts +314 -0
  145. package/packages/core/src/view/Painting.ts +182 -0
  146. package/packages/core/src/view/ViewBlock.ts +124 -0
  147. package/packages/daemon/src/Daemon.ts +63 -0
  148. package/packages/daemon/src/DaemonInstall.ts +115 -0
  149. package/packages/daemon/src/Surfaces.ts +66 -0
  150. package/packages/daemon/src/TelegramSurface.ts +30 -0
  151. package/packages/daemon/src/WebSurface.ts +57 -0
  152. package/packages/daemon/src/index.ts +37 -0
  153. package/packages/protocol/src/Command.ts +92 -0
  154. package/packages/protocol/src/ImageRoute.ts +23 -0
  155. package/packages/protocol/src/Protocol.ts +6 -0
  156. package/packages/protocol/src/ServerEvent.ts +251 -0
  157. package/packages/server/src/AttachmentEndpoint.ts +184 -0
  158. package/packages/server/src/ClientConnection.ts +265 -0
  159. package/packages/server/src/ImageEndpoint.ts +36 -0
  160. package/packages/server/src/ProbeClient.ts +312 -0
  161. package/packages/server/src/Reloader.ts +93 -0
  162. package/packages/server/src/SendFileTool.ts +88 -0
  163. package/packages/server/src/SessionCatalogue.ts +202 -0
  164. package/packages/server/src/SessionProjection.ts +194 -0
  165. package/packages/server/src/SessionStream.ts +601 -0
  166. package/packages/server/src/StaticClient.ts +95 -0
  167. package/packages/server/src/WebOptions.ts +72 -0
  168. package/packages/server/src/WsGateway.ts +563 -0
  169. package/packages/server/src/probe.ts +146 -0
  170. package/packages/telegram/src/Bot.ts +229 -0
  171. package/packages/telegram/src/Commands.ts +768 -0
  172. package/packages/telegram/src/Config.ts +148 -0
  173. package/packages/telegram/src/Markdown.ts +235 -0
  174. package/packages/telegram/src/Message.ts +152 -0
  175. package/packages/telegram/src/Renderer.ts +456 -0
  176. package/packages/telegram/src/SendFileSchema.ts +18 -0
  177. package/packages/telegram/src/SendFileTool.ts +65 -0
  178. package/packages/telegram/src/Session.ts +252 -0
  179. package/packages/telegram/src/SessionRegistry.ts +126 -0
  180. package/packages/telegram/src/TaskScheduler.ts +296 -0
  181. package/packages/telegram/src/TaskSchema.ts +88 -0
  182. package/packages/telegram/src/TaskStore.ts +67 -0
  183. package/packages/telegram/src/TaskTool.ts +208 -0
  184. package/packages/telegram/src/TelegramHtml.ts +288 -0
  185. package/packages/telegram/src/TypingIndicator.ts +43 -0
  186. package/packages/telegram/src/UpdateConfirm.ts +61 -0
  187. package/packages/tui/src/extensions/_init/index.ts +144 -0
  188. package/packages/tui/src/extensions/command-picker/index.ts +43 -0
  189. package/packages/tui/src/extensions/file-picker/index.ts +156 -0
  190. package/packages/tui/src/extensions/footer/index.ts +117 -0
  191. package/packages/tui/src/extensions/footer/powerline.ts +87 -0
  192. package/packages/tui/src/extensions/footer/segments.ts +202 -0
  193. package/packages/tui/src/extensions/pim/index.ts +89 -0
  194. package/packages/tui/src/extensions/session-lease/index.ts +248 -0
  195. package/packages/tui/src/extensions/tps/index.ts +118 -0
  196. package/packages/tui/src/extensions/working-indicator/index.ts +75 -0
  197. package/packages/tui/src/extensions/wrapProvider.ts +18 -0
  198. package/packages/tui/src/themes/pim-dark.json +84 -0
  199. package/packages/tui/src/themes/pim-light.json +84 -0
  200. package/packages/tui/src/themes/themeCliArgs.ts +20 -0
  201. package/packages/web/dist/client/apple-touch-icon.png +0 -0
  202. package/packages/web/dist/client/assets/bash-LmRjUKlR.js +1 -0
  203. package/packages/web/dist/client/assets/c-DBVbJFpC.js +1 -0
  204. package/packages/web/dist/client/assets/clojure-CR7dvLJ4.js +1 -0
  205. package/packages/web/dist/client/assets/cmake-CgP6gB4W.js +1 -0
  206. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-B48RJMeK.woff +0 -0
  207. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-DBq5bJCE.woff2 +0 -0
  208. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-BXinMwCi.woff +0 -0
  209. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-DjSHLl2N.woff2 +0 -0
  210. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-s0S3qwFW.woff +0 -0
  211. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-wzhe4RuD.woff2 +0 -0
  212. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-BuQDXT0M.woff +0 -0
  213. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-SwgSWHaV.woff2 +0 -0
  214. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-1-GU0IUE.woff +0 -0
  215. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-DU8mrtj2.woff2 +0 -0
  216. package/packages/web/dist/client/assets/core-CGdQx9la.js +3 -0
  217. package/packages/web/dist/client/assets/cpp-N2d8M7Nc.js +1 -0
  218. package/packages/web/dist/client/assets/csharp-yYt0Ikks.js +1 -0
  219. package/packages/web/dist/client/assets/css-D8M8GYaw.js +1 -0
  220. package/packages/web/dist/client/assets/diff-BhdZizse.js +1 -0
  221. package/packages/web/dist/client/assets/dockerfile-C0zufGsU.js +1 -0
  222. package/packages/web/dist/client/assets/elixir-DXIHlGsR.js +1 -0
  223. package/packages/web/dist/client/assets/erlang-BjYI8dhz.js +1 -0
  224. package/packages/web/dist/client/assets/go-CYstVVJV.js +1 -0
  225. package/packages/web/dist/client/assets/graphql-BHHhacaR.js +1 -0
  226. package/packages/web/dist/client/assets/haskell-DUkhXIda.js +1 -0
  227. package/packages/web/dist/client/assets/index-BMNwleWO.js +40 -0
  228. package/packages/web/dist/client/assets/index-CqjB2RPr.css +1 -0
  229. package/packages/web/dist/client/assets/ini-B1CroMsf.js +1 -0
  230. package/packages/web/dist/client/assets/java-Dde-Wnb4.js +1 -0
  231. package/packages/web/dist/client/assets/javascript-8AmHuH5O.js +1 -0
  232. package/packages/web/dist/client/assets/json-PmyKWHmm.js +1 -0
  233. package/packages/web/dist/client/assets/kotlin-DgSK8bu-.js +2 -0
  234. package/packages/web/dist/client/assets/less-Bp0mgmpG.js +1 -0
  235. package/packages/web/dist/client/assets/lua-1dXYaL-H.js +1 -0
  236. package/packages/web/dist/client/assets/makefile-BX-o3EIz.js +1 -0
  237. package/packages/web/dist/client/assets/markdown-D6FrnlqJ.js +1 -0
  238. package/packages/web/dist/client/assets/ocaml-B-P1Uyq8.js +1 -0
  239. package/packages/web/dist/client/assets/perl-CvqMZYuf.js +1 -0
  240. package/packages/web/dist/client/assets/php-DuZl5e-L.js +2 -0
  241. package/packages/web/dist/client/assets/powershell-BUe16zE4.js +1 -0
  242. package/packages/web/dist/client/assets/protobuf-DGOW1qT8.js +1 -0
  243. package/packages/web/dist/client/assets/python-gHmu6VPG.js +1 -0
  244. package/packages/web/dist/client/assets/r-DNWPCnjT.js +1 -0
  245. package/packages/web/dist/client/assets/ruby-BGpZ3p4_.js +1 -0
  246. package/packages/web/dist/client/assets/rust-BIKPVoi6.js +1 -0
  247. package/packages/web/dist/client/assets/scala-C9lP4FWY.js +1 -0
  248. package/packages/web/dist/client/assets/scss-gOyOKiXd.js +1 -0
  249. package/packages/web/dist/client/assets/sql-DWRKyvJ-.js +1 -0
  250. package/packages/web/dist/client/assets/swift-BTn9__hL.js +1 -0
  251. package/packages/web/dist/client/assets/typescript-CxkPLmSc.js +1 -0
  252. package/packages/web/dist/client/assets/vim-Dhhcn_CA.js +1 -0
  253. package/packages/web/dist/client/assets/xml-COl5yFSC.js +1 -0
  254. package/packages/web/dist/client/assets/yaml-DjOhhVLG.js +1 -0
  255. package/packages/web/dist/client/favicon.svg +1 -0
  256. package/packages/web/dist/client/icon-192.png +0 -0
  257. package/packages/web/dist/client/icon-512.png +0 -0
  258. package/packages/web/dist/client/icon-maskable.png +0 -0
  259. package/packages/web/dist/client/index.html +24 -0
  260. package/packages/web/dist/client/manifest.webmanifest +21 -0
  261. package/packages/web/dist/client/wordmark.svg +1 -0
@@ -0,0 +1,767 @@
1
+ import { Levenshtein } from "./Levenshtein";
2
+
3
+ export type EditMatchStrategy =
4
+ | "simple"
5
+ | "lineTrimmed"
6
+ | "whitespaceNormalized"
7
+ | "indentationFlexible"
8
+ | "escapeNormalized"
9
+ | "trimmedBoundary"
10
+ | "unicodeNormalized"
11
+ | "blockAnchor"
12
+ | "contextAware";
13
+
14
+ export type EditRange = readonly [start: number, end: number];
15
+
16
+ export type ClosestRegion = {
17
+ readonly startLine: number;
18
+ readonly endLine: number;
19
+ readonly similarity: number;
20
+ readonly text: string;
21
+ };
22
+
23
+ export type ResolveOutcome =
24
+ | {
25
+ readonly range: EditRange;
26
+ readonly strategy: EditMatchStrategy;
27
+ readonly matchCount: number;
28
+ }
29
+ | {
30
+ readonly ranges: readonly EditRange[];
31
+ readonly strategy: EditMatchStrategy;
32
+ readonly matchCount: number;
33
+ };
34
+
35
+ type Candidate = {
36
+ readonly range: EditRange;
37
+ readonly text: string;
38
+ };
39
+
40
+ type MatchInput = {
41
+ readonly content: string;
42
+ readonly oldString: string;
43
+ readonly contentLines: readonly OffsetLine[];
44
+ readonly searchLines: readonly string[];
45
+ };
46
+
47
+ type Strategy = {
48
+ readonly name: EditMatchStrategy;
49
+ readonly find: (input: MatchInput) => readonly Candidate[];
50
+ };
51
+
52
+ function resolve(
53
+ content: string,
54
+ oldString: string,
55
+ replaceAll = false
56
+ ): ResolveOutcome {
57
+ if (oldString.length === 0) {
58
+ throw new NotFoundError(oldString, findClosestRegions(content, oldString));
59
+ }
60
+
61
+ let foundAmbiguous = false;
62
+ let contentLines: readonly OffsetLine[] | undefined;
63
+ let searchLines: readonly string[] | undefined;
64
+ const input: MatchInput = {
65
+ content,
66
+ oldString,
67
+ get contentLines() {
68
+ return (contentLines ??= offsetLines(content));
69
+ },
70
+ get searchLines() {
71
+ return (searchLines ??= logicalLines(oldString));
72
+ },
73
+ };
74
+
75
+ for (const strategy of strategies) {
76
+ const candidates = dedupeCandidates(strategy.find(input));
77
+
78
+ if (candidates.length === 0) {
79
+ continue;
80
+ }
81
+
82
+ if (replaceAll) {
83
+ return {
84
+ ranges: sortRanges(candidates.map((candidate) => candidate.range)),
85
+ strategy: strategy.name,
86
+ matchCount: candidates.length,
87
+ };
88
+ }
89
+
90
+ const unique = candidates.filter((candidate) => {
91
+ const first = content.indexOf(candidate.text);
92
+ const last = content.lastIndexOf(candidate.text);
93
+ return first !== -1 && first === last;
94
+ });
95
+
96
+ if (unique.length === 1) {
97
+ return {
98
+ range: unique[0]!.range,
99
+ strategy: strategy.name,
100
+ matchCount: candidates.length,
101
+ };
102
+ }
103
+
104
+ foundAmbiguous = true;
105
+ }
106
+
107
+ if (foundAmbiguous) {
108
+ throw new MultipleMatchesError(oldString);
109
+ }
110
+
111
+ throw new NotFoundError(oldString, findClosestRegions(content, oldString));
112
+ }
113
+
114
+ function applyAll(
115
+ content: string,
116
+ mutations: readonly {
117
+ readonly range: EditRange;
118
+ readonly newString: string;
119
+ }[]
120
+ ): string {
121
+ let result = content;
122
+
123
+ for (const mutation of [...mutations].sort(
124
+ (left, right) => right.range[0] - left.range[0]
125
+ )) {
126
+ result =
127
+ result.slice(0, mutation.range[0]) +
128
+ mutation.newString +
129
+ result.slice(mutation.range[1]);
130
+ }
131
+
132
+ return result;
133
+ }
134
+
135
+ function assertNoEscapeDrift(
136
+ strategy: EditMatchStrategy,
137
+ newString: string,
138
+ matchedRegion: string
139
+ ): void {
140
+ if (strategy === "simple") {
141
+ return;
142
+ }
143
+
144
+ const introduced = escapeSequences(newString).filter(
145
+ (sequence) => !matchedRegion.includes(sequence)
146
+ );
147
+
148
+ if (introduced.length === 0) {
149
+ return;
150
+ }
151
+
152
+ throw new Error(
153
+ `Edit failed: newString contains literal escape text not present in the matched file text: ${[...new Set(introduced)].join(", ")}. Re-read the file and retry using exact file text in oldString and the intended replacement text in newString.`
154
+ );
155
+ }
156
+
157
+ function findClosestRegions(
158
+ content: string,
159
+ oldString: string,
160
+ max = 3,
161
+ threshold = 0.5
162
+ ): readonly ClosestRegion[] {
163
+ const contentLines = logicalLines(content);
164
+ const searchLines = logicalLines(oldString);
165
+ const windowSize = Math.max(1, searchLines.length);
166
+ const regions: ClosestRegion[] = [];
167
+
168
+ if (contentLines.length === 0 || searchLines.length === 0) {
169
+ return [];
170
+ }
171
+
172
+ const cache = new Map<string, number>();
173
+ const similarityOf = (left: string, right: string): number => {
174
+ const key = `${left}\u0000${right}`;
175
+ let cached = cache.get(key);
176
+
177
+ if (cached === undefined) {
178
+ cached = lineSimilarity(left, right);
179
+ cache.set(key, cached);
180
+ }
181
+
182
+ return cached;
183
+ };
184
+
185
+ for (let index = 0; index + windowSize <= contentLines.length; index += 1) {
186
+ let total = 0;
187
+
188
+ for (let offset = 0; offset < windowSize; offset += 1) {
189
+ total += similarityOf(
190
+ searchLines[offset] ?? "",
191
+ contentLines[index + offset] ?? ""
192
+ );
193
+ }
194
+
195
+ const similarity = total / windowSize;
196
+
197
+ if (similarity >= threshold) {
198
+ regions.push({
199
+ startLine: index + 1,
200
+ endLine: index + windowSize,
201
+ similarity,
202
+ text: contentLines.slice(index, index + windowSize).join("\n"),
203
+ });
204
+ }
205
+ }
206
+
207
+ return regions
208
+ .sort((left, right) => right.similarity - left.similarity)
209
+ .slice(0, max);
210
+ }
211
+
212
+ function renderNotFound(error: NotFoundError): string {
213
+ if (error.closest.length === 0) {
214
+ return "oldString was not found in the file.";
215
+ }
216
+
217
+ return [
218
+ "oldString was not found in the file. Closest candidate regions:",
219
+ "",
220
+ ...error.closest.flatMap((region, index) => [
221
+ `${index + 1}. lines ${formatLineRange(region.startLine, region.endLine)} (${Math.round(region.similarity * 100)}% similar)`,
222
+ region.text,
223
+ ]),
224
+ ].join("\n");
225
+ }
226
+
227
+ function lineRangeFor(content: string, range: EditRange): string {
228
+ const start = lineNumberAt(content, range[0]);
229
+ const end = lineNumberAt(content, Math.max(range[0], range[1] - 1));
230
+ return formatLineRange(start, end);
231
+ }
232
+
233
+ class NotFoundError extends Error {
234
+ public constructor(
235
+ public readonly oldString: string,
236
+ public readonly closest: readonly ClosestRegion[]
237
+ ) {
238
+ super("oldString was not found in the file.");
239
+ this.name = "NotFoundError";
240
+ }
241
+ }
242
+
243
+ class MultipleMatchesError extends Error {
244
+ public constructor(public readonly oldString: string) {
245
+ super(
246
+ "oldString matched multiple regions. Use enough surrounding context to make oldString unique, or set replaceAll=true to replace all matching oldString."
247
+ );
248
+ this.name = "MultipleMatchesError";
249
+ }
250
+ }
251
+
252
+ function windowCandidates(
253
+ input: MatchInput,
254
+ windowSize: number,
255
+ normalize: (text: string) => string,
256
+ normalizedFind: string
257
+ ): readonly Candidate[] {
258
+ const { content, contentLines } = input;
259
+ const candidates: Candidate[] = [];
260
+
261
+ for (let index = 0; index + windowSize <= contentLines.length; index += 1) {
262
+ if (
263
+ normalize(windowText(contentLines, index, windowSize)) === normalizedFind
264
+ ) {
265
+ candidates.push(candidateAt(content, contentLines, index, windowSize));
266
+ }
267
+ }
268
+
269
+ return candidates;
270
+ }
271
+
272
+ function anchoredCandidates(
273
+ input: MatchInput,
274
+ accept: (start: number) => boolean
275
+ ): readonly Candidate[] {
276
+ const { content, contentLines, searchLines } = input;
277
+
278
+ if (searchLines.length < 3) {
279
+ return [];
280
+ }
281
+
282
+ const first = searchLines[0]?.trim() ?? "";
283
+ const last = searchLines.at(-1)?.trim() ?? "";
284
+ const candidates: Candidate[] = [];
285
+
286
+ for (
287
+ let index = 0;
288
+ index + searchLines.length <= contentLines.length;
289
+ index += 1
290
+ ) {
291
+ const endIndex = index + searchLines.length - 1;
292
+
293
+ if (
294
+ contentLines[index]?.text.trim() !== first ||
295
+ contentLines[endIndex]?.text.trim() !== last
296
+ ) {
297
+ continue;
298
+ }
299
+
300
+ if (accept(index)) {
301
+ candidates.push(
302
+ candidateAt(content, contentLines, index, searchLines.length)
303
+ );
304
+ }
305
+ }
306
+
307
+ return candidates;
308
+ }
309
+
310
+ function removeIndentation(text: string): string {
311
+ const lines = text.split("\n");
312
+ const nonEmpty = lines.filter((line) => line.trim().length > 0);
313
+
314
+ if (nonEmpty.length === 0) {
315
+ return text;
316
+ }
317
+
318
+ const minIndent = Math.min(
319
+ ...nonEmpty.map((line) => line.match(/^(\s*)/u)?.[1]?.length ?? 0)
320
+ );
321
+
322
+ return lines
323
+ .map((line) => (line.trim().length === 0 ? line : line.slice(minIndent)))
324
+ .join("\n");
325
+ }
326
+
327
+ const trimLines = (text: string): string =>
328
+ text
329
+ .split("\n")
330
+ .map((line) => line.trim())
331
+ .join("\n");
332
+
333
+ const collapseWhitespace = (text: string): string =>
334
+ text.replace(/\s+/gu, " ").trim();
335
+
336
+ function lineTrimmed(input: MatchInput): readonly Candidate[] {
337
+ return windowCandidates(
338
+ input,
339
+ input.searchLines.length,
340
+ trimLines,
341
+ input.searchLines.map((line) => line.trim()).join("\n")
342
+ );
343
+ }
344
+
345
+ function whitespaceNormalized(input: MatchInput): readonly Candidate[] {
346
+ const { content, oldString, contentLines, searchLines } = input;
347
+ const normalizedFind = collapseWhitespace(oldString);
348
+ const candidates: Candidate[] = [];
349
+ let flexible: RegExp | undefined;
350
+
351
+ for (const line of contentLines) {
352
+ const normalizedLine = collapseWhitespace(line.text);
353
+
354
+ if (normalizedLine === normalizedFind) {
355
+ candidates.push({
356
+ range: [line.start, line.end],
357
+ text: content.slice(line.start, line.end),
358
+ });
359
+ continue;
360
+ }
361
+
362
+ if (normalizedLine.includes(normalizedFind)) {
363
+ flexible ??= new RegExp(
364
+ oldString
365
+ .trim()
366
+ .split(/\s+/u)
367
+ .map((word) => escapeRegex(word))
368
+ .join("\\s+"),
369
+ "gu"
370
+ );
371
+
372
+ for (const match of line.text.matchAll(flexible)) {
373
+ if (match.index === undefined) {
374
+ continue;
375
+ }
376
+ const start = line.start + match.index;
377
+ candidates.push({
378
+ range: [start, start + match[0].length],
379
+ text: match[0],
380
+ });
381
+ }
382
+ }
383
+ }
384
+
385
+ if (searchLines.length > 1) {
386
+ candidates.push(
387
+ ...windowCandidates(
388
+ input,
389
+ searchLines.length,
390
+ collapseWhitespace,
391
+ normalizedFind
392
+ )
393
+ );
394
+ }
395
+
396
+ return candidates;
397
+ }
398
+
399
+ function indentationFlexible(input: MatchInput): readonly Candidate[] {
400
+ return windowCandidates(
401
+ input,
402
+ input.searchLines.length,
403
+ removeIndentation,
404
+ removeIndentation(input.oldString)
405
+ );
406
+ }
407
+
408
+ function escapeNormalized(input: MatchInput): readonly Candidate[] {
409
+ const unescaped = unescapeString(input.oldString);
410
+
411
+ return [
412
+ ...findAll(input.content, unescaped),
413
+ ...windowCandidates(
414
+ input,
415
+ logicalLines(unescaped).length,
416
+ unescapeString,
417
+ unescaped
418
+ ),
419
+ ];
420
+ }
421
+
422
+ function trimmedBoundary(input: MatchInput): readonly Candidate[] {
423
+ const trimmed = input.oldString.trim();
424
+
425
+ if (trimmed === input.oldString || trimmed.length === 0) {
426
+ return [];
427
+ }
428
+
429
+ return [
430
+ ...findAll(input.content, trimmed),
431
+ ...windowCandidates(
432
+ input,
433
+ input.searchLines.length,
434
+ (text) => text.trim(),
435
+ trimmed
436
+ ),
437
+ ];
438
+ }
439
+
440
+ function unicodeNormalized({
441
+ content,
442
+ oldString,
443
+ }: MatchInput): readonly Candidate[] {
444
+ // normalizeUnicode substitutions must stay 1:1 by UTF-16 code unit, or offsets desync.
445
+ const normalizedContent = normalizeUnicode(content);
446
+ const normalizedOld = normalizeUnicode(oldString);
447
+ const candidates: Candidate[] = [];
448
+ let index = 0;
449
+
450
+ while (true) {
451
+ const start = normalizedContent.indexOf(normalizedOld, index);
452
+
453
+ if (start === -1) {
454
+ break;
455
+ }
456
+
457
+ const end = start + oldString.length;
458
+ candidates.push({ range: [start, end], text: content.slice(start, end) });
459
+ index = Math.max(start + 1, end);
460
+ }
461
+
462
+ return candidates;
463
+ }
464
+
465
+ function blockAnchor(input: MatchInput): readonly Candidate[] {
466
+ const { contentLines, searchLines } = input;
467
+ const middleCount = Math.max(1, searchLines.length - 2);
468
+
469
+ return anchoredCandidates(input, (start) => {
470
+ let similarity = 0;
471
+
472
+ for (let offset = 1; offset < searchLines.length - 1; offset += 1) {
473
+ similarity +=
474
+ lineSimilarity(
475
+ searchLines[offset] ?? "",
476
+ contentLines[start + offset]?.text ?? ""
477
+ ) / middleCount;
478
+ }
479
+
480
+ // 0.3 floor filters anchor coincidence on unrelated blocks sharing first/last line text.
481
+ return similarity >= 0.3;
482
+ });
483
+ }
484
+
485
+ function contextAware(input: MatchInput): readonly Candidate[] {
486
+ const { contentLines, searchLines } = input;
487
+
488
+ return anchoredCandidates(input, (start) => {
489
+ let matching = 0;
490
+ let total = 0;
491
+
492
+ for (let offset = 1; offset < searchLines.length - 1; offset += 1) {
493
+ const actual = contentLines[start + offset]?.text.trim() ?? "";
494
+ const expected = searchLines[offset]?.trim() ?? "";
495
+
496
+ if (actual.length > 0 || expected.length > 0) {
497
+ total += 1;
498
+ if (actual === expected) {
499
+ matching += 1;
500
+ }
501
+ }
502
+ }
503
+
504
+ return total === 0 || matching / total >= 0.5;
505
+ });
506
+ }
507
+
508
+ const strategies: readonly Strategy[] = [
509
+ {
510
+ name: "simple",
511
+ find: ({ content, oldString }) => findAll(content, oldString),
512
+ },
513
+ { name: "lineTrimmed", find: lineTrimmed },
514
+ { name: "whitespaceNormalized", find: whitespaceNormalized },
515
+ { name: "indentationFlexible", find: indentationFlexible },
516
+ { name: "escapeNormalized", find: escapeNormalized },
517
+ { name: "trimmedBoundary", find: trimmedBoundary },
518
+ { name: "unicodeNormalized", find: unicodeNormalized },
519
+ { name: "blockAnchor", find: blockAnchor },
520
+ { name: "contextAware", find: contextAware },
521
+ ];
522
+
523
+ function findAll(content: string, search: string): readonly Candidate[] {
524
+ const candidates: Candidate[] = [];
525
+
526
+ if (search.length === 0) {
527
+ return candidates;
528
+ }
529
+
530
+ let index = 0;
531
+
532
+ while (true) {
533
+ const start = content.indexOf(search, index);
534
+
535
+ if (start === -1) {
536
+ return candidates;
537
+ }
538
+
539
+ candidates.push({
540
+ range: [start, start + search.length],
541
+ text: content.slice(start, start + search.length),
542
+ });
543
+ index = Math.max(start + 1, start + search.length);
544
+ }
545
+ }
546
+
547
+ function windowText(
548
+ lines: readonly OffsetLine[],
549
+ start: number,
550
+ size: number
551
+ ): string {
552
+ let text = "";
553
+
554
+ for (let offset = 0; offset < size; offset += 1) {
555
+ if (offset > 0) {
556
+ text += "\n";
557
+ }
558
+ text += lines[start + offset]?.text ?? "";
559
+ }
560
+
561
+ return text;
562
+ }
563
+
564
+ function candidateAt(
565
+ content: string,
566
+ lines: readonly OffsetLine[],
567
+ start: number,
568
+ size: number
569
+ ): Candidate {
570
+ const first = lines[start];
571
+ const last = lines[start + size - 1];
572
+
573
+ if (first === undefined || last === undefined) {
574
+ return { range: [0, 0], text: "" };
575
+ }
576
+
577
+ return {
578
+ range: [first.start, last.end],
579
+ text: content.slice(first.start, last.end),
580
+ };
581
+ }
582
+
583
+ function dedupeCandidates(
584
+ candidates: readonly Candidate[]
585
+ ): readonly Candidate[] {
586
+ const seen = new Set<string>();
587
+ const deduped: Candidate[] = [];
588
+
589
+ for (const candidate of candidates) {
590
+ const key = `${candidate.range[0]}:${candidate.range[1]}`;
591
+
592
+ if (!seen.has(key)) {
593
+ seen.add(key);
594
+ deduped.push(candidate);
595
+ }
596
+ }
597
+
598
+ return deduped;
599
+ }
600
+
601
+ function sortRanges(ranges: readonly EditRange[]): readonly EditRange[] {
602
+ return [...ranges].sort((left, right) => left[0] - right[0]);
603
+ }
604
+
605
+ function logicalLines(content: string): readonly string[] {
606
+ if (content.length === 0) {
607
+ return [];
608
+ }
609
+
610
+ const lines = content.split(/\r?\n/u);
611
+
612
+ if (lines.at(-1) === "") {
613
+ lines.pop();
614
+ }
615
+
616
+ return lines;
617
+ }
618
+
619
+ function offsetLines(content: string): readonly OffsetLine[] {
620
+ if (content.length === 0) {
621
+ return [];
622
+ }
623
+
624
+ const lines: OffsetLine[] = [];
625
+ let start = 0;
626
+
627
+ while (start <= content.length) {
628
+ const newline = content.indexOf("\n", start);
629
+ const end = newline === -1 ? content.length : newline;
630
+ const textEnd = end > start && content[end - 1] === "\r" ? end - 1 : end;
631
+
632
+ if (start === content.length && newline === -1) {
633
+ break;
634
+ }
635
+
636
+ lines.push({
637
+ start,
638
+ end,
639
+ text: content.slice(start, textEnd),
640
+ });
641
+
642
+ if (newline === -1) {
643
+ break;
644
+ }
645
+
646
+ start = newline + 1;
647
+ }
648
+
649
+ if (lines.at(-1)?.text === "" && content.endsWith("\n")) {
650
+ lines.pop();
651
+ }
652
+
653
+ return lines;
654
+ }
655
+
656
+ function unescapeString(text: string): string {
657
+ return text.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/gu, (match, value) => {
658
+ switch (value) {
659
+ case "n":
660
+ return "\n";
661
+ case "t":
662
+ return "\t";
663
+ case "r":
664
+ return "\r";
665
+ case "'":
666
+ return "'";
667
+ case '"':
668
+ return '"';
669
+ case "`":
670
+ return "`";
671
+ case "\\":
672
+ return "\\";
673
+ case "\n":
674
+ return "\n";
675
+ case "$":
676
+ return "$";
677
+ default:
678
+ return match;
679
+ }
680
+ });
681
+ }
682
+
683
+ function escapeSequences(text: string): readonly string[] {
684
+ return text.match(/\\(?:n|t|r|'|"|`|\$|\\)/gu) ?? [];
685
+ }
686
+
687
+ function normalizeUnicode(text: string): string {
688
+ return text.replace(/[‘’‚‛“”„‟‐‑‒–—―− -  ]/gu, (value) => {
689
+ switch (value) {
690
+ case "‘":
691
+ case "’":
692
+ case "‚":
693
+ case "‛":
694
+ return "'";
695
+ case "“":
696
+ case "”":
697
+ case "„":
698
+ case "‟":
699
+ return '"';
700
+ case "‐":
701
+ case "‑":
702
+ case "‒":
703
+ case "–":
704
+ case "—":
705
+ case "―":
706
+ case "−":
707
+ return "-";
708
+ default:
709
+ return " ";
710
+ }
711
+ });
712
+ }
713
+
714
+ function lineSimilarity(left: string, right: string): number {
715
+ const a = left.trim();
716
+ const b = right.trim();
717
+ const max = Math.max(a.length, b.length);
718
+
719
+ if (max === 0) {
720
+ return 1;
721
+ }
722
+
723
+ return 1 - Levenshtein.distance(a, b) / max;
724
+ }
725
+
726
+ function lineNumberAt(content: string, offset: number): number {
727
+ let line = 1;
728
+ let index = 0;
729
+
730
+ while (index < offset) {
731
+ const newline = content.indexOf("\n", index);
732
+
733
+ if (newline === -1 || newline >= offset) {
734
+ break;
735
+ }
736
+
737
+ line += 1;
738
+ index = newline + 1;
739
+ }
740
+
741
+ return line;
742
+ }
743
+
744
+ function formatLineRange(start: number, end: number): string {
745
+ return start === end ? String(start) : `${start}-${end}`;
746
+ }
747
+
748
+ function escapeRegex(text: string): string {
749
+ return text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
750
+ }
751
+
752
+ type OffsetLine = {
753
+ readonly start: number;
754
+ readonly end: number;
755
+ readonly text: string;
756
+ };
757
+
758
+ export const EditMatcher = {
759
+ resolve,
760
+ applyAll,
761
+ assertNoEscapeDrift,
762
+ findClosestRegions,
763
+ renderNotFound,
764
+ lineRangeFor,
765
+ NotFoundError,
766
+ MultipleMatchesError,
767
+ };