pim-agent 0.0.1 → 0.10.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 (275) 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 +54 -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 +177 -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 +36 -0
  47. package/packages/core/src/extensions/system-prompt/prompt.ts +232 -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 +132 -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 +77 -0
  87. package/packages/core/src/session/AgentRuntime.ts +85 -0
  88. package/packages/core/src/session/EventLog.ts +156 -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/SearchIndex.ts +752 -0
  92. package/packages/core/src/session/SearchTokens.ts +97 -0
  93. package/packages/core/src/session/SessionCache.ts +68 -0
  94. package/packages/core/src/session/SessionDigest.ts +161 -0
  95. package/packages/core/src/session/SessionHost.ts +892 -0
  96. package/packages/core/src/session/SessionLease.ts +279 -0
  97. package/packages/core/src/session/SessionMeta.ts +267 -0
  98. package/packages/core/src/session/SessionName.ts +18 -0
  99. package/packages/core/src/session/SessionRegistry.ts +244 -0
  100. package/packages/core/src/session/WriteMark.ts +51 -0
  101. package/packages/core/src/shared/Cli.ts +25 -0
  102. package/packages/core/src/shared/DaemonUnit.ts +13 -0
  103. package/packages/core/src/shared/DiffLines.ts +304 -0
  104. package/packages/core/src/shared/DiffPatch.ts +70 -0
  105. package/packages/core/src/shared/DiffRenderer.ts +283 -0
  106. package/packages/core/src/shared/DiffView.ts +29 -0
  107. package/packages/core/src/shared/Directories.ts +61 -0
  108. package/packages/core/src/shared/EditMatcher.ts +767 -0
  109. package/packages/core/src/shared/Errors.ts +9 -0
  110. package/packages/core/src/shared/ExtensionToggles.ts +130 -0
  111. package/packages/core/src/shared/FileEnumerator.ts +361 -0
  112. package/packages/core/src/shared/FileScanner.ts +57 -0
  113. package/packages/core/src/shared/FileWatch.ts +121 -0
  114. package/packages/core/src/shared/Format.ts +74 -0
  115. package/packages/core/src/shared/Fs.ts +91 -0
  116. package/packages/core/src/shared/FsErrors.ts +67 -0
  117. package/packages/core/src/shared/FuzzyMatcher.ts +71 -0
  118. package/packages/core/src/shared/Git.ts +530 -0
  119. package/packages/core/src/shared/GitMonitor.ts +221 -0
  120. package/packages/core/src/shared/GlobExclusions.ts +13 -0
  121. package/packages/core/src/shared/Http.ts +14 -0
  122. package/packages/core/src/shared/ImageMime.ts +36 -0
  123. package/packages/core/src/shared/Images.ts +357 -0
  124. package/packages/core/src/shared/Json.ts +19 -0
  125. package/packages/core/src/shared/Languages.ts +95 -0
  126. package/packages/core/src/shared/Levenshtein.ts +83 -0
  127. package/packages/core/src/shared/Lines.ts +79 -0
  128. package/packages/core/src/shared/McpClient.ts +444 -0
  129. package/packages/core/src/shared/OutputBudget.ts +84 -0
  130. package/packages/core/src/shared/PatchSummary.ts +40 -0
  131. package/packages/core/src/shared/Paths.ts +80 -0
  132. package/packages/core/src/shared/PimSettings.ts +128 -0
  133. package/packages/core/src/shared/PimVersion.ts +93 -0
  134. package/packages/core/src/shared/Pool.ts +26 -0
  135. package/packages/core/src/shared/Proc.ts +50 -0
  136. package/packages/core/src/shared/RateLimiter.ts +50 -0
  137. package/packages/core/src/shared/Renderer.ts +371 -0
  138. package/packages/core/src/shared/RepoDiff.ts +741 -0
  139. package/packages/core/src/shared/SafePath.ts +17 -0
  140. package/packages/core/src/shared/SearchRender.ts +82 -0
  141. package/packages/core/src/shared/SendFile.ts +23 -0
  142. package/packages/core/src/shared/SpillCache.ts +78 -0
  143. package/packages/core/src/shared/SubagentLogs.ts +102 -0
  144. package/packages/core/src/shared/Supervisor.ts +460 -0
  145. package/packages/core/src/shared/Surface.ts +2 -0
  146. package/packages/core/src/shared/Sweeper.ts +32 -0
  147. package/packages/core/src/shared/Tools.ts +801 -0
  148. package/packages/core/src/shared/Updater.ts +209 -0
  149. package/packages/core/src/view/AnsiPainter.ts +227 -0
  150. package/packages/core/src/view/BodyRenderer.ts +102 -0
  151. package/packages/core/src/view/DiffBlocks.ts +52 -0
  152. package/packages/core/src/view/DiffExpand.ts +214 -0
  153. package/packages/core/src/view/DiffLayout.ts +69 -0
  154. package/packages/core/src/view/DiffPairs.ts +47 -0
  155. package/packages/core/src/view/MarkdownPainter.ts +314 -0
  156. package/packages/core/src/view/MovePath.ts +70 -0
  157. package/packages/core/src/view/Painting.ts +182 -0
  158. package/packages/core/src/view/ViewBlock.ts +124 -0
  159. package/packages/daemon/src/Daemon.ts +63 -0
  160. package/packages/daemon/src/DaemonInstall.ts +115 -0
  161. package/packages/daemon/src/Surfaces.ts +66 -0
  162. package/packages/daemon/src/TelegramSurface.ts +30 -0
  163. package/packages/daemon/src/WebSurface.ts +56 -0
  164. package/packages/daemon/src/index.ts +37 -0
  165. package/packages/protocol/src/Command.ts +219 -0
  166. package/packages/protocol/src/Diff.ts +15 -0
  167. package/packages/protocol/src/ImageRoute.ts +23 -0
  168. package/packages/protocol/src/ServerEvent.ts +406 -0
  169. package/packages/server/src/AttachmentEndpoint.ts +184 -0
  170. package/packages/server/src/ClientConnection.ts +277 -0
  171. package/packages/server/src/ImageEndpoint.ts +36 -0
  172. package/packages/server/src/ProbeClient.ts +374 -0
  173. package/packages/server/src/Reloader.ts +93 -0
  174. package/packages/server/src/SendFileTool.ts +88 -0
  175. package/packages/server/src/SessionCatalogue.ts +456 -0
  176. package/packages/server/src/SessionProjection.ts +194 -0
  177. package/packages/server/src/SessionStream.ts +627 -0
  178. package/packages/server/src/StaticClient.ts +178 -0
  179. package/packages/server/src/WebOptions.ts +72 -0
  180. package/packages/server/src/WsGateway.ts +798 -0
  181. package/packages/server/src/probe.ts +200 -0
  182. package/packages/telegram/src/Bot.ts +229 -0
  183. package/packages/telegram/src/Commands.ts +768 -0
  184. package/packages/telegram/src/Config.ts +148 -0
  185. package/packages/telegram/src/Markdown.ts +235 -0
  186. package/packages/telegram/src/Message.ts +152 -0
  187. package/packages/telegram/src/Renderer.ts +456 -0
  188. package/packages/telegram/src/SendFileSchema.ts +18 -0
  189. package/packages/telegram/src/SendFileTool.ts +65 -0
  190. package/packages/telegram/src/Session.ts +253 -0
  191. package/packages/telegram/src/SessionRegistry.ts +126 -0
  192. package/packages/telegram/src/TaskScheduler.ts +296 -0
  193. package/packages/telegram/src/TaskSchema.ts +88 -0
  194. package/packages/telegram/src/TaskStore.ts +67 -0
  195. package/packages/telegram/src/TaskTool.ts +208 -0
  196. package/packages/telegram/src/TelegramHtml.ts +288 -0
  197. package/packages/telegram/src/TypingIndicator.ts +43 -0
  198. package/packages/telegram/src/UpdateConfirm.ts +61 -0
  199. package/packages/tui/src/extensions/_init/index.ts +144 -0
  200. package/packages/tui/src/extensions/command-picker/index.ts +75 -0
  201. package/packages/tui/src/extensions/file-picker/index.ts +156 -0
  202. package/packages/tui/src/extensions/footer/index.ts +93 -0
  203. package/packages/tui/src/extensions/footer/powerline.ts +87 -0
  204. package/packages/tui/src/extensions/footer/segments.ts +202 -0
  205. package/packages/tui/src/extensions/pim/index.ts +89 -0
  206. package/packages/tui/src/extensions/session-lease/index.ts +253 -0
  207. package/packages/tui/src/extensions/tps/index.ts +118 -0
  208. package/packages/tui/src/extensions/working-indicator/index.ts +75 -0
  209. package/packages/tui/src/extensions/wrapProvider.ts +18 -0
  210. package/packages/tui/src/themes/pim-dark.json +84 -0
  211. package/packages/tui/src/themes/pim-light.json +84 -0
  212. package/packages/tui/src/themes/themeCliArgs.ts +20 -0
  213. package/packages/web/dist/client/apple-touch-icon.png +0 -0
  214. package/packages/web/dist/client/assets/bash-LmRjUKlR.js +1 -0
  215. package/packages/web/dist/client/assets/c-DBVbJFpC.js +1 -0
  216. package/packages/web/dist/client/assets/clojure-CR7dvLJ4.js +1 -0
  217. package/packages/web/dist/client/assets/cmake-CgP6gB4W.js +1 -0
  218. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-B48RJMeK.woff +0 -0
  219. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-DBq5bJCE.woff2 +0 -0
  220. package/packages/web/dist/client/assets/commit-mono-latin-300-normal-B-iV2FbL.woff2 +0 -0
  221. package/packages/web/dist/client/assets/commit-mono-latin-300-normal-kM0OTZCv.woff +0 -0
  222. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-BXinMwCi.woff +0 -0
  223. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-DjSHLl2N.woff2 +0 -0
  224. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-s0S3qwFW.woff +0 -0
  225. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-wzhe4RuD.woff2 +0 -0
  226. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-BuQDXT0M.woff +0 -0
  227. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-SwgSWHaV.woff2 +0 -0
  228. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-1-GU0IUE.woff +0 -0
  229. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-DU8mrtj2.woff2 +0 -0
  230. package/packages/web/dist/client/assets/core-CA0aSzPu.js +3 -0
  231. package/packages/web/dist/client/assets/cpp-N2d8M7Nc.js +1 -0
  232. package/packages/web/dist/client/assets/csharp-yYt0Ikks.js +1 -0
  233. package/packages/web/dist/client/assets/css-D8M8GYaw.js +1 -0
  234. package/packages/web/dist/client/assets/diff-BhdZizse.js +1 -0
  235. package/packages/web/dist/client/assets/dockerfile-C0zufGsU.js +1 -0
  236. package/packages/web/dist/client/assets/elixir-DXIHlGsR.js +1 -0
  237. package/packages/web/dist/client/assets/erlang-BjYI8dhz.js +1 -0
  238. package/packages/web/dist/client/assets/go-CYstVVJV.js +1 -0
  239. package/packages/web/dist/client/assets/graphql-BHHhacaR.js +1 -0
  240. package/packages/web/dist/client/assets/haskell-DUkhXIda.js +1 -0
  241. package/packages/web/dist/client/assets/index-B9be-i9d.css +1 -0
  242. package/packages/web/dist/client/assets/index-CpinM5NK.js +45 -0
  243. package/packages/web/dist/client/assets/ini-B1CroMsf.js +1 -0
  244. package/packages/web/dist/client/assets/java-Dde-Wnb4.js +1 -0
  245. package/packages/web/dist/client/assets/javascript-8AmHuH5O.js +1 -0
  246. package/packages/web/dist/client/assets/json-PmyKWHmm.js +1 -0
  247. package/packages/web/dist/client/assets/kotlin-DgSK8bu-.js +2 -0
  248. package/packages/web/dist/client/assets/less-Bp0mgmpG.js +1 -0
  249. package/packages/web/dist/client/assets/lua-1dXYaL-H.js +1 -0
  250. package/packages/web/dist/client/assets/makefile-BX-o3EIz.js +1 -0
  251. package/packages/web/dist/client/assets/markdown-D6FrnlqJ.js +1 -0
  252. package/packages/web/dist/client/assets/ocaml-B-P1Uyq8.js +1 -0
  253. package/packages/web/dist/client/assets/perl-CvqMZYuf.js +1 -0
  254. package/packages/web/dist/client/assets/php-DuZl5e-L.js +2 -0
  255. package/packages/web/dist/client/assets/powershell-BUe16zE4.js +1 -0
  256. package/packages/web/dist/client/assets/protobuf-DGOW1qT8.js +1 -0
  257. package/packages/web/dist/client/assets/python-gHmu6VPG.js +1 -0
  258. package/packages/web/dist/client/assets/r-DNWPCnjT.js +1 -0
  259. package/packages/web/dist/client/assets/ruby-BGpZ3p4_.js +1 -0
  260. package/packages/web/dist/client/assets/rust-BIKPVoi6.js +1 -0
  261. package/packages/web/dist/client/assets/scala-C9lP4FWY.js +1 -0
  262. package/packages/web/dist/client/assets/scss-gOyOKiXd.js +1 -0
  263. package/packages/web/dist/client/assets/sql-DWRKyvJ-.js +1 -0
  264. package/packages/web/dist/client/assets/swift-BTn9__hL.js +1 -0
  265. package/packages/web/dist/client/assets/typescript-CxkPLmSc.js +1 -0
  266. package/packages/web/dist/client/assets/vim-Dhhcn_CA.js +1 -0
  267. package/packages/web/dist/client/assets/xml-COl5yFSC.js +1 -0
  268. package/packages/web/dist/client/assets/yaml-DjOhhVLG.js +1 -0
  269. package/packages/web/dist/client/favicon.svg +1 -0
  270. package/packages/web/dist/client/icon-192.png +0 -0
  271. package/packages/web/dist/client/icon-512.png +0 -0
  272. package/packages/web/dist/client/icon-maskable.png +0 -0
  273. package/packages/web/dist/client/index.html +24 -0
  274. package/packages/web/dist/client/manifest.webmanifest +21 -0
  275. package/packages/web/dist/client/wordmark.svg +1 -0
@@ -0,0 +1,379 @@
1
+ import {
2
+ ADD_FILE_MARKER,
3
+ cleanPath,
4
+ DELETE_FILE_MARKER,
5
+ MOVE_TO_MARKER,
6
+ UPDATE_FILE_MARKER,
7
+ } from "../../shared/PatchSummary";
8
+ import type { Hunk, Patch, UpdateChunk } from "./types";
9
+
10
+ const BEGIN_PATCH_MARKER = "*** Begin Patch";
11
+ const END_PATCH_MARKER = "*** End Patch";
12
+ const EOF_MARKER = "*** End of File";
13
+ const CHANGE_CONTEXT_MARKER = "@@ ";
14
+ const EMPTY_CHANGE_CONTEXT_MARKER = "@@";
15
+ const MOVE_DESTINATION_REQUIRED =
16
+ "Invalid *** Move to directive: destination path is required.";
17
+
18
+ type ParseErrorDetails =
19
+ | { readonly type: "patch"; readonly message: string }
20
+ | {
21
+ readonly type: "hunk";
22
+ readonly message: string;
23
+ readonly lineNumber: number;
24
+ };
25
+
26
+ // Keep these formats byte-identical to Codex's ParseError Display; GPT models recover from them.
27
+ function formatParseError(error: ParseErrorDetails): string {
28
+ switch (error.type) {
29
+ case "patch":
30
+ return `invalid patch: ${error.message}`;
31
+ case "hunk":
32
+ return `invalid hunk at line ${error.lineNumber}, ${error.message}`;
33
+ }
34
+ }
35
+
36
+ export function parsePatch(text: string): Patch {
37
+ const lines = text.trim().split("\n");
38
+ checkBoundaries(lines);
39
+
40
+ const hunkLines = lines.slice(1, lines.length - 1);
41
+ const hunks: Hunk[] = [];
42
+ let remaining = hunkLines;
43
+ let lineNumber = 2;
44
+
45
+ while (remaining.length > 0) {
46
+ const { hunk, consumed } = parseOneHunk(remaining, lineNumber);
47
+ hunks.push(hunk);
48
+ lineNumber += consumed;
49
+ remaining = remaining.slice(consumed);
50
+ }
51
+
52
+ return { hunks };
53
+ }
54
+
55
+ function checkBoundaries(lines: readonly string[]): void {
56
+ const first = lines[0]?.trim();
57
+ const last = lines.at(-1)?.trim();
58
+
59
+ if (first === undefined || !lines[0]!.trim().startsWith(BEGIN_PATCH_MARKER)) {
60
+ throw new Error(
61
+ formatParseError({
62
+ type: "patch",
63
+ message:
64
+ "The first line of the patch must be '*** Begin Patch'. Do not include Markdown fences, prose, or shell heredoc text before it.",
65
+ })
66
+ );
67
+ }
68
+
69
+ if (last !== END_PATCH_MARKER) {
70
+ throw new Error(
71
+ formatParseError({
72
+ type: "patch",
73
+ message:
74
+ "The last line of the patch must be '*** End Patch'. Do not include Markdown fences or trailing prose after it.",
75
+ })
76
+ );
77
+ }
78
+ }
79
+
80
+ function parseOneHunk(
81
+ lines: readonly string[],
82
+ lineNumber: number
83
+ ): { readonly hunk: Hunk; readonly consumed: number } {
84
+ const firstLine = lines[0]!.trim();
85
+
86
+ const addPath = stripPrefix(firstLine, ADD_FILE_MARKER);
87
+ if (addPath !== undefined) {
88
+ return parseAddHunk(lines, lineNumber, addPath);
89
+ }
90
+
91
+ const deletePath = stripPrefix(firstLine, DELETE_FILE_MARKER);
92
+ if (deletePath !== undefined) {
93
+ return parseDeleteHunk(lines, lineNumber, deletePath);
94
+ }
95
+
96
+ const updatePath = stripPrefix(firstLine, UPDATE_FILE_MARKER);
97
+ if (updatePath !== undefined) {
98
+ return parseUpdateHunk(lines, lineNumber, updatePath);
99
+ }
100
+
101
+ throw new Error(
102
+ formatParseError({
103
+ type: "hunk",
104
+ lineNumber,
105
+ message:
106
+ `'${firstLine}' is not a valid hunk header. ` +
107
+ "Valid hunk headers: '*** Add File: {path}', '*** Delete File: {path}', '*** Update File: {path}'. " +
108
+ "Do not use unified-diff file headers like '---' or '+++' as hunk headers.",
109
+ })
110
+ );
111
+ }
112
+
113
+ function parseAddHunk(
114
+ lines: readonly string[],
115
+ lineNumber: number,
116
+ addPath: string
117
+ ): { readonly hunk: Hunk; readonly consumed: number } {
118
+ let contents = "";
119
+ let consumed = 1;
120
+ for (const line of lines.slice(1)) {
121
+ if (line.startsWith("+")) {
122
+ contents += `${line.slice(1)}\n`;
123
+ consumed += 1;
124
+ } else {
125
+ break;
126
+ }
127
+ }
128
+ const nextLine = lines[consumed];
129
+ if (
130
+ nextLine !== undefined &&
131
+ !isHunkHeader(nextLine) &&
132
+ !nextLine.trim().startsWith("*")
133
+ ) {
134
+ throw new Error(
135
+ formatParseError({
136
+ type: "hunk",
137
+ lineNumber: lineNumber + consumed,
138
+ message: `Invalid Add File body: '${nextLine}' must start with '+'. Added file content lines must start with '+'.`,
139
+ })
140
+ );
141
+ }
142
+ return {
143
+ hunk: { kind: "add", path: cleanPath(addPath), contents },
144
+ consumed,
145
+ };
146
+ }
147
+
148
+ function parseDeleteHunk(
149
+ lines: readonly string[],
150
+ lineNumber: number,
151
+ deletePath: string
152
+ ): { readonly hunk: Hunk; readonly consumed: number } {
153
+ const nextLine = lines[1];
154
+ if (
155
+ nextLine !== undefined &&
156
+ !isHunkHeader(nextLine) &&
157
+ !nextLine.trim().startsWith("*")
158
+ ) {
159
+ throw new Error(
160
+ formatParseError({
161
+ type: "hunk",
162
+ lineNumber: lineNumber + 1,
163
+ message: `Delete File hunks must not contain content lines, got: '${nextLine}'.`,
164
+ })
165
+ );
166
+ }
167
+ return {
168
+ hunk: { kind: "delete", path: cleanPath(deletePath) },
169
+ consumed: 1,
170
+ };
171
+ }
172
+
173
+ function parseUpdateHunk(
174
+ lines: readonly string[],
175
+ lineNumber: number,
176
+ updatePath: string
177
+ ): { readonly hunk: Hunk; readonly consumed: number } {
178
+ let remaining = lines.slice(1);
179
+ let consumed = 1;
180
+
181
+ let movePath: string | undefined;
182
+ const moveLine = remaining[0]?.trim();
183
+ if (moveLine?.startsWith(MOVE_TO_MARKER)) {
184
+ const rawMovePath = moveLine.slice(MOVE_TO_MARKER.length);
185
+ if (rawMovePath.length === 0) {
186
+ throw new Error(
187
+ formatParseError({
188
+ type: "hunk",
189
+ lineNumber: lineNumber + consumed,
190
+ message: MOVE_DESTINATION_REQUIRED,
191
+ })
192
+ );
193
+ }
194
+ if (!rawMovePath.startsWith(" ")) {
195
+ throw new Error(
196
+ formatParseError({
197
+ type: "hunk",
198
+ lineNumber: lineNumber + consumed,
199
+ message: `Invalid *** Move to directive: use '*** Move to: {path}'.`,
200
+ })
201
+ );
202
+ }
203
+ if (cleanPath(rawMovePath).length === 0) {
204
+ throw new Error(
205
+ formatParseError({
206
+ type: "hunk",
207
+ lineNumber: lineNumber + consumed,
208
+ message: MOVE_DESTINATION_REQUIRED,
209
+ })
210
+ );
211
+ }
212
+ movePath = rawMovePath;
213
+ } else if (moveLine?.startsWith("*** Move")) {
214
+ throw new Error(
215
+ formatParseError({
216
+ type: "hunk",
217
+ lineNumber: lineNumber + consumed,
218
+ message: `Invalid move directive '${moveLine}'. Use '*** Move to: {path}'.`,
219
+ })
220
+ );
221
+ }
222
+ if (movePath !== undefined) {
223
+ remaining = remaining.slice(1);
224
+ consumed += 1;
225
+ }
226
+
227
+ const chunks: UpdateChunk[] = [];
228
+ while (remaining.length > 0) {
229
+ if (remaining[0]!.trim() === "") {
230
+ consumed += 1;
231
+ remaining = remaining.slice(1);
232
+ continue;
233
+ }
234
+ if (remaining[0]!.startsWith("*")) {
235
+ break;
236
+ }
237
+
238
+ const { chunk, consumed: chunkLines } = parseUpdateChunk(
239
+ remaining,
240
+ lineNumber + consumed,
241
+ chunks.length === 0
242
+ );
243
+ chunks.push(chunk);
244
+ consumed += chunkLines;
245
+ remaining = remaining.slice(chunkLines);
246
+ }
247
+
248
+ if (chunks.length === 0 && movePath === undefined) {
249
+ throw new Error(
250
+ formatParseError({
251
+ type: "hunk",
252
+ lineNumber,
253
+ message: `Update file hunk for path '${cleanPath(updatePath)}' is empty. Include @@ plus at least one context, added, or removed line, or add *** Move to for a pure rename.`,
254
+ })
255
+ );
256
+ }
257
+
258
+ return {
259
+ hunk: {
260
+ kind: "update",
261
+ path: cleanPath(updatePath),
262
+ movePath: movePath === undefined ? undefined : cleanPath(movePath),
263
+ chunks,
264
+ },
265
+ consumed,
266
+ };
267
+ }
268
+
269
+ function parseUpdateChunk(
270
+ lines: readonly string[],
271
+ lineNumber: number,
272
+ allowMissingContext: boolean
273
+ ): { readonly chunk: UpdateChunk; readonly consumed: number } {
274
+ let changeContext: string | undefined;
275
+ let startIndex: number;
276
+
277
+ if (lines[0] === EMPTY_CHANGE_CONTEXT_MARKER) {
278
+ changeContext = undefined;
279
+ startIndex = 1;
280
+ } else {
281
+ const ctx = stripPrefix(lines[0]!, CHANGE_CONTEXT_MARKER);
282
+ if (ctx !== undefined) {
283
+ changeContext = ctx;
284
+ startIndex = 1;
285
+ } else {
286
+ if (!allowMissingContext) {
287
+ throw new Error(
288
+ formatParseError({
289
+ type: "hunk",
290
+ lineNumber,
291
+ message: `Expected update hunk to start with a @@ context marker, got: '${lines[0]}'. Start each additional edit chunk with @@ or @@ followed by nearby context.`,
292
+ })
293
+ );
294
+ }
295
+ changeContext = undefined;
296
+ startIndex = 0;
297
+ }
298
+ }
299
+
300
+ if (startIndex >= lines.length) {
301
+ throw new Error(
302
+ formatParseError({
303
+ type: "hunk",
304
+ lineNumber,
305
+ message:
306
+ "Update hunk does not contain any context, added, or removed lines. Include at least one line starting with ' ', '+', or '-'.",
307
+ })
308
+ );
309
+ }
310
+
311
+ const oldLines: string[] = [];
312
+ const newLines: string[] = [];
313
+ let isEndOfFile = false;
314
+ let parsed = 0;
315
+
316
+ for (const line of lines.slice(startIndex)) {
317
+ if (line === EOF_MARKER) {
318
+ if (parsed === 0) {
319
+ throw new Error(
320
+ formatParseError({
321
+ type: "hunk",
322
+ lineNumber,
323
+ message:
324
+ "Update hunk does not contain any context, added, or removed lines. Include at least one line starting with ' ', '+', or '-'.",
325
+ })
326
+ );
327
+ }
328
+ isEndOfFile = true;
329
+ parsed += 1;
330
+ break;
331
+ }
332
+
333
+ const marker = line[0];
334
+ if (marker === undefined) {
335
+ oldLines.push("");
336
+ newLines.push("");
337
+ } else if (marker === " ") {
338
+ oldLines.push(line.slice(1));
339
+ newLines.push(line.slice(1));
340
+ } else if (marker === "+") {
341
+ newLines.push(line.slice(1));
342
+ } else if (marker === "-") {
343
+ oldLines.push(line.slice(1));
344
+ } else {
345
+ if (parsed === 0) {
346
+ throw new Error(
347
+ formatParseError({
348
+ type: "hunk",
349
+ lineNumber,
350
+ message:
351
+ `Unexpected line found in update hunk: '${line}'. ` +
352
+ "Every line should start with ' ' (context line), '+' (added line), or '-' (removed line). " +
353
+ "Unchanged context lines must be prefixed with a single space.",
354
+ })
355
+ );
356
+ }
357
+ break;
358
+ }
359
+ parsed += 1;
360
+ }
361
+
362
+ return {
363
+ chunk: { changeContext, oldLines, newLines, isEndOfFile },
364
+ consumed: parsed + startIndex,
365
+ };
366
+ }
367
+
368
+ function stripPrefix(value: string, prefix: string): string | undefined {
369
+ return value.startsWith(prefix) ? value.slice(prefix.length) : undefined;
370
+ }
371
+
372
+ function isHunkHeader(line: string): boolean {
373
+ const trimmed = line.trim();
374
+ return (
375
+ trimmed.startsWith(ADD_FILE_MARKER) ||
376
+ trimmed.startsWith(DELETE_FILE_MARKER) ||
377
+ trimmed.startsWith(UPDATE_FILE_MARKER)
378
+ );
379
+ }
@@ -0,0 +1,177 @@
1
+ import type { ToolDiff } from "../../shared/DiffLines";
2
+ import { Paths } from "../../shared/Paths";
3
+ import { PatchSummary } from "../../shared/PatchSummary";
4
+ import type { ToolViewInput } from "../../shared/Tools";
5
+ import { DiffBlocks } from "../../view/DiffBlocks";
6
+ import { MovePath } from "../../view/MovePath";
7
+ import type { Span, ToolIcon, ToolView, ViewBlock } from "../../view/ViewBlock";
8
+ import type { ApplyEntry } from "./executor";
9
+ import { type applyPatchSchema, prepareApplyPatchArguments } from "./schema";
10
+
11
+ const ARROW = MovePath.ARROW;
12
+
13
+ export type ApplyPatchDetails = {
14
+ readonly entries?: readonly ApplyEntry[];
15
+ };
16
+
17
+ export type ApplyPatchViewInput = ToolViewInput<
18
+ typeof applyPatchSchema,
19
+ ApplyPatchDetails
20
+ >;
21
+
22
+ type EntryView = {
23
+ readonly label: string;
24
+ readonly icon: ToolIcon;
25
+ readonly title: readonly Span[];
26
+ readonly path: string | undefined;
27
+ readonly stats: readonly Span[];
28
+ readonly body: ToolDiff | undefined;
29
+ };
30
+
31
+ export function applyPatchView({
32
+ args,
33
+ result,
34
+ cwd,
35
+ }: ApplyPatchViewInput): ToolView {
36
+ const [first, ...rest] = visibleEntries(result?.details).map((entry) =>
37
+ describeEntry(entry, cwd)
38
+ );
39
+
40
+ if (first === undefined) {
41
+ return {
42
+ label: "Edit",
43
+ icon: "edit",
44
+ title: [{ kind: "file", path: callPath(args, cwd) }],
45
+ };
46
+ }
47
+
48
+ return {
49
+ label: first.label,
50
+ icon: first.icon,
51
+ title: [titleBlock(first), ...statsBlocks(first)],
52
+ body: [
53
+ ...DiffBlocks.body(first.body),
54
+ ...rest.flatMap((entry) => [
55
+ sectionBlock(entry),
56
+ ...DiffBlocks.body(entry.body),
57
+ ]),
58
+ ],
59
+ };
60
+ }
61
+
62
+ function visibleEntries(
63
+ details: ApplyPatchDetails | undefined
64
+ ): readonly ApplyEntry[] {
65
+ return (details?.entries ?? []).filter(
66
+ (entry) => !(entry.action.kind === "update" && entry.diff === undefined)
67
+ );
68
+ }
69
+
70
+ function callPath(
71
+ args: ApplyPatchViewInput["args"] | undefined,
72
+ cwd: string
73
+ ): string {
74
+ const prepared =
75
+ args === undefined ? undefined : prepareApplyPatchArguments(args);
76
+ const input =
77
+ typeof prepared?.input === "string" ? prepared.input : undefined;
78
+ const firstPath = input ? PatchSummary.firstPath(input) : undefined;
79
+ return Paths.titleOr(
80
+ firstPath ? Paths.resolve(firstPath, cwd) : undefined,
81
+ cwd
82
+ );
83
+ }
84
+
85
+ function describeEntry(entry: ApplyEntry, cwd: string): EntryView {
86
+ const rel = (p: string): string =>
87
+ Paths.toForwardSlashes(Paths.displayRelative(Paths.resolve(p, cwd), cwd));
88
+ const stats = DiffBlocks.statSpans(entry.diff);
89
+ const plain = (path: string): Pick<EntryView, "title" | "path"> => ({
90
+ title: [{ text: rel(path) }],
91
+ path: rel(path),
92
+ });
93
+
94
+ switch (entry.action.kind) {
95
+ case "add":
96
+ return {
97
+ label: "Write",
98
+ icon: "edit",
99
+ ...plain(entry.action.path),
100
+ stats,
101
+ body: entry.diff,
102
+ };
103
+ case "delete":
104
+ return {
105
+ label: "Delete",
106
+ icon: "trash",
107
+ ...plain(entry.action.path),
108
+ stats,
109
+ body: undefined,
110
+ };
111
+ case "move":
112
+ return {
113
+ label: entry.diff ? "Edit" : "Move",
114
+ icon: "edit",
115
+ title: moveTitle(
116
+ rel(entry.action.path),
117
+ rel(entry.action.movePath ?? entry.action.path)
118
+ ),
119
+ path: undefined,
120
+ stats,
121
+ body: entry.diff,
122
+ };
123
+ default:
124
+ return {
125
+ label: "Edit",
126
+ icon: "edit",
127
+ ...plain(entry.action.path),
128
+ stats,
129
+ body: entry.diff,
130
+ };
131
+ }
132
+ }
133
+
134
+ function titleBlock(entry: EntryView): ViewBlock {
135
+ return entry.path === undefined
136
+ ? { kind: "spans", spans: entry.title }
137
+ : { kind: "file", path: entry.path };
138
+ }
139
+
140
+ function statsBlocks(entry: EntryView): readonly ViewBlock[] {
141
+ return entry.stats.length === 0
142
+ ? []
143
+ : [{ kind: "spans", spans: entry.stats }];
144
+ }
145
+
146
+ function sectionBlock(entry: EntryView): ViewBlock {
147
+ return {
148
+ kind: "section",
149
+ label: entry.label,
150
+ icon: entry.icon,
151
+ content: [titleBlock(entry), ...statsBlocks(entry)],
152
+ };
153
+ }
154
+
155
+ function moveTitle(oldPath: string, newPath: string): readonly Span[] {
156
+ const folded = MovePath.fold(oldPath, newPath);
157
+
158
+ if (folded === undefined) {
159
+ return [
160
+ { text: oldPath, tone: "dim", strike: true },
161
+ { text: " " },
162
+ { text: ARROW, tone: "dim" },
163
+ { text: " " },
164
+ { text: newPath, tone: "title" },
165
+ ];
166
+ }
167
+
168
+ return [
169
+ { text: folded.prefix },
170
+ { text: "{", tone: "dim" },
171
+ { text: folded.from, tone: "dim", strike: true },
172
+ { text: ` ${ARROW} `, tone: "dim" },
173
+ { text: folded.to, tone: "title" },
174
+ { text: "}", tone: "dim" },
175
+ { text: folded.suffix, tone: "title" },
176
+ ];
177
+ }
@@ -0,0 +1,37 @@
1
+ import { type Static, Type } from "typebox";
2
+
3
+ export const applyPatchSchema = Type.Object({
4
+ input: Type.String({
5
+ description: "Patch text wrapped in *** Begin Patch / *** End Patch.",
6
+ }),
7
+ });
8
+
9
+ export type ApplyPatchInput = Static<typeof applyPatchSchema>;
10
+
11
+ const ALIAS_KEYS = ["patch", "patchText", "patch_text"] as const;
12
+
13
+ /** Normalize a bare string or a `patch`/`patchText`/`patch_text` key to `{input}`, dropping the alias. */
14
+ export function prepareApplyPatchArguments(rawArgs: unknown): ApplyPatchInput {
15
+ if (typeof rawArgs === "string") {
16
+ return { input: rawArgs };
17
+ }
18
+
19
+ if (rawArgs === null || typeof rawArgs !== "object") {
20
+ return rawArgs as ApplyPatchInput;
21
+ }
22
+
23
+ const record = rawArgs as Record<string, unknown>;
24
+ if (typeof record.input === "string") {
25
+ return rawArgs as ApplyPatchInput;
26
+ }
27
+
28
+ for (const key of ALIAS_KEYS) {
29
+ const value = record[key];
30
+ if (typeof value === "string") {
31
+ const { [key]: _dropped, ...rest } = record;
32
+ return { ...rest, input: value } as ApplyPatchInput;
33
+ }
34
+ }
35
+
36
+ return rawArgs as ApplyPatchInput;
37
+ }
@@ -0,0 +1,30 @@
1
+ export type UpdateChunk = {
2
+ readonly changeContext: string | undefined;
3
+ readonly oldLines: readonly string[];
4
+ readonly newLines: readonly string[];
5
+ readonly isEndOfFile: boolean;
6
+ };
7
+
8
+ export type AddHunk = {
9
+ readonly kind: "add";
10
+ readonly path: string;
11
+ readonly contents: string;
12
+ };
13
+
14
+ export type DeleteHunk = {
15
+ readonly kind: "delete";
16
+ readonly path: string;
17
+ };
18
+
19
+ export type UpdateHunk = {
20
+ readonly kind: "update";
21
+ readonly path: string;
22
+ readonly movePath: string | undefined;
23
+ readonly chunks: readonly UpdateChunk[];
24
+ };
25
+
26
+ export type Hunk = AddHunk | DeleteHunk | UpdateHunk;
27
+
28
+ export type Patch = {
29
+ readonly hunks: readonly Hunk[];
30
+ };
@@ -0,0 +1,86 @@
1
+ import { Lines } from "../../shared/Lines";
2
+ import {
3
+ type CapturedStream,
4
+ STREAM_HEAD_BYTES,
5
+ STREAM_TAIL_BYTES,
6
+ } from "./schema";
7
+
8
+ export class StreamCapture {
9
+ private chunks: Uint8Array[] = [];
10
+ private totalBytesAccum = 0;
11
+ private fullBytes: Uint8Array | null = null;
12
+
13
+ push(chunk: Uint8Array): void {
14
+ if (chunk.byteLength === 0) {
15
+ return;
16
+ }
17
+ this.chunks.push(chunk);
18
+ this.totalBytesAccum += chunk.byteLength;
19
+ }
20
+
21
+ get totalBytes(): number {
22
+ return this.totalBytesAccum;
23
+ }
24
+
25
+ /** The first `n` bytes, read across however many chunks the kernel split them into. */
26
+ lead(n: number): Uint8Array {
27
+ if (this.fullBytes) {
28
+ return this.fullBytes.subarray(0, n);
29
+ }
30
+ const lead = new Uint8Array(Math.min(n, this.totalBytesAccum));
31
+ let at = 0;
32
+ for (const chunk of this.chunks) {
33
+ if (at === lead.byteLength) {
34
+ break;
35
+ }
36
+ const take = chunk.subarray(0, lead.byteLength - at);
37
+ lead.set(take, at);
38
+ at += take.byteLength;
39
+ }
40
+ return lead;
41
+ }
42
+
43
+ full(): Uint8Array {
44
+ if (!this.fullBytes) {
45
+ this.fullBytes = Buffer.concat(this.chunks, this.totalBytesAccum);
46
+ this.chunks = [];
47
+ }
48
+ return this.fullBytes;
49
+ }
50
+
51
+ /** Opaque bytes are not text: a half-image is worthless, so they are neither cut nor decoded. */
52
+ snapshot(opaque = false): CapturedStream {
53
+ if (this.totalBytesAccum === 0 || opaque) {
54
+ return {
55
+ text: "",
56
+ totalBytes: this.totalBytesAccum,
57
+ truncated: false,
58
+ path: null,
59
+ nextStart: null,
60
+ };
61
+ }
62
+ const dec = new TextDecoder();
63
+ const all = this.full();
64
+ if (this.totalBytesAccum <= STREAM_HEAD_BYTES + STREAM_TAIL_BYTES) {
65
+ return {
66
+ text: dec.decode(all),
67
+ totalBytes: this.totalBytesAccum,
68
+ truncated: false,
69
+ path: null,
70
+ nextStart: null,
71
+ };
72
+ }
73
+ const headText = dec.decode(all.subarray(0, STREAM_HEAD_BYTES));
74
+ const tailText = dec.decode(
75
+ all.subarray(all.byteLength - STREAM_TAIL_BYTES)
76
+ );
77
+ const middle = this.totalBytesAccum - STREAM_HEAD_BYTES - STREAM_TAIL_BYTES;
78
+ return {
79
+ text: `${headText}\n... ${middle} bytes truncated ...\n${tailText}`,
80
+ totalBytes: this.totalBytesAccum,
81
+ truncated: true,
82
+ path: null,
83
+ nextStart: Lines.continuationLine(headText),
84
+ };
85
+ }
86
+ }