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,366 @@
1
+ import type { Stats } from "node:fs";
2
+ import { realpath, stat } from "node:fs/promises";
3
+ import {
4
+ EditMatcher,
5
+ type EditMatchStrategy,
6
+ type EditRange,
7
+ } from "../../shared/EditMatcher";
8
+ import { DiffLines, type ToolDiff } from "../../shared/DiffLines";
9
+ import { Fs } from "../../shared/Fs";
10
+ import { FsErrors } from "../../shared/FsErrors";
11
+ import { Lines } from "../../shared/Lines";
12
+ import type { RawEdit } from "./schema";
13
+
14
+ export type NoopEdit = {
15
+ readonly index: number;
16
+ readonly range: string;
17
+ };
18
+
19
+ export type ResolvedEditMetadata = {
20
+ readonly index: number;
21
+ readonly ranges: readonly string[];
22
+ readonly strategy: EditMatchStrategy;
23
+ readonly matchCount: number;
24
+ readonly replaceAll: boolean;
25
+ };
26
+
27
+ export type EditOutcome = {
28
+ readonly editCount: number;
29
+ readonly noops: readonly NoopEdit[];
30
+ readonly ranges: readonly string[];
31
+ readonly resolvedEdits: readonly ResolvedEditMetadata[];
32
+ readonly diff?: ToolDiff;
33
+ };
34
+
35
+ type ParsedEdit = {
36
+ readonly index: number;
37
+ readonly oldString: string;
38
+ readonly newString: string;
39
+ readonly replaceAll: boolean;
40
+ };
41
+
42
+ type ResolvedEdit = {
43
+ readonly index: number;
44
+ readonly ranges: readonly EditRange[];
45
+ readonly newString: string;
46
+ readonly strategy: EditMatchStrategy;
47
+ readonly matchCount: number;
48
+ readonly replaceAll: boolean;
49
+ };
50
+
51
+ type Mutation = {
52
+ readonly index: number;
53
+ readonly range: EditRange;
54
+ readonly newString: string;
55
+ };
56
+
57
+ const CONTEXT_LINES = 2;
58
+ const MAX_EDIT_BYTES = 8 * 1024 * 1024;
59
+
60
+ const editQueues = new Map<string, Promise<unknown>>();
61
+
62
+ export async function editFile(
63
+ absolutePath: string,
64
+ rawEdits: readonly RawEdit[]
65
+ ): Promise<EditOutcome> {
66
+ let canonicalPath: string;
67
+ try {
68
+ canonicalPath = await realpath(absolutePath);
69
+ } catch (error) {
70
+ if (FsErrors.code(error) === "ENOENT") {
71
+ throw new Error(await FsErrors.renderMissing(absolutePath));
72
+ }
73
+ throw error;
74
+ }
75
+
76
+ return enqueue(canonicalPath, () =>
77
+ performEdit(absolutePath, canonicalPath, rawEdits)
78
+ );
79
+ }
80
+
81
+ async function performEdit(
82
+ displayPath: string,
83
+ canonicalPath: string,
84
+ rawEdits: readonly RawEdit[]
85
+ ): Promise<EditOutcome> {
86
+ const edits = parseEdits(rawEdits);
87
+ const metadata = await stat(canonicalPath);
88
+
89
+ if (metadata.isDirectory()) {
90
+ throw new Error(`Path is a directory: ${displayPath}.`);
91
+ }
92
+
93
+ if (metadata.size > MAX_EDIT_BYTES) {
94
+ throw new Error(
95
+ `Path exceeds the ${MAX_EDIT_BYTES}-byte edit cap (${metadata.size} bytes): ${displayPath}.`
96
+ );
97
+ }
98
+
99
+ assertNoDuplicateEdits(edits);
100
+
101
+ const file = Bun.file(canonicalPath);
102
+ const bytes = await file.bytes();
103
+
104
+ if (bytes.subarray(0, 8192).includes(0)) {
105
+ throw new Error(
106
+ `Path is a binary file: ${displayPath}. Edit only supports UTF-8 text files.`
107
+ );
108
+ }
109
+
110
+ const hadBom = Lines.hasUtf8Bom(bytes);
111
+ const originalContent = Lines.stripUtf8Bom(
112
+ new TextDecoder("utf-8").decode(bytes)
113
+ );
114
+ const lineEnding = originalContent.includes("\r\n") ? "\r\n" : "\n";
115
+ const normalizedEdits = edits.map((edit) => ({
116
+ ...edit,
117
+ oldString: normalizeEditString(edit.oldString, lineEnding),
118
+ newString: normalizeEditString(edit.newString, lineEnding),
119
+ }));
120
+ const resolved = normalizedEdits.map((edit) =>
121
+ resolveEdit(originalContent, edit)
122
+ );
123
+ const allMutations = resolved.flatMap((edit) =>
124
+ edit.ranges.map((range) => ({
125
+ index: edit.index,
126
+ range,
127
+ newString: edit.newString,
128
+ }))
129
+ );
130
+
131
+ const sortedMutations = sortMutations(allMutations);
132
+ assertNoOverlaps(sortedMutations);
133
+
134
+ const isNoop = (mutation: Mutation) =>
135
+ originalContent.slice(mutation.range[0], mutation.range[1]) ===
136
+ mutation.newString;
137
+
138
+ const lineRangeCache = new Map<string, string>();
139
+ const lineRange = (range: EditRange): string => {
140
+ const key = `${range[0]}:${range[1]}`;
141
+ let cached = lineRangeCache.get(key);
142
+ if (cached === undefined) {
143
+ cached = EditMatcher.lineRangeFor(originalContent, range);
144
+ lineRangeCache.set(key, cached);
145
+ }
146
+ return cached;
147
+ };
148
+
149
+ const noops: NoopEdit[] = [];
150
+ const effectiveMutations: Mutation[] = [];
151
+ for (const mutation of sortedMutations) {
152
+ if (isNoop(mutation)) {
153
+ noops.push({ index: mutation.index, range: lineRange(mutation.range) });
154
+ } else {
155
+ effectiveMutations.push(mutation);
156
+ }
157
+ }
158
+
159
+ if (effectiveMutations.length === 0) {
160
+ throw new Error(renderAllNoopError(noops));
161
+ }
162
+
163
+ const nextContent = EditMatcher.applyAll(originalContent, effectiveMutations);
164
+
165
+ await writeFileAtomic(
166
+ canonicalPath,
167
+ hadBom ? `${Lines.utf8Bom}${nextContent}` : nextContent,
168
+ metadata
169
+ );
170
+
171
+ const original = Lines.splitWithTrailingNewline(originalContent);
172
+ const next = Lines.splitWithTrailingNewline(nextContent);
173
+ const diff = DiffLines.buildToolDiff(
174
+ displayPath,
175
+ original,
176
+ next,
177
+ CONTEXT_LINES
178
+ );
179
+
180
+ const resolvedEdits = resolved.map((edit) => ({
181
+ index: edit.index,
182
+ ranges: edit.ranges.map((range) => lineRange(range)),
183
+ strategy: edit.strategy,
184
+ matchCount: edit.matchCount,
185
+ replaceAll: edit.replaceAll,
186
+ }));
187
+
188
+ return {
189
+ editCount: edits.length,
190
+ noops,
191
+ ranges: effectiveMutations.map((mutation) => lineRange(mutation.range)),
192
+ resolvedEdits,
193
+ ...(diff === undefined ? {} : { diff }),
194
+ };
195
+ }
196
+
197
+ function parseEdits(rawEdits: readonly RawEdit[]): readonly ParsedEdit[] {
198
+ if (rawEdits.length === 0) {
199
+ throw new Error("Expected non-empty edits array.");
200
+ }
201
+
202
+ return rawEdits.map((raw, index) => {
203
+ if (raw.oldString === raw.newString) {
204
+ throw new Error(`Edit ${index}: oldString and newString are identical.`);
205
+ }
206
+
207
+ return {
208
+ index,
209
+ oldString: raw.oldString,
210
+ newString: raw.newString,
211
+ replaceAll: raw.replaceAll ?? false,
212
+ };
213
+ });
214
+ }
215
+
216
+ function normalizeEditString(text: string, lineEnding: "\n" | "\r\n"): string {
217
+ const normalized = Lines.normalize(text);
218
+ return lineEnding === "\n" ? normalized : normalized.replaceAll("\n", "\r\n");
219
+ }
220
+
221
+ function resolveEdit(content: string, edit: ParsedEdit): ResolvedEdit {
222
+ try {
223
+ const outcome = EditMatcher.resolve(
224
+ content,
225
+ edit.oldString,
226
+ edit.replaceAll
227
+ );
228
+ const ranges = "ranges" in outcome ? outcome.ranges : [outcome.range];
229
+
230
+ for (const range of ranges) {
231
+ EditMatcher.assertNoEscapeDrift(
232
+ outcome.strategy,
233
+ edit.newString,
234
+ content.slice(range[0], range[1])
235
+ );
236
+ }
237
+
238
+ return {
239
+ index: edit.index,
240
+ ranges,
241
+ newString: edit.newString,
242
+ strategy: outcome.strategy,
243
+ matchCount: outcome.matchCount,
244
+ replaceAll: edit.replaceAll,
245
+ };
246
+ } catch (error) {
247
+ if (error instanceof EditMatcher.NotFoundError) {
248
+ throw new Error(EditMatcher.renderNotFound(error));
249
+ }
250
+
251
+ throw error;
252
+ }
253
+ }
254
+
255
+ function assertNoDuplicateEdits(edits: readonly ParsedEdit[]): void {
256
+ const seen = new Map<string, number>();
257
+
258
+ for (const [index, edit] of edits.entries()) {
259
+ const key = duplicateKey(edit);
260
+ const previous = seen.get(key);
261
+
262
+ if (previous !== undefined) {
263
+ throw new Error(
264
+ `Edits ${previous} and ${index} are identical. Remove one.`
265
+ );
266
+ }
267
+
268
+ seen.set(key, index);
269
+ }
270
+ }
271
+
272
+ function duplicateKey(edit: ParsedEdit): string {
273
+ return JSON.stringify({
274
+ oldString: edit.oldString,
275
+ newString: edit.newString,
276
+ replaceAll: edit.replaceAll,
277
+ });
278
+ }
279
+
280
+ function sortMutations(mutations: readonly Mutation[]): readonly Mutation[] {
281
+ return [...mutations].sort((left, right) => {
282
+ if (left.range[0] !== right.range[0]) {
283
+ return left.range[0] - right.range[0];
284
+ }
285
+
286
+ return left.range[1] - right.range[1];
287
+ });
288
+ }
289
+
290
+ function assertNoOverlaps(sorted: readonly Mutation[]): void {
291
+ for (let index = 1; index < sorted.length; index += 1) {
292
+ const previous = sorted[index - 1]!;
293
+ const current = sorted[index]!;
294
+
295
+ if (previous.range[1] > current.range[0]) {
296
+ throw new Error(renderOverlapError(previous, current));
297
+ }
298
+ }
299
+ }
300
+
301
+ function renderOverlapError(left: Mutation, right: Mutation): string {
302
+ return [
303
+ `Edits ${left.index} and ${right.index} target overlapping byte ranges.`,
304
+ `- Edit ${left.index}: range ${left.range[0]}-${left.range[1]}`,
305
+ `- Edit ${right.index}: range ${right.range[0]}-${right.range[1]}`,
306
+ "Combine into a single edit, or drop one.",
307
+ ].join("\n");
308
+ }
309
+
310
+ function renderAllNoopError(noops: readonly NoopEdit[]): string {
311
+ return [
312
+ "All edits were no-ops. The file already contains the requested replacement content.",
313
+ "",
314
+ ...noops.map(
315
+ (noop) =>
316
+ `- Edit ${noop.index}: matched range ${noop.range} already equals newString.`
317
+ ),
318
+ "",
319
+ "Re-read the file and widen oldString if you meant to replace adjacent duplicated content.",
320
+ ].join("\n");
321
+ }
322
+
323
+ async function writeFileAtomic(
324
+ canonicalPath: string,
325
+ content: string,
326
+ metadata: Stats
327
+ ): Promise<void> {
328
+ await Fs.writeKeepingLinks(canonicalPath, content, {
329
+ mode: Number(metadata.mode),
330
+ nlink: metadata.nlink,
331
+ });
332
+ }
333
+
334
+ async function enqueue<T>(key: string, task: () => Promise<T>): Promise<T> {
335
+ const previous = editQueues.get(key) ?? Promise.resolve();
336
+ const queued = previous.then(task, task);
337
+ editQueues.set(key, queued);
338
+
339
+ try {
340
+ return await queued;
341
+ } finally {
342
+ if (editQueues.get(key) === queued) {
343
+ editQueues.delete(key);
344
+ }
345
+ }
346
+ }
347
+
348
+ export function formatEditSummary(
349
+ displayPath: string,
350
+ outcome: EditOutcome
351
+ ): string {
352
+ const noun = outcome.editCount === 1 ? "edit" : "edits";
353
+ return `${outcome.editCount} ${noun} made to ${displayPath}: lines ${joinHuman(outcome.ranges)}.`;
354
+ }
355
+
356
+ function joinHuman(items: readonly string[]): string {
357
+ if (items.length === 0) {
358
+ return "unknown";
359
+ }
360
+
361
+ if (items.length === 1) {
362
+ return items[0] ?? "unknown";
363
+ }
364
+
365
+ return `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
366
+ }
@@ -0,0 +1,36 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Paths } from "../../shared/Paths";
3
+ import { Tools } from "../../shared/Tools";
4
+ import { editFile, formatEditSummary } from "./edit";
5
+ import { editView } from "./render";
6
+ import { type EditInput, editSchema } from "./schema";
7
+
8
+ export default function (pi: ExtensionAPI): void {
9
+ Tools.register(pi, {
10
+ name: "edit",
11
+ label: "edit",
12
+ description:
13
+ "Replace strings in a UTF-8 text file. " +
14
+ "Prefer edit over write for changes to existing files.",
15
+ parameters: editSchema,
16
+ renderShell: "self",
17
+ effect: { kind: "writesPaths", paths: ({ path }) => [path] },
18
+ executionMode: "sequential",
19
+ async execute(_id, params, signal, _onUpdate, ctx) {
20
+ const { path, edits } = params as EditInput;
21
+
22
+ if (signal?.aborted) {
23
+ throw new Error("Edit aborted before execution.");
24
+ }
25
+
26
+ const absolutePath = Paths.resolve(path, ctx.cwd);
27
+ const outcome = await editFile(absolutePath, edits);
28
+
29
+ return {
30
+ content: [{ type: "text", text: formatEditSummary(path, outcome) }],
31
+ details: outcome,
32
+ };
33
+ },
34
+ toViewModel: editView,
35
+ });
36
+ }
@@ -0,0 +1,16 @@
1
+ import type { ToolViewInput } from "../../shared/Tools";
2
+ import { DiffBlocks } from "../../view/DiffBlocks";
3
+ import type { ToolView } from "../../view/ViewBlock";
4
+ import type { EditOutcome } from "./edit";
5
+ import type { editSchema } from "./schema";
6
+
7
+ type EditViewInput = ToolViewInput<typeof editSchema, EditOutcome>;
8
+
9
+ export function editView({ args, result, cwd }: EditViewInput): ToolView {
10
+ return DiffBlocks.fileView({
11
+ label: "Edit",
12
+ path: typeof args?.path === "string" ? args.path : undefined,
13
+ cwd,
14
+ diff: result?.details?.diff,
15
+ });
16
+ }
@@ -0,0 +1,37 @@
1
+ import { type Static, Type } from "typebox";
2
+
3
+ export const editSchema = Type.Object({
4
+ path: Type.String({
5
+ description: "Absolute or relative path to file (resolved against cwd).",
6
+ }),
7
+ edits: Type.Array(
8
+ Type.Object(
9
+ {
10
+ oldString: Type.String({
11
+ description:
12
+ "Use the actual file content without the `LINE:` prefix from read output. Must be unique unless replaceAll=true. Include enough surrounding context for uniqueness.",
13
+ }),
14
+ newString: Type.String({
15
+ description:
16
+ "Replacement text. Empty string deletes the matched range.",
17
+ }),
18
+ replaceAll: Type.Optional(
19
+ Type.Boolean({
20
+ description:
21
+ "If true, replaces every occurrence of oldString. Defaults to false.",
22
+ })
23
+ ),
24
+ },
25
+ { additionalProperties: false }
26
+ ),
27
+ {
28
+ minItems: 1,
29
+ description:
30
+ "Non-empty atomic batch of edits. Batched edits resolve against the initial file state and must not overlap. For sequential transformations where edit 2 depends on edit 1's result, use separate tool calls.",
31
+ }
32
+ ),
33
+ });
34
+
35
+ export type EditInput = Static<typeof editSchema>;
36
+
37
+ export type RawEdit = EditInput["edits"][number];
@@ -0,0 +1,34 @@
1
+ import { FileScanner, type FileScanOptions } from "../../shared/FileScanner";
2
+ import { FsErrors } from "../../shared/FsErrors";
3
+
4
+ export type GlobMatch = {
5
+ readonly path: string;
6
+ readonly mtime: number;
7
+ };
8
+
9
+ export type GlobScanOptions = FileScanOptions;
10
+
11
+ export async function findFiles(
12
+ root: string,
13
+ pattern: string,
14
+ options: GlobScanOptions
15
+ ): Promise<readonly GlobMatch[]> {
16
+ const metadata = await FsErrors.statOrThrow(root);
17
+
18
+ if (!metadata.isDirectory()) {
19
+ throw new Error(
20
+ `Glob path must be a directory: ${root}. Drop "path" and put the filename in "pattern", or use the read tool to inspect a single file.`
21
+ );
22
+ }
23
+
24
+ const paths = await FileScanner.scan(root, pattern, options);
25
+ const matches: GlobMatch[] = paths.map((path) => ({
26
+ path,
27
+ mtime: Bun.file(path).lastModified,
28
+ }));
29
+
30
+ return matches.sort(
31
+ (left, right) =>
32
+ right.mtime - left.mtime || left.path.localeCompare(right.path)
33
+ );
34
+ }
@@ -0,0 +1,83 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Paths } from "../../shared/Paths";
3
+ import { Tools } from "../../shared/Tools";
4
+ import { findFiles } from "./glob";
5
+ import { globView, renderFiles } from "./render";
6
+ import {
7
+ GLOB_HEAD_LIMIT_MAX,
8
+ type GlobInput,
9
+ type GlobPathFormat,
10
+ globSchema,
11
+ } from "./schema";
12
+
13
+ const DEFAULT_PATH_FORMAT: GlobPathFormat = "relative";
14
+
15
+ export default function (pi: ExtensionAPI): void {
16
+ Tools.register(pi, {
17
+ name: "glob",
18
+ label: "glob",
19
+ description:
20
+ "Find files by glob pattern under a directory, sorted newest first. " +
21
+ "Skips gitignored paths and dotfiles unless requested. " +
22
+ "Use glob to enumerate files instead of bash with find, fd, ls -R, or similar.",
23
+ parameters: globSchema,
24
+ renderShell: "self",
25
+ effect: { kind: "readOnly" },
26
+ executionMode: "parallel",
27
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
28
+ const {
29
+ pattern,
30
+ path,
31
+ exclude,
32
+ includeDotfiles,
33
+ includeIgnored,
34
+ pathFormat,
35
+ headLimit,
36
+ } = params as GlobInput;
37
+
38
+ if (signal?.aborted) {
39
+ throw new Error("Glob aborted before execution.");
40
+ }
41
+
42
+ const limit = headLimit ?? GLOB_HEAD_LIMIT_MAX;
43
+ const resolvedPathFormat = pathFormat ?? DEFAULT_PATH_FORMAT;
44
+ const absolutePath = Paths.resolve(path ?? ".", ctx.cwd);
45
+ const matches = await findFiles(absolutePath, pattern, {
46
+ exclude,
47
+ includeDotfiles: includeDotfiles ?? false,
48
+ includeIgnored: includeIgnored ?? false,
49
+ });
50
+ const outcome = renderFiles(matches, limit, {
51
+ cwd: ctx.cwd,
52
+ pathFormat: resolvedPathFormat,
53
+ });
54
+ const content: Array<{ type: "text"; text: string }> = [
55
+ { type: "text", text: outcome.body },
56
+ ];
57
+
58
+ if (outcome.truncated) {
59
+ content.push({
60
+ type: "text",
61
+ text: `[glob tool: showing ${outcome.visibleItems} of ${outcome.totalItems} entries; narrow the pattern or scope to a specific path to reduce results.]`,
62
+ });
63
+ }
64
+
65
+ return {
66
+ content,
67
+ details: {
68
+ absolutePath,
69
+ pattern,
70
+ exclude,
71
+ includeDotfiles: includeDotfiles ?? false,
72
+ includeIgnored: includeIgnored ?? false,
73
+ pathFormat: resolvedPathFormat,
74
+ fileCount: matches.length,
75
+ totalItems: outcome.totalItems,
76
+ visibleItems: outcome.visibleItems,
77
+ truncated: outcome.truncated,
78
+ },
79
+ };
80
+ },
81
+ toViewModel: globView,
82
+ });
83
+ }
@@ -0,0 +1,72 @@
1
+ import { Format } from "../../shared/Format";
2
+ import { Renderer } from "../../shared/Renderer";
3
+ import { SearchRender, type SearchList } from "../../shared/SearchRender";
4
+ import type { ToolViewInput } from "../../shared/Tools";
5
+ import type { ToolView, ViewBlock } from "../../view/ViewBlock";
6
+ import type { GlobMatch } from "./glob";
7
+ import type {
8
+ GlobDetails,
9
+ GlobInput,
10
+ GlobPathFormat,
11
+ globSchema,
12
+ } from "./schema";
13
+
14
+ export type RenderOutcome = SearchList;
15
+
16
+ export type RenderOptions = {
17
+ readonly cwd: string;
18
+ readonly pathFormat: GlobPathFormat;
19
+ };
20
+
21
+ export function renderFiles(
22
+ matches: readonly GlobMatch[],
23
+ headLimit: number,
24
+ options: RenderOptions
25
+ ): RenderOutcome {
26
+ return SearchRender.capList(
27
+ matches.map((match) => SearchRender.formatPath(match.path, options)),
28
+ headLimit
29
+ );
30
+ }
31
+
32
+ type GlobViewInput = ToolViewInput<typeof globSchema, GlobDetails>;
33
+
34
+ export function globView({ args, result, cwd }: GlobViewInput): ToolView {
35
+ const input = (args ?? {}) as Partial<GlobInput>;
36
+ const fileCount = result?.details?.fileCount;
37
+ return {
38
+ label: "Glob",
39
+ icon: "search",
40
+ title: [
41
+ {
42
+ kind: "text",
43
+ text: SearchRender.subjectTitle({
44
+ subject: input.pattern ?? "...",
45
+ path: input.path,
46
+ cwd,
47
+ }),
48
+ },
49
+ ...(fileCount === undefined
50
+ ? []
51
+ : ([
52
+ {
53
+ kind: "text",
54
+ tone: "muted",
55
+ text: Format.count(fileCount, "file"),
56
+ },
57
+ ] as const)),
58
+ ],
59
+ body: formatBody(result),
60
+ };
61
+ }
62
+
63
+ function formatBody(result: GlobViewInput["result"]): readonly ViewBlock[] {
64
+ const text = Renderer.firstText(result);
65
+ if (text === "") {
66
+ return [];
67
+ }
68
+ if (result?.details?.fileCount === 0) {
69
+ return [{ kind: "text", text }];
70
+ }
71
+ return text.split("\n").map((path) => ({ kind: "file", path }));
72
+ }