@cdoing/core 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 (378) hide show
  1. package/dist/agents/coordinator.d.ts +114 -0
  2. package/dist/agents/coordinator.d.ts.map +1 -0
  3. package/dist/agents/coordinator.js +158 -0
  4. package/dist/agents/coordinator.js.map +1 -0
  5. package/dist/context-providers/clipboard.d.ts +13 -0
  6. package/dist/context-providers/clipboard.d.ts.map +1 -0
  7. package/dist/context-providers/clipboard.js +53 -0
  8. package/dist/context-providers/clipboard.js.map +1 -0
  9. package/dist/context-providers/codebase.d.ts +46 -0
  10. package/dist/context-providers/codebase.d.ts.map +1 -0
  11. package/dist/context-providers/codebase.js +273 -0
  12. package/dist/context-providers/codebase.js.map +1 -0
  13. package/dist/context-providers/diff.d.ts +18 -0
  14. package/dist/context-providers/diff.d.ts.map +1 -0
  15. package/dist/context-providers/diff.js +63 -0
  16. package/dist/context-providers/diff.js.map +1 -0
  17. package/dist/context-providers/docs.d.ts +21 -0
  18. package/dist/context-providers/docs.d.ts.map +1 -0
  19. package/dist/context-providers/docs.js +180 -0
  20. package/dist/context-providers/docs.js.map +1 -0
  21. package/dist/context-providers/file-include.d.ts +13 -0
  22. package/dist/context-providers/file-include.d.ts.map +1 -0
  23. package/dist/context-providers/file-include.js +82 -0
  24. package/dist/context-providers/file-include.js.map +1 -0
  25. package/dist/context-providers/folder.d.ts +19 -0
  26. package/dist/context-providers/folder.d.ts.map +1 -0
  27. package/dist/context-providers/folder.js +130 -0
  28. package/dist/context-providers/folder.js.map +1 -0
  29. package/dist/context-providers/git.d.ts +19 -0
  30. package/dist/context-providers/git.d.ts.map +1 -0
  31. package/dist/context-providers/git.js +74 -0
  32. package/dist/context-providers/git.js.map +1 -0
  33. package/dist/context-providers/index.d.ts +26 -0
  34. package/dist/context-providers/index.d.ts.map +1 -0
  35. package/dist/context-providers/index.js +37 -0
  36. package/dist/context-providers/index.js.map +1 -0
  37. package/dist/context-providers/open-files.d.ts +25 -0
  38. package/dist/context-providers/open-files.d.ts.map +1 -0
  39. package/dist/context-providers/open-files.js +134 -0
  40. package/dist/context-providers/open-files.js.map +1 -0
  41. package/dist/context-providers/problems.d.ts +24 -0
  42. package/dist/context-providers/problems.d.ts.map +1 -0
  43. package/dist/context-providers/problems.js +97 -0
  44. package/dist/context-providers/problems.js.map +1 -0
  45. package/dist/context-providers/registry.d.ts +61 -0
  46. package/dist/context-providers/registry.d.ts.map +1 -0
  47. package/dist/context-providers/registry.js +92 -0
  48. package/dist/context-providers/registry.js.map +1 -0
  49. package/dist/context-providers/terminal.d.ts +25 -0
  50. package/dist/context-providers/terminal.d.ts.map +1 -0
  51. package/dist/context-providers/terminal.js +55 -0
  52. package/dist/context-providers/terminal.js.map +1 -0
  53. package/dist/context-providers/tree.d.ts +29 -0
  54. package/dist/context-providers/tree.d.ts.map +1 -0
  55. package/dist/context-providers/tree.js +172 -0
  56. package/dist/context-providers/tree.js.map +1 -0
  57. package/dist/context-providers/types.d.ts +72 -0
  58. package/dist/context-providers/types.d.ts.map +1 -0
  59. package/dist/context-providers/types.js +10 -0
  60. package/dist/context-providers/types.js.map +1 -0
  61. package/dist/context-providers/url.d.ts +27 -0
  62. package/dist/context-providers/url.d.ts.map +1 -0
  63. package/dist/context-providers/url.js +131 -0
  64. package/dist/context-providers/url.js.map +1 -0
  65. package/dist/effort/index.d.ts +78 -0
  66. package/dist/effort/index.d.ts.map +1 -0
  67. package/dist/effort/index.js +146 -0
  68. package/dist/effort/index.js.map +1 -0
  69. package/dist/hooks/index.d.ts +47 -0
  70. package/dist/hooks/index.d.ts.map +1 -0
  71. package/dist/hooks/index.js +151 -0
  72. package/dist/hooks/index.js.map +1 -0
  73. package/dist/index.d.ts +75 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +152 -0
  76. package/dist/index.js.map +1 -0
  77. package/dist/indexing/chunker.d.ts +25 -0
  78. package/dist/indexing/chunker.d.ts.map +1 -0
  79. package/dist/indexing/chunker.js +217 -0
  80. package/dist/indexing/chunker.js.map +1 -0
  81. package/dist/indexing/database.d.ts +49 -0
  82. package/dist/indexing/database.d.ts.map +1 -0
  83. package/dist/indexing/database.js +287 -0
  84. package/dist/indexing/database.js.map +1 -0
  85. package/dist/indexing/index.d.ts +9 -0
  86. package/dist/indexing/index.d.ts.map +1 -0
  87. package/dist/indexing/index.js +13 -0
  88. package/dist/indexing/index.js.map +1 -0
  89. package/dist/indexing/indexer.d.ts +63 -0
  90. package/dist/indexing/indexer.d.ts.map +1 -0
  91. package/dist/indexing/indexer.js +352 -0
  92. package/dist/indexing/indexer.js.map +1 -0
  93. package/dist/indexing/recent-edits-cache.d.ts +77 -0
  94. package/dist/indexing/recent-edits-cache.d.ts.map +1 -0
  95. package/dist/indexing/recent-edits-cache.js +123 -0
  96. package/dist/indexing/recent-edits-cache.js.map +1 -0
  97. package/dist/indexing/types.d.ts +39 -0
  98. package/dist/indexing/types.d.ts.map +1 -0
  99. package/dist/indexing/types.js +6 -0
  100. package/dist/indexing/types.js.map +1 -0
  101. package/dist/mcp/index.d.ts +33 -0
  102. package/dist/mcp/index.d.ts.map +1 -0
  103. package/dist/mcp/index.js +37 -0
  104. package/dist/mcp/index.js.map +1 -0
  105. package/dist/mcp/manager.d.ts +123 -0
  106. package/dist/mcp/manager.d.ts.map +1 -0
  107. package/dist/mcp/manager.js +331 -0
  108. package/dist/mcp/manager.js.map +1 -0
  109. package/dist/oauth.d.ts +33 -0
  110. package/dist/oauth.d.ts.map +1 -0
  111. package/dist/oauth.js +312 -0
  112. package/dist/oauth.js.map +1 -0
  113. package/dist/permissions/index.d.ts +216 -0
  114. package/dist/permissions/index.d.ts.map +1 -0
  115. package/dist/permissions/index.js +938 -0
  116. package/dist/permissions/index.js.map +1 -0
  117. package/dist/plan/index.d.ts +20 -0
  118. package/dist/plan/index.d.ts.map +1 -0
  119. package/dist/plan/index.js +24 -0
  120. package/dist/plan/index.js.map +1 -0
  121. package/dist/plan/manager.d.ts +101 -0
  122. package/dist/plan/manager.d.ts.map +1 -0
  123. package/dist/plan/manager.js +170 -0
  124. package/dist/plan/manager.js.map +1 -0
  125. package/dist/rules/index.d.ts +28 -0
  126. package/dist/rules/index.d.ts.map +1 -0
  127. package/dist/rules/index.js +31 -0
  128. package/dist/rules/index.js.map +1 -0
  129. package/dist/rules/manager.d.ts +77 -0
  130. package/dist/rules/manager.d.ts.map +1 -0
  131. package/dist/rules/manager.js +279 -0
  132. package/dist/rules/manager.js.map +1 -0
  133. package/dist/rules/types.d.ts +34 -0
  134. package/dist/rules/types.d.ts.map +1 -0
  135. package/dist/rules/types.js +9 -0
  136. package/dist/rules/types.js.map +1 -0
  137. package/dist/sandbox/filesystem.d.ts +20 -0
  138. package/dist/sandbox/filesystem.d.ts.map +1 -0
  139. package/dist/sandbox/filesystem.js +141 -0
  140. package/dist/sandbox/filesystem.js.map +1 -0
  141. package/dist/sandbox/index.d.ts +4 -0
  142. package/dist/sandbox/index.d.ts.map +1 -0
  143. package/dist/sandbox/index.js +8 -0
  144. package/dist/sandbox/index.js.map +1 -0
  145. package/dist/sandbox/manager.d.ts +47 -0
  146. package/dist/sandbox/manager.d.ts.map +1 -0
  147. package/dist/sandbox/manager.js +220 -0
  148. package/dist/sandbox/manager.js.map +1 -0
  149. package/dist/sandbox/network.d.ts +14 -0
  150. package/dist/sandbox/network.d.ts.map +1 -0
  151. package/dist/sandbox/network.js +87 -0
  152. package/dist/sandbox/network.js.map +1 -0
  153. package/dist/sandbox/types.d.ts +42 -0
  154. package/dist/sandbox/types.d.ts.map +1 -0
  155. package/dist/sandbox/types.js +25 -0
  156. package/dist/sandbox/types.js.map +1 -0
  157. package/dist/tools/ast-edit.d.ts +57 -0
  158. package/dist/tools/ast-edit.d.ts.map +1 -0
  159. package/dist/tools/ast-edit.js +443 -0
  160. package/dist/tools/ast-edit.js.map +1 -0
  161. package/dist/tools/code-verify.d.ts +8 -0
  162. package/dist/tools/code-verify.d.ts.map +1 -0
  163. package/dist/tools/code-verify.js +159 -0
  164. package/dist/tools/code-verify.js.map +1 -0
  165. package/dist/tools/codebase-search.d.ts +17 -0
  166. package/dist/tools/codebase-search.d.ts.map +1 -0
  167. package/dist/tools/codebase-search.js +104 -0
  168. package/dist/tools/codebase-search.js.map +1 -0
  169. package/dist/tools/file-delete.d.ts +26 -0
  170. package/dist/tools/file-delete.d.ts.map +1 -0
  171. package/dist/tools/file-delete.js +179 -0
  172. package/dist/tools/file-delete.js.map +1 -0
  173. package/dist/tools/file-edit.d.ts +10 -0
  174. package/dist/tools/file-edit.d.ts.map +1 -0
  175. package/dist/tools/file-edit.js +138 -0
  176. package/dist/tools/file-edit.js.map +1 -0
  177. package/dist/tools/file-read.d.ts +12 -0
  178. package/dist/tools/file-read.d.ts.map +1 -0
  179. package/dist/tools/file-read.js +211 -0
  180. package/dist/tools/file-read.js.map +1 -0
  181. package/dist/tools/file-run.d.ts +10 -0
  182. package/dist/tools/file-run.d.ts.map +1 -0
  183. package/dist/tools/file-run.js +179 -0
  184. package/dist/tools/file-run.js.map +1 -0
  185. package/dist/tools/file-write.d.ts +10 -0
  186. package/dist/tools/file-write.d.ts.map +1 -0
  187. package/dist/tools/file-write.js +134 -0
  188. package/dist/tools/file-write.js.map +1 -0
  189. package/dist/tools/glob-search.d.ts +8 -0
  190. package/dist/tools/glob-search.d.ts.map +1 -0
  191. package/dist/tools/glob-search.js +108 -0
  192. package/dist/tools/glob-search.js.map +1 -0
  193. package/dist/tools/grep-search.d.ts +8 -0
  194. package/dist/tools/grep-search.d.ts.map +1 -0
  195. package/dist/tools/grep-search.js +139 -0
  196. package/dist/tools/grep-search.js.map +1 -0
  197. package/dist/tools/list-dir.d.ts +16 -0
  198. package/dist/tools/list-dir.d.ts.map +1 -0
  199. package/dist/tools/list-dir.js +183 -0
  200. package/dist/tools/list-dir.js.map +1 -0
  201. package/dist/tools/multi-edit.d.ts +16 -0
  202. package/dist/tools/multi-edit.d.ts.map +1 -0
  203. package/dist/tools/multi-edit.js +163 -0
  204. package/dist/tools/multi-edit.js.map +1 -0
  205. package/dist/tools/notebook-edit.d.ts +31 -0
  206. package/dist/tools/notebook-edit.d.ts.map +1 -0
  207. package/dist/tools/notebook-edit.js +321 -0
  208. package/dist/tools/notebook-edit.js.map +1 -0
  209. package/dist/tools/registry.d.ts +16 -0
  210. package/dist/tools/registry.d.ts.map +1 -0
  211. package/dist/tools/registry.js +41 -0
  212. package/dist/tools/registry.js.map +1 -0
  213. package/dist/tools/shell-exec.d.ts +12 -0
  214. package/dist/tools/shell-exec.d.ts.map +1 -0
  215. package/dist/tools/shell-exec.js +261 -0
  216. package/dist/tools/shell-exec.js.map +1 -0
  217. package/dist/tools/sub-agent-manager.d.ts +57 -0
  218. package/dist/tools/sub-agent-manager.d.ts.map +1 -0
  219. package/dist/tools/sub-agent-manager.js +153 -0
  220. package/dist/tools/sub-agent-manager.js.map +1 -0
  221. package/dist/tools/sub-agent-status.d.ts +12 -0
  222. package/dist/tools/sub-agent-status.d.ts.map +1 -0
  223. package/dist/tools/sub-agent-status.js +59 -0
  224. package/dist/tools/sub-agent-status.js.map +1 -0
  225. package/dist/tools/sub-agent-terminate.d.ts +12 -0
  226. package/dist/tools/sub-agent-terminate.d.ts.map +1 -0
  227. package/dist/tools/sub-agent-terminate.js +55 -0
  228. package/dist/tools/sub-agent-terminate.js.map +1 -0
  229. package/dist/tools/sub-agent.d.ts +34 -0
  230. package/dist/tools/sub-agent.d.ts.map +1 -0
  231. package/dist/tools/sub-agent.js +140 -0
  232. package/dist/tools/sub-agent.js.map +1 -0
  233. package/dist/tools/system-info.d.ts +24 -0
  234. package/dist/tools/system-info.d.ts.map +1 -0
  235. package/dist/tools/system-info.js +220 -0
  236. package/dist/tools/system-info.js.map +1 -0
  237. package/dist/tools/todo.d.ts +16 -0
  238. package/dist/tools/todo.d.ts.map +1 -0
  239. package/dist/tools/todo.js +144 -0
  240. package/dist/tools/todo.js.map +1 -0
  241. package/dist/tools/types.d.ts +20 -0
  242. package/dist/tools/types.d.ts.map +1 -0
  243. package/dist/tools/types.js +3 -0
  244. package/dist/tools/types.js.map +1 -0
  245. package/dist/tools/view-diff.d.ts +11 -0
  246. package/dist/tools/view-diff.d.ts.map +1 -0
  247. package/dist/tools/view-diff.js +88 -0
  248. package/dist/tools/view-diff.js.map +1 -0
  249. package/dist/tools/view-repo-map.d.ts +18 -0
  250. package/dist/tools/view-repo-map.d.ts.map +1 -0
  251. package/dist/tools/view-repo-map.js +245 -0
  252. package/dist/tools/view-repo-map.js.map +1 -0
  253. package/dist/tools/web-fetch.d.ts +13 -0
  254. package/dist/tools/web-fetch.d.ts.map +1 -0
  255. package/dist/tools/web-fetch.js +106 -0
  256. package/dist/tools/web-fetch.js.map +1 -0
  257. package/dist/tools/web-search.d.ts +10 -0
  258. package/dist/tools/web-search.d.ts.map +1 -0
  259. package/dist/tools/web-search.js +106 -0
  260. package/dist/tools/web-search.js.map +1 -0
  261. package/dist/utils/gitignore.d.ts +10 -0
  262. package/dist/utils/gitignore.d.ts.map +1 -0
  263. package/dist/utils/gitignore.js +104 -0
  264. package/dist/utils/gitignore.js.map +1 -0
  265. package/dist/utils/lazy-apply.d.ts +45 -0
  266. package/dist/utils/lazy-apply.d.ts.map +1 -0
  267. package/dist/utils/lazy-apply.js +164 -0
  268. package/dist/utils/lazy-apply.js.map +1 -0
  269. package/dist/utils/memory.d.ts +36 -0
  270. package/dist/utils/memory.d.ts.map +1 -0
  271. package/dist/utils/memory.js +136 -0
  272. package/dist/utils/memory.js.map +1 -0
  273. package/dist/utils/path-matching.d.ts +24 -0
  274. package/dist/utils/path-matching.d.ts.map +1 -0
  275. package/dist/utils/path-matching.js +116 -0
  276. package/dist/utils/path-matching.js.map +1 -0
  277. package/dist/utils/path-safety.d.ts +13 -0
  278. package/dist/utils/path-safety.d.ts.map +1 -0
  279. package/dist/utils/path-safety.js +54 -0
  280. package/dist/utils/path-safety.js.map +1 -0
  281. package/dist/utils/project-config.d.ts +18 -0
  282. package/dist/utils/project-config.d.ts.map +1 -0
  283. package/dist/utils/project-config.js +76 -0
  284. package/dist/utils/project-config.js.map +1 -0
  285. package/dist/utils/search-match.d.ts +63 -0
  286. package/dist/utils/search-match.d.ts.map +1 -0
  287. package/dist/utils/search-match.js +426 -0
  288. package/dist/utils/search-match.js.map +1 -0
  289. package/dist/utils/shell-paths.d.ts +17 -0
  290. package/dist/utils/shell-paths.d.ts.map +1 -0
  291. package/dist/utils/shell-paths.js +107 -0
  292. package/dist/utils/shell-paths.js.map +1 -0
  293. package/dist/utils/streaming-diff.d.ts +45 -0
  294. package/dist/utils/streaming-diff.d.ts.map +1 -0
  295. package/dist/utils/streaming-diff.js +230 -0
  296. package/dist/utils/streaming-diff.js.map +1 -0
  297. package/dist/utils/todo.d.ts +47 -0
  298. package/dist/utils/todo.d.ts.map +1 -0
  299. package/dist/utils/todo.js +102 -0
  300. package/dist/utils/todo.js.map +1 -0
  301. package/package.json +23 -0
  302. package/src/agents/coordinator.ts +240 -0
  303. package/src/context-providers/clipboard.ts +48 -0
  304. package/src/context-providers/codebase.ts +274 -0
  305. package/src/context-providers/diff.ts +66 -0
  306. package/src/context-providers/docs.ts +160 -0
  307. package/src/context-providers/file-include.ts +54 -0
  308. package/src/context-providers/folder.ts +106 -0
  309. package/src/context-providers/git.ts +72 -0
  310. package/src/context-providers/index.ts +26 -0
  311. package/src/context-providers/open-files.ts +113 -0
  312. package/src/context-providers/problems.ts +100 -0
  313. package/src/context-providers/registry.ts +99 -0
  314. package/src/context-providers/terminal.ts +58 -0
  315. package/src/context-providers/tree.ts +161 -0
  316. package/src/context-providers/types.ts +84 -0
  317. package/src/context-providers/url.ts +138 -0
  318. package/src/effort/index.ts +177 -0
  319. package/src/hooks/index.ts +148 -0
  320. package/src/index.ts +114 -0
  321. package/src/indexing/README.md +267 -0
  322. package/src/indexing/chunker.ts +206 -0
  323. package/src/indexing/database.ts +299 -0
  324. package/src/indexing/index.ts +15 -0
  325. package/src/indexing/indexer.ts +383 -0
  326. package/src/indexing/recent-edits-cache.ts +150 -0
  327. package/src/indexing/types.ts +44 -0
  328. package/src/mcp/index.ts +33 -0
  329. package/src/mcp/manager.ts +385 -0
  330. package/src/oauth.ts +330 -0
  331. package/src/permissions/index.ts +1011 -0
  332. package/src/plan/index.ts +20 -0
  333. package/src/plan/manager.ts +233 -0
  334. package/src/rules/index.ts +28 -0
  335. package/src/rules/manager.ts +276 -0
  336. package/src/rules/types.ts +40 -0
  337. package/src/sandbox/filesystem.ts +135 -0
  338. package/src/sandbox/index.ts +9 -0
  339. package/src/sandbox/manager.ts +213 -0
  340. package/src/sandbox/network.ts +101 -0
  341. package/src/sandbox/types.ts +63 -0
  342. package/src/tools/ast-edit.ts +493 -0
  343. package/src/tools/code-verify.ts +143 -0
  344. package/src/tools/codebase-search.ts +117 -0
  345. package/src/tools/file-delete.ts +155 -0
  346. package/src/tools/file-edit.ts +115 -0
  347. package/src/tools/file-read.ts +195 -0
  348. package/src/tools/file-run.ts +158 -0
  349. package/src/tools/file-write.ts +104 -0
  350. package/src/tools/glob-search.ts +80 -0
  351. package/src/tools/grep-search.ts +120 -0
  352. package/src/tools/list-dir.ts +172 -0
  353. package/src/tools/multi-edit.ts +138 -0
  354. package/src/tools/notebook-edit.ts +342 -0
  355. package/src/tools/registry.ts +43 -0
  356. package/src/tools/shell-exec.ts +251 -0
  357. package/src/tools/sub-agent-manager.ts +183 -0
  358. package/src/tools/sub-agent-status.ts +67 -0
  359. package/src/tools/sub-agent-terminate.ts +62 -0
  360. package/src/tools/sub-agent.ts +162 -0
  361. package/src/tools/system-info.ts +248 -0
  362. package/src/tools/todo.ts +149 -0
  363. package/src/tools/types.ts +21 -0
  364. package/src/tools/view-diff.ts +99 -0
  365. package/src/tools/view-repo-map.ts +249 -0
  366. package/src/tools/web-fetch.ts +118 -0
  367. package/src/tools/web-search.ts +129 -0
  368. package/src/utils/gitignore.ts +73 -0
  369. package/src/utils/lazy-apply.ts +189 -0
  370. package/src/utils/memory.ts +124 -0
  371. package/src/utils/path-matching.ts +84 -0
  372. package/src/utils/path-safety.ts +19 -0
  373. package/src/utils/project-config.ts +41 -0
  374. package/src/utils/search-match.ts +495 -0
  375. package/src/utils/shell-paths.ts +79 -0
  376. package/src/utils/streaming-diff.ts +260 -0
  377. package/src/utils/todo.ts +115 -0
  378. package/tsconfig.json +18 -0
@@ -0,0 +1,495 @@
1
+ /**
2
+ * Search Match — multi-strategy string matching for file editing.
3
+ *
4
+ * Inspired by Continue's cascading match strategies:
5
+ * 1. Exact match
6
+ * 2. Trimmed match (ignore leading/trailing whitespace)
7
+ * 3. Case-insensitive match
8
+ * 4. Whitespace-ignored match (strips all whitespace, maps back to original positions)
9
+ *
10
+ * This makes the edit tool far more resilient to LLM formatting differences.
11
+ */
12
+
13
+ export interface SearchMatchResult {
14
+ startIndex: number;
15
+ endIndex: number;
16
+ strategyName: string;
17
+ }
18
+
19
+ type MatchStrategy = (fileContent: string, searchContent: string) => SearchMatchResult | null;
20
+
21
+ /** 1. Exact string match */
22
+ function exactMatch(fileContent: string, searchContent: string): SearchMatchResult | null {
23
+ const idx = fileContent.indexOf(searchContent);
24
+ if (idx !== -1) {
25
+ return { startIndex: idx, endIndex: idx + searchContent.length, strategyName: "exact" };
26
+ }
27
+ return null;
28
+ }
29
+
30
+ /** 2. Trimmed match — ignore leading/trailing whitespace */
31
+ function trimmedMatch(fileContent: string, searchContent: string): SearchMatchResult | null {
32
+ const trimmed = searchContent.trim();
33
+ if (!trimmed) return null;
34
+ if (trimmed === searchContent) return null;
35
+ const idx = fileContent.indexOf(trimmed);
36
+ if (idx !== -1) {
37
+ return { startIndex: idx, endIndex: idx + trimmed.length, strategyName: "trimmed" };
38
+ }
39
+ return null;
40
+ }
41
+
42
+ /** 3. Case-insensitive match */
43
+ function caseInsensitiveMatch(fileContent: string, searchContent: string): SearchMatchResult | null {
44
+ const idx = fileContent.toLowerCase().indexOf(searchContent.toLowerCase());
45
+ if (idx !== -1) {
46
+ return { startIndex: idx, endIndex: idx + searchContent.length, strategyName: "caseInsensitive" };
47
+ }
48
+ return null;
49
+ }
50
+
51
+ /** 4. Whitespace-ignored match — strips all whitespace, maps positions back */
52
+ function whitespaceIgnoredMatch(fileContent: string, searchContent: string): SearchMatchResult | null {
53
+ const strippedFile = fileContent.replace(/\s/g, "");
54
+ const strippedSearch = searchContent.replace(/\s/g, "");
55
+
56
+ if (!strippedSearch) return null;
57
+
58
+ const strippedIdx = strippedFile.indexOf(strippedSearch);
59
+ if (strippedIdx === -1) return null;
60
+
61
+ // Map stripped position back to original
62
+ let originalStart = -1;
63
+ let nonWsCount = 0;
64
+
65
+ for (let i = 0; i < fileContent.length; i++) {
66
+ if (!/\s/.test(fileContent[i])) {
67
+ if (nonWsCount === strippedIdx) {
68
+ originalStart = i;
69
+ break;
70
+ }
71
+ nonWsCount++;
72
+ }
73
+ }
74
+
75
+ if (originalStart === -1) return null;
76
+
77
+ // Find end position
78
+ let originalEnd = originalStart;
79
+ let matchedChars = 0;
80
+
81
+ for (let i = originalStart; i < fileContent.length; i++) {
82
+ if (!/\s/.test(fileContent[i])) {
83
+ matchedChars++;
84
+ if (matchedChars === strippedSearch.length) {
85
+ originalEnd = i + 1;
86
+ break;
87
+ }
88
+ }
89
+ originalEnd = i + 1;
90
+ }
91
+
92
+ return { startIndex: originalStart, endIndex: originalEnd, strategyName: "whitespaceIgnored" };
93
+ }
94
+
95
+ /** 5. Jaro-Winkler fuzzy match — line-based sliding window with 90%+ threshold */
96
+ function fuzzyMatch(fileContent: string, searchContent: string): SearchMatchResult | null {
97
+ const searchBlock = searchContent.trim();
98
+ if (searchBlock.length < 10) return null; // Too short for meaningful fuzzy match
99
+
100
+ const searchLines = searchBlock.split("\n");
101
+ const fileLines = fileContent.split("\n");
102
+ if (searchLines.length > fileLines.length) return null;
103
+
104
+ let bestMatch: SearchMatchResult | null = null;
105
+ let bestSimilarity = 0;
106
+ const threshold = 0.9;
107
+
108
+ // Sliding window of searchLines.length over fileLines
109
+ for (let i = 0; i <= fileLines.length - searchLines.length; i++) {
110
+ const candidate = fileLines.slice(i, i + searchLines.length).join("\n").trim();
111
+ if (candidate.length < 5) continue;
112
+
113
+ const similarity = jaroWinklerSimilarity(searchBlock, candidate);
114
+ if (similarity >= threshold && similarity > bestSimilarity) {
115
+ const before = fileLines.slice(0, i).join("\n");
116
+ const startIndex = before.length + (i > 0 ? 1 : 0);
117
+ bestMatch = {
118
+ startIndex,
119
+ endIndex: startIndex + candidate.length,
120
+ strategyName: "fuzzy",
121
+ };
122
+ bestSimilarity = similarity;
123
+ }
124
+ }
125
+
126
+ return bestMatch;
127
+ }
128
+
129
+ function jaroSimilarity(s1: string, s2: string): number {
130
+ if (s1 === s2) return 1.0;
131
+ if (!s1.length || !s2.length) return 0.0;
132
+
133
+ const matchDist = Math.floor(Math.max(s1.length, s2.length) / 2) - 1;
134
+ if (matchDist < 0) return 0.0;
135
+
136
+ const s1m = new Array(s1.length).fill(false);
137
+ const s2m = new Array(s2.length).fill(false);
138
+ let matches = 0;
139
+ let transpositions = 0;
140
+
141
+ for (let i = 0; i < s1.length; i++) {
142
+ const lo = Math.max(0, i - matchDist);
143
+ const hi = Math.min(i + matchDist + 1, s2.length);
144
+ for (let j = lo; j < hi; j++) {
145
+ if (s2m[j] || s1[i] !== s2[j]) continue;
146
+ s1m[i] = s2m[j] = true;
147
+ matches++;
148
+ break;
149
+ }
150
+ }
151
+
152
+ if (!matches) return 0.0;
153
+
154
+ let k = 0;
155
+ for (let i = 0; i < s1.length; i++) {
156
+ if (!s1m[i]) continue;
157
+ while (!s2m[k]) k++;
158
+ if (s1[i] !== s2[k]) transpositions++;
159
+ k++;
160
+ }
161
+
162
+ return (matches / s1.length + matches / s2.length + (matches - transpositions / 2) / matches) / 3;
163
+ }
164
+
165
+ function jaroWinklerSimilarity(s1: string, s2: string): number {
166
+ const jaro = jaroSimilarity(s1, s2);
167
+ if (jaro < 0.7) return jaro;
168
+ let prefix = 0;
169
+ const max = Math.min(4, Math.min(s1.length, s2.length));
170
+ for (let i = 0; i < max; i++) {
171
+ if (s1[i] === s2[i]) prefix++;
172
+ else break;
173
+ }
174
+ return jaro + prefix * 0.1 * (1 - jaro);
175
+ }
176
+
177
+ const strategies: MatchStrategy[] = [
178
+ exactMatch,
179
+ trimmedMatch,
180
+ caseInsensitiveMatch,
181
+ whitespaceIgnoredMatch,
182
+ fuzzyMatch,
183
+ ];
184
+
185
+ /**
186
+ * Find a match for searchContent in fileContent using cascading strategies.
187
+ * Returns null if no strategy matches.
188
+ */
189
+ export function findSearchMatch(fileContent: string, searchContent: string): SearchMatchResult | null {
190
+ if (!searchContent.trim()) {
191
+ return { startIndex: 0, endIndex: 0, strategyName: "empty" };
192
+ }
193
+
194
+ for (const strategy of strategies) {
195
+ const result = strategy(fileContent, searchContent);
196
+ if (result) return result;
197
+ }
198
+
199
+ return null;
200
+ }
201
+
202
+ /**
203
+ * Find all matches for searchContent in fileContent.
204
+ */
205
+ export function findAllSearchMatches(fileContent: string, searchContent: string): SearchMatchResult[] {
206
+ if (!searchContent.trim()) {
207
+ return [{ startIndex: 0, endIndex: 0, strategyName: "empty" }];
208
+ }
209
+
210
+ const matches: SearchMatchResult[] = [];
211
+ let remaining = fileContent;
212
+ let offset = 0;
213
+
214
+ while (remaining.length > 0) {
215
+ const match = findSearchMatch(remaining, searchContent);
216
+ if (!match) break;
217
+
218
+ const adjusted: SearchMatchResult = {
219
+ startIndex: match.startIndex + offset,
220
+ endIndex: match.endIndex + offset,
221
+ strategyName: match.strategyName,
222
+ };
223
+
224
+ // Prevent infinite loops
225
+ if (matches.length > 0 && adjusted.startIndex <= matches[matches.length - 1].startIndex) break;
226
+
227
+ matches.push(adjusted);
228
+ offset = adjusted.endIndex;
229
+ remaining = fileContent.slice(offset);
230
+ }
231
+
232
+ return matches;
233
+ }
234
+
235
+ /**
236
+ * Execute a single find-and-replace on content.
237
+ * Uses multi-strategy matching. Returns the new content.
238
+ * Throws if match not found or multiple matches without replace_all.
239
+ *
240
+ * When a non-exact strategy is used, indentation is automatically
241
+ * adjusted so that new_string matches the file's original indentation
242
+ * rather than whatever the LLM happened to produce.
243
+ */
244
+ export function executeFindAndReplace(
245
+ content: string,
246
+ oldString: string,
247
+ newString: string,
248
+ replaceAll: boolean,
249
+ ): { result: string; count: number; strategy: string } {
250
+ if (oldString === newString) {
251
+ throw new Error("old_string and new_string are identical — no change needed");
252
+ }
253
+
254
+ const matches = findAllSearchMatches(content, oldString);
255
+
256
+ if (matches.length === 0) {
257
+ throw new Error(`old_string not found in file (tried exact, trimmed, case-insensitive, and whitespace-ignored matching)`);
258
+ }
259
+
260
+ if (!replaceAll && matches.length > 1) {
261
+ throw new Error(`Multiple matches found (${matches.length}). Use replace_all or add more surrounding context to make old_string unique.`);
262
+ }
263
+
264
+ const toReplace = replaceAll ? matches : [matches[0]];
265
+ const strategy = matches[0].strategyName;
266
+
267
+ // Apply replacements in reverse order to preserve positions
268
+ let result = content;
269
+ for (let i = toReplace.length - 1; i >= 0; i--) {
270
+ const m = toReplace[i];
271
+ const adjustedNewString = normalizeIndentation(content, m.startIndex, oldString, newString, strategy === "exact");
272
+ result = result.substring(0, m.startIndex) + adjustedNewString + result.substring(m.endIndex);
273
+ }
274
+
275
+ return { result, count: toReplace.length, strategy };
276
+ }
277
+
278
+ /**
279
+ * Normalize indentation of new_string so it matches the file's context.
280
+ *
281
+ * The key idea: preserve the RELATIVE indentation within new_string,
282
+ * but set the BASE indentation to match the target (the file's actual
283
+ * indentation at the match point).
284
+ *
285
+ * Example:
286
+ * File: " update: (id, data) => {\n return id;\n }" (base: 2 spaces)
287
+ * new_string: " modifyItem: (id, data) => {\n return id;\n }" (base: 6 spaces)
288
+ * Result: " modifyItem: (id, data) => {\n return id;\n }" (base: 2 spaces, relative preserved)
289
+ */
290
+ function normalizeIndentation(
291
+ fileContent: string,
292
+ matchStartIndex: number,
293
+ _oldString: string,
294
+ newString: string,
295
+ _isExact: boolean,
296
+ ): string {
297
+ // Get the full line indentation at the match point in the file
298
+ const fileLineIndent = getLineIndent(fileContent, matchStartIndex);
299
+
300
+ // How much of that indent is BEFORE matchStartIndex?
301
+ // (already in the file, outside the matched range — it stays in the output)
302
+ //
303
+ // For exact match: match includes the indent → preMatchIndent = ""
304
+ // For trimmed match: match starts after indent → preMatchIndent = fileLineIndent
305
+ const lineStartPos = matchStartIndex - fileLineIndent.length;
306
+ const preMatchIndent = fileContent.substring(lineStartPos, matchStartIndex);
307
+
308
+ // The first line of the replacement needs LESS indent because preMatchIndent
309
+ // is already in the file before the insertion point.
310
+ // Subsequent lines start on fresh lines (after \n) — they need the full indent.
311
+ const firstLineTargetIndent = fileLineIndent.substring(preMatchIndent.length);
312
+
313
+ // What base indentation does new_string currently have?
314
+ const newBaseIndent = getLeadingWhitespace(newString);
315
+
316
+ // If first-line target already matches new_string's base, no adjustment needed
317
+ if (firstLineTargetIndent === newBaseIndent) return newString;
318
+
319
+ // Re-indent every line: strip newBaseIndent, prepend the correct target indent.
320
+ const lines = newString.split("\n");
321
+ const adjusted = lines.map((line, i) => {
322
+ const lineIndent = getLeadingWhitespace(line);
323
+ const content = line.substring(lineIndent.length);
324
+
325
+ // Empty/whitespace-only lines: keep as-is
326
+ if (!content) return line;
327
+
328
+ // Compute relative indent (how much deeper than new_string's base)
329
+ let relativeIndent = "";
330
+ if (lineIndent.length >= newBaseIndent.length && lineIndent.startsWith(newBaseIndent)) {
331
+ relativeIndent = lineIndent.substring(newBaseIndent.length);
332
+ }
333
+
334
+ // First line: use firstLineTargetIndent (accounts for pre-match whitespace already in file)
335
+ // Subsequent lines: use full fileLineIndent (they start on fresh lines after \n)
336
+ const baseIndent = i === 0 ? firstLineTargetIndent : fileLineIndent;
337
+
338
+ return baseIndent + relativeIndent + content;
339
+ });
340
+
341
+ return adjusted.join("\n");
342
+ }
343
+
344
+ /** Get the indentation (leading whitespace) at a position in the file */
345
+ function getLineIndent(content: string, position: number): string {
346
+ // Walk backwards to find the start of the line
347
+ let lineStart = position;
348
+ while (lineStart > 0 && content[lineStart - 1] !== "\n") {
349
+ lineStart--;
350
+ }
351
+ // Extract leading whitespace from line start to the first non-whitespace
352
+ let indent = "";
353
+ for (let i = lineStart; i < content.length && (content[i] === " " || content[i] === "\t"); i++) {
354
+ indent += content[i];
355
+ }
356
+ return indent;
357
+ }
358
+
359
+ /** Get leading whitespace of a string's first line */
360
+ function getLeadingWhitespace(text: string): string {
361
+ const match = text.match(/^([ \t]*)/);
362
+ return match ? match[1] : "";
363
+ }
364
+
365
+
366
+ /**
367
+ * Execute multiple sequential find-and-replace operations.
368
+ * Each edit operates on the result of the previous one.
369
+ * Atomic: if any edit fails, none are applied (throws).
370
+ */
371
+ export function executeMultiFindAndReplace(
372
+ content: string,
373
+ edits: Array<{ old_string: string; new_string: string; replace_all?: boolean }>,
374
+ ): { result: string; totalCount: number } {
375
+ let current = content;
376
+ let totalCount = 0;
377
+
378
+ for (let i = 0; i < edits.length; i++) {
379
+ const edit = edits[i];
380
+ try {
381
+ const { result, count } = executeFindAndReplace(
382
+ current,
383
+ edit.old_string,
384
+ edit.new_string,
385
+ edit.replace_all ?? false,
386
+ );
387
+ current = result;
388
+ totalCount += count;
389
+ } catch (err) {
390
+ throw new Error(`Edit ${i + 1}/${edits.length} failed: ${(err as Error).message}`);
391
+ }
392
+ }
393
+
394
+ return { result: current, totalCount };
395
+ }
396
+
397
+ // ── Unified Diff Application ────────────────────────────────────────────────
398
+
399
+ /**
400
+ * Check if a string looks like a unified diff format.
401
+ */
402
+ export function isUnifiedDiff(text: string): boolean {
403
+ return /^@@\s+-\d+(?:,\d+)?\s+\+\d+(?:,\d+)?\s+@@/m.test(text);
404
+ }
405
+
406
+ /**
407
+ * Apply a unified diff to file content.
408
+ * Supports standard @@ -n,m +n,m @@ hunk headers.
409
+ * Tolerates minor whitespace differences in context lines.
410
+ */
411
+ export function applyUnifiedDiff(originalContent: string, diff: string): string {
412
+ const lines = diff.split("\n");
413
+ const sourceLines = originalContent.split("\n");
414
+ const resultLines = [...sourceLines];
415
+
416
+ // Parse hunks
417
+ const hunks: Array<{
418
+ oldStart: number;
419
+ oldCount: number;
420
+ newStart: number;
421
+ newCount: number;
422
+ lines: Array<{ type: "context" | "add" | "remove"; text: string }>;
423
+ }> = [];
424
+
425
+ let currentHunk: typeof hunks[0] | null = null;
426
+
427
+ for (const line of lines) {
428
+ // Skip file headers
429
+ if (line.startsWith("---") || line.startsWith("+++")) continue;
430
+
431
+ // Parse hunk header
432
+ const hunkMatch = line.match(/^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/);
433
+ if (hunkMatch) {
434
+ if (currentHunk) hunks.push(currentHunk);
435
+ currentHunk = {
436
+ oldStart: parseInt(hunkMatch[1], 10),
437
+ oldCount: parseInt(hunkMatch[2] ?? "1", 10),
438
+ newStart: parseInt(hunkMatch[3], 10),
439
+ newCount: parseInt(hunkMatch[4] ?? "1", 10),
440
+ lines: [],
441
+ };
442
+ continue;
443
+ }
444
+
445
+ if (!currentHunk) continue;
446
+
447
+ if (line.startsWith("+")) {
448
+ currentHunk.lines.push({ type: "add", text: line.substring(1) });
449
+ } else if (line.startsWith("-")) {
450
+ currentHunk.lines.push({ type: "remove", text: line.substring(1) });
451
+ } else if (line.startsWith(" ") || line === "") {
452
+ currentHunk.lines.push({ type: "context", text: line.startsWith(" ") ? line.substring(1) : line });
453
+ }
454
+ }
455
+
456
+ if (currentHunk) hunks.push(currentHunk);
457
+
458
+ if (hunks.length === 0) {
459
+ throw new Error("No valid hunks found in diff");
460
+ }
461
+
462
+ // Apply hunks in reverse order to preserve line numbers
463
+ for (let h = hunks.length - 1; h >= 0; h--) {
464
+ const hunk = hunks[h];
465
+ const startLine = hunk.oldStart - 1; // Convert to 0-based
466
+
467
+ // Collect removals and additions
468
+ const toRemove: number[] = [];
469
+ const toAdd: string[] = [];
470
+ let lineIdx = startLine;
471
+
472
+ for (const dl of hunk.lines) {
473
+ if (dl.type === "remove") {
474
+ toRemove.push(lineIdx);
475
+ lineIdx++;
476
+ } else if (dl.type === "add") {
477
+ toAdd.push(dl.text);
478
+ } else {
479
+ // context
480
+ lineIdx++;
481
+ }
482
+ }
483
+
484
+ // Remove lines (reverse order)
485
+ for (let i = toRemove.length - 1; i >= 0; i--) {
486
+ resultLines.splice(toRemove[i], 1);
487
+ }
488
+
489
+ // Insert new lines at the position of first removal (or startLine if no removals)
490
+ const insertAt = toRemove.length > 0 ? toRemove[0] : startLine;
491
+ resultLines.splice(insertAt, 0, ...toAdd);
492
+ }
493
+
494
+ return resultLines.join("\n");
495
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Shell command path extraction — shared between ShellExecTool and PermissionManager.
3
+ *
4
+ * Heuristically extracts file paths from a shell command and classifies them
5
+ * as read / write / delete operations so permission rules can be applied.
6
+ */
7
+
8
+ import * as path from "path";
9
+
10
+ /** Commands that READ files */
11
+ const READ_COMMANDS = /\b(?:cat|less|more|head|tail|bat|view|open|code|vim|nano|emacs|pg|nl|od|xxd|strings|file|wc|cksum|md5|sha\w+sum|diff|cmp|sort|uniq|awk|sed|grep|rg|ag|fzf)\b/;
12
+ /** Commands that WRITE/MODIFY files */
13
+ const WRITE_COMMANDS = /\b(?:cp|mv|install|patch|chmod|chown|chgrp|touch|truncate|split|csplit|sed\s+-i|perl\s+-[pi])\b/;
14
+ /** Commands that DELETE files */
15
+ const DELETE_COMMANDS = /\b(?:rm|rmdir|del|rd|unlink|shred|trash|git\s+clean)\b/;
16
+
17
+ export interface ExtractedPaths {
18
+ read: string[];
19
+ write: string[];
20
+ delete: string[];
21
+ }
22
+
23
+ /**
24
+ * Extract file paths from a shell command and classify by operation type.
25
+ * Best-effort heuristic — compound commands are split on operators and analyzed per segment.
26
+ */
27
+ export function extractShellPaths(command: string, workingDir: string): ExtractedPaths {
28
+ const result: ExtractedPaths = { read: [], write: [], delete: [] };
29
+
30
+ const segments = command.split(/\s*(?:&&|\|\||;|\|)\s*/);
31
+
32
+ for (const seg of segments) {
33
+ const trimmed = seg.trim();
34
+ if (!trimmed) continue;
35
+
36
+ const parts = trimmed.split(/\s+/).filter((p) => !p.includes("=") || p.startsWith("-"));
37
+ if (parts.length === 0) continue;
38
+
39
+ const args = parts.slice(1).filter((a) => !a.startsWith("-") && a !== "");
40
+
41
+ const resolvePath = (p: string): string => {
42
+ if (p.startsWith("/") || p.startsWith("~")) return p;
43
+ return path.resolve(workingDir, p);
44
+ };
45
+
46
+ if (DELETE_COMMANDS.test(trimmed)) {
47
+ for (const a of args) result.delete.push(resolvePath(a));
48
+ } else if (WRITE_COMMANDS.test(trimmed)) {
49
+ // cp/mv: last arg is destination (write), rest are sources (read)
50
+ if (/\b(?:cp|mv)\b/.test(trimmed) && args.length >= 2) {
51
+ for (let i = 0; i < args.length - 1; i++) result.read.push(resolvePath(args[i]));
52
+ result.write.push(resolvePath(args[args.length - 1]));
53
+ } else {
54
+ for (const a of args) result.write.push(resolvePath(a));
55
+ }
56
+ } else if (READ_COMMANDS.test(trimmed)) {
57
+ for (const a of args) result.read.push(resolvePath(a));
58
+ }
59
+
60
+ // Redirect writes: > file, >> file
61
+ let m;
62
+ const redirectRe = />{1,2}\s*([^\s;|&>]+)/g;
63
+ while ((m = redirectRe.exec(trimmed)) !== null) {
64
+ result.write.push(resolvePath(m[1]));
65
+ }
66
+
67
+ // tee writes
68
+ const teeRe = /\btee\s+(?:-[a-zA-Z]\s+)*([^\s;|&]+)/g;
69
+ while ((m = teeRe.exec(trimmed)) !== null) {
70
+ result.write.push(resolvePath(m[1]));
71
+ }
72
+ }
73
+
74
+ result.read = [...new Set(result.read)];
75
+ result.write = [...new Set(result.write)];
76
+ result.delete = [...new Set(result.delete)];
77
+
78
+ return result;
79
+ }