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,66 @@
1
+ import { StringEnum } from "@earendil-works/pi-ai";
2
+ import { type Static, Type } from "typebox";
3
+
4
+ export const GLOB_HEAD_LIMIT_MAX = 1000;
5
+
6
+ export const GLOB_PATH_FORMATS = ["relative", "absolute"] as const;
7
+
8
+ export const globSchema = Type.Object({
9
+ pattern: Type.String({
10
+ description:
11
+ "Glob pattern relative to path (eg. **/*.ts). Brace expansion spans sibling dirs (eg. {src,docs}/**/*.ts). A bare directory (eg. src/tools) expands to everything under it.",
12
+ }),
13
+ path: Type.Optional(
14
+ Type.String({
15
+ description:
16
+ "Absolute or relative path to directory (resolved against cwd). Defaults to cwd.",
17
+ })
18
+ ),
19
+ exclude: Type.Optional(
20
+ Type.Array(Type.String(), {
21
+ description:
22
+ "Glob patterns to exclude, relative to path (eg. **/*.test.ts or dist/**).",
23
+ })
24
+ ),
25
+ includeDotfiles: Type.Optional(
26
+ Type.Boolean({
27
+ description:
28
+ "Include dot-prefixed files and directories such as .env or .github. Defaults to false.",
29
+ })
30
+ ),
31
+ includeIgnored: Type.Optional(
32
+ Type.Boolean({
33
+ description:
34
+ "Include gitignored and normally ignored paths such as node_modules. Defaults to false.",
35
+ })
36
+ ),
37
+ pathFormat: Type.Optional(
38
+ StringEnum(GLOB_PATH_FORMATS, {
39
+ description:
40
+ "`relative` (default): output paths relative to cwd when possible. `absolute`: output absolute paths.",
41
+ })
42
+ ),
43
+ headLimit: Type.Optional(
44
+ Type.Integer({
45
+ minimum: 1,
46
+ maximum: GLOB_HEAD_LIMIT_MAX,
47
+ description: `Maximum returned entries. Defaults to ${GLOB_HEAD_LIMIT_MAX}.`,
48
+ })
49
+ ),
50
+ });
51
+
52
+ export type GlobInput = Static<typeof globSchema>;
53
+ export type GlobPathFormat = (typeof GLOB_PATH_FORMATS)[number];
54
+
55
+ export type GlobDetails = {
56
+ readonly absolutePath: string;
57
+ readonly pattern: string;
58
+ readonly exclude: readonly string[] | undefined;
59
+ readonly includeDotfiles: boolean;
60
+ readonly includeIgnored: boolean;
61
+ readonly pathFormat: GlobPathFormat;
62
+ readonly fileCount: number;
63
+ readonly totalItems: number;
64
+ readonly visibleItems: number;
65
+ readonly truncated: boolean;
66
+ };
@@ -0,0 +1,256 @@
1
+ import { FileScanner, type FileScanOptions } from "../../shared/FileScanner";
2
+ import { FsErrors } from "../../shared/FsErrors";
3
+ import { Lines } from "../../shared/Lines";
4
+ import { Paths } from "../../shared/Paths";
5
+ import { Pool } from "../../shared/Pool";
6
+
7
+ const MATCH_CONCURRENCY = 16;
8
+
9
+ export type GrepLine = {
10
+ readonly lineNumber: number;
11
+ readonly text: string;
12
+ };
13
+
14
+ export type GrepLineRange = {
15
+ readonly startLineNumber: number;
16
+ readonly endLineNumber: number;
17
+ };
18
+
19
+ export type GrepMatch = {
20
+ readonly filePath: string;
21
+ readonly mtime: number;
22
+ readonly lines: readonly GrepLine[];
23
+ readonly ranges: readonly GrepLineRange[];
24
+ readonly fileLines: readonly string[];
25
+ };
26
+
27
+ export type GrepMatcher = {
28
+ readonly regex: RegExp;
29
+ readonly matchAcrossLines: boolean;
30
+ /** Raw-byte needle for the literal fast path; undefined leaves the regex path unchanged. */
31
+ readonly literal: Buffer | undefined;
32
+ };
33
+
34
+ export type GrepScanOptions = FileScanOptions & {
35
+ readonly retainFileLines?: boolean;
36
+ };
37
+
38
+ // ASCII-only: these stand for themselves in both a default-flag regex and raw UTF-8 bytes.
39
+ const PURE_LITERAL = /^[A-Za-z0-9_ \-/]+$/;
40
+
41
+ function literalNeedle(
42
+ pattern: string,
43
+ caseInsensitive: boolean,
44
+ matchAcrossLines: boolean
45
+ ): Buffer | undefined {
46
+ if (caseInsensitive || matchAcrossLines || !PURE_LITERAL.test(pattern)) {
47
+ return undefined;
48
+ }
49
+ return Buffer.from(pattern, "utf8");
50
+ }
51
+
52
+ export function buildMatcher(options: {
53
+ readonly pattern: string;
54
+ readonly caseInsensitive: boolean;
55
+ readonly matchAcrossLines: boolean;
56
+ }): GrepMatcher {
57
+ const flags = `${options.matchAcrossLines ? "s" : ""}${
58
+ options.caseInsensitive ? "i" : ""
59
+ }`;
60
+
61
+ try {
62
+ return {
63
+ regex: new RegExp(options.pattern, flags),
64
+ matchAcrossLines: options.matchAcrossLines,
65
+ literal: literalNeedle(
66
+ options.pattern,
67
+ options.caseInsensitive,
68
+ options.matchAcrossLines
69
+ ),
70
+ };
71
+ } catch (error) {
72
+ const message = error instanceof Error ? error.message : String(error);
73
+ throw new Error(
74
+ `Invalid regular expression /${options.pattern}/${flags}: ${message}. Escape regex metacharacters for a literal search or simplify the pattern.`
75
+ );
76
+ }
77
+ }
78
+
79
+ export async function findMatches(
80
+ path: string,
81
+ glob: string | undefined,
82
+ matcher: GrepMatcher,
83
+ options: GrepScanOptions
84
+ ): Promise<readonly GrepMatch[]> {
85
+ const metadata = await FsErrors.statOrThrow(path);
86
+ const files = metadata.isFile()
87
+ ? [path]
88
+ : (await FileScanner.scan(path, glob ?? "**/*", options)).toSorted(
89
+ Paths.compare
90
+ );
91
+ const scanned = await Pool.mapPooled(files, MATCH_CONCURRENCY, (filePath) =>
92
+ matchFile(filePath, matcher, options.retainFileLines ?? true)
93
+ );
94
+
95
+ return scanned.filter((match) => match !== undefined);
96
+ }
97
+
98
+ // An entry may not be a readable regular file: an unreadable one is a non-match, never a failure.
99
+ async function matchFile(
100
+ filePath: string,
101
+ matcher: GrepMatcher,
102
+ retainFileLines: boolean
103
+ ): Promise<GrepMatch | undefined> {
104
+ const file = Bun.file(filePath);
105
+
106
+ let text: string;
107
+ try {
108
+ if (await Lines.isBinary(file)) {
109
+ return undefined;
110
+ }
111
+
112
+ if (matcher.literal !== undefined) {
113
+ // Literal fast path: an ASCII needle's raw-byte hit/miss matches the decoded result.
114
+ const bytes = Buffer.from(await file.arrayBuffer());
115
+ if (bytes.indexOf(matcher.literal) < 0) {
116
+ return undefined;
117
+ }
118
+ text = bytes.toString("utf8");
119
+ } else {
120
+ text = await file.text();
121
+ }
122
+ } catch {
123
+ return undefined;
124
+ }
125
+
126
+ const content = Lines.normalize(text);
127
+ const fileLines = Lines.splitNormalized(content);
128
+ const ranges = matcher.matchAcrossLines
129
+ ? regexRanges(content, matcher.regex)
130
+ : matchLineByLine(fileLines, matcher.regex);
131
+
132
+ if (ranges.length === 0) {
133
+ return undefined;
134
+ }
135
+
136
+ return {
137
+ filePath,
138
+ mtime: file.lastModified,
139
+ lines: linesForRanges(fileLines, ranges),
140
+ ranges,
141
+ fileLines: retainFileLines ? fileLines : [],
142
+ };
143
+ }
144
+
145
+ function matchLineByLine(
146
+ lines: readonly string[],
147
+ regex: RegExp
148
+ ): readonly GrepLineRange[] {
149
+ const ranges: GrepLineRange[] = [];
150
+
151
+ for (const [index, line] of lines.entries()) {
152
+ if (regex.test(line)) {
153
+ const lineNumber = index + 1;
154
+ ranges.push({ startLineNumber: lineNumber, endLineNumber: lineNumber });
155
+ }
156
+ }
157
+
158
+ return ranges;
159
+ }
160
+
161
+ function regexRanges(content: string, regex: RegExp): readonly GrepLineRange[] {
162
+ const globalRegex = new RegExp(regex.source, addFlag(regex.flags, "g"));
163
+ const ranges: GrepLineRange[] = [];
164
+ const cursor: LineCursor = { offset: 0, line: 1 };
165
+
166
+ while (true) {
167
+ const match = globalRegex.exec(content);
168
+
169
+ if (match === null) {
170
+ break;
171
+ }
172
+
173
+ ranges.push(
174
+ lineRangeForOffsets(
175
+ content,
176
+ match.index,
177
+ match.index + match[0].length,
178
+ cursor
179
+ )
180
+ );
181
+
182
+ if (match[0].length === 0) {
183
+ globalRegex.lastIndex += 1;
184
+ }
185
+ }
186
+
187
+ return ranges;
188
+ }
189
+
190
+ function addFlag(flags: string, flag: string): string {
191
+ return flags.includes(flag) ? flags : `${flags}${flag}`;
192
+ }
193
+
194
+ type LineCursor = {
195
+ offset: number;
196
+ line: number;
197
+ };
198
+
199
+ function lineRangeForOffsets(
200
+ content: string,
201
+ startOffset: number,
202
+ endOffset: number,
203
+ cursor: LineCursor
204
+ ): GrepLineRange {
205
+ return {
206
+ startLineNumber: lineNumberForOffset(content, startOffset, cursor),
207
+ endLineNumber: lineNumberForOffset(
208
+ content,
209
+ Math.max(startOffset, endOffset - 1),
210
+ cursor
211
+ ),
212
+ };
213
+ }
214
+
215
+ function lineNumberForOffset(
216
+ content: string,
217
+ offset: number,
218
+ cursor: LineCursor
219
+ ): number {
220
+ const target = Math.min(offset, content.length);
221
+
222
+ for (let index = cursor.offset; index < target; index += 1) {
223
+ if (content[index] === "\n") {
224
+ cursor.line += 1;
225
+ }
226
+ }
227
+
228
+ cursor.offset = target;
229
+
230
+ return cursor.line;
231
+ }
232
+
233
+ function linesForRanges(
234
+ fileLines: readonly string[],
235
+ ranges: readonly GrepLineRange[]
236
+ ): readonly GrepLine[] {
237
+ const seen = new Set<number>();
238
+ const lines: GrepLine[] = [];
239
+
240
+ for (const range of ranges) {
241
+ for (
242
+ let lineNumber = range.startLineNumber;
243
+ lineNumber <= range.endLineNumber;
244
+ lineNumber += 1
245
+ ) {
246
+ if (seen.has(lineNumber)) {
247
+ continue;
248
+ }
249
+
250
+ seen.add(lineNumber);
251
+ lines.push({ lineNumber, text: fileLines[lineNumber - 1] ?? "" });
252
+ }
253
+ }
254
+
255
+ return lines;
256
+ }
@@ -0,0 +1,114 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Paths } from "../../shared/Paths";
3
+ import { Renderer } from "../../shared/Renderer";
4
+ import { Tools } from "../../shared/Tools";
5
+ import { buildMatcher, findMatches } from "./grep";
6
+ import { buildView, type GrepViewDetails, renderMatches } from "./render";
7
+ import {
8
+ GREP_HEAD_LIMIT_MAX,
9
+ type GrepInput,
10
+ type GrepOutputMode,
11
+ type GrepPathFormat,
12
+ grepSchema,
13
+ } from "./schema";
14
+
15
+ const DEFAULT_OUTPUT_MODE: GrepOutputMode = "files_with_matches";
16
+ const DEFAULT_PATH_FORMAT: GrepPathFormat = "relative";
17
+
18
+ export default function (pi: ExtensionAPI): void {
19
+ Tools.register(pi, {
20
+ name: "grep",
21
+ label: "grep",
22
+ description:
23
+ "Search UTF-8 text files with a JavaScript regex. " +
24
+ "Directory scans skip binary files, gitignored paths, and dotfiles unless requested; direct file paths are always searched. " +
25
+ "Use grep to search file contents instead of bash with grep, rg, ag, find -exec, or similar.",
26
+ parameters: grepSchema,
27
+ renderShell: "self",
28
+ effect: { kind: "readOnly" },
29
+ executionMode: "parallel",
30
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
31
+ const {
32
+ pattern,
33
+ path,
34
+ glob,
35
+ exclude,
36
+ outputMode,
37
+ matchAcrossLines,
38
+ context,
39
+ includeDotfiles,
40
+ includeIgnored,
41
+ pathFormat,
42
+ caseInsensitive,
43
+ headLimit,
44
+ } = params as GrepInput;
45
+
46
+ if (signal?.aborted) {
47
+ throw new Error("Grep aborted before execution.");
48
+ }
49
+
50
+ const resolvedPathFormat = pathFormat ?? DEFAULT_PATH_FORMAT;
51
+ const resolvedContext = context ?? 0;
52
+ const resolvedOutputMode = outputMode ?? DEFAULT_OUTPUT_MODE;
53
+ const limit = Math.min(
54
+ headLimit ?? GREP_HEAD_LIMIT_MAX,
55
+ GREP_HEAD_LIMIT_MAX
56
+ );
57
+ const matcher = buildMatcher({
58
+ pattern,
59
+ caseInsensitive: caseInsensitive ?? false,
60
+ matchAcrossLines: matchAcrossLines ?? false,
61
+ });
62
+ const absolutePath = Paths.resolve(path ?? ".", ctx.cwd);
63
+ const matches = await findMatches(absolutePath, glob, matcher, {
64
+ exclude,
65
+ includeDotfiles: includeDotfiles ?? false,
66
+ includeIgnored: includeIgnored ?? false,
67
+ retainFileLines:
68
+ resolvedOutputMode === "content" && resolvedContext > 0,
69
+ });
70
+ const outcome = renderMatches(matches, resolvedOutputMode, limit, {
71
+ cwd: ctx.cwd,
72
+ pathFormat: resolvedPathFormat,
73
+ context: resolvedContext,
74
+ });
75
+ const content: Array<{ type: "text"; text: string }> = [
76
+ { type: "text", text: outcome.body },
77
+ ];
78
+
79
+ if (outcome.truncated) {
80
+ content.push({
81
+ type: "text",
82
+ text: `[grep tool: showing ${outcome.visibleItems} of ${outcome.totalItems} ${outcome.itemNoun}; narrow the pattern, scope to a specific path, or use a glob filter to reduce results.]`,
83
+ });
84
+ }
85
+
86
+ return {
87
+ content,
88
+ details: {
89
+ absolutePath,
90
+ outputMode: resolvedOutputMode,
91
+ exclude,
92
+ matchAcrossLines: matchAcrossLines ?? false,
93
+ context: resolvedContext,
94
+ includeDotfiles: includeDotfiles ?? false,
95
+ includeIgnored: includeIgnored ?? false,
96
+ pathFormat: resolvedPathFormat,
97
+ fileCount: outcome.fileCount,
98
+ totalMatches: outcome.totalMatches,
99
+ totalItems: outcome.totalItems,
100
+ visibleItems: outcome.visibleItems,
101
+ truncated: outcome.truncated,
102
+ },
103
+ };
104
+ },
105
+ toViewModel({ args, result, cwd }) {
106
+ return buildView({
107
+ args: (args ?? {}) as Partial<GrepInput>,
108
+ body: Renderer.firstText(result),
109
+ details: result?.details as GrepViewDetails | undefined,
110
+ cwd,
111
+ });
112
+ },
113
+ });
114
+ }
@@ -0,0 +1,271 @@
1
+ import { Format } from "../../shared/Format";
2
+ import { OutputBudget } from "../../shared/OutputBudget";
3
+ import { Paths } from "../../shared/Paths";
4
+ import {
5
+ NO_MATCHES,
6
+ type SearchList,
7
+ SearchRender,
8
+ } from "../../shared/SearchRender";
9
+ import type { ToolView, ViewBlock } from "../../view/ViewBlock";
10
+ import type { GrepLineRange, GrepMatch } from "./grep";
11
+ import type { GrepInput, GrepOutputMode, GrepPathFormat } from "./schema";
12
+
13
+ export type RenderOutcome = SearchList & {
14
+ readonly fileCount: number;
15
+ readonly totalMatches: number;
16
+ readonly itemNoun: string;
17
+ };
18
+
19
+ export type RenderOptions = {
20
+ readonly cwd: string;
21
+ readonly pathFormat: GrepPathFormat;
22
+ readonly context: number;
23
+ };
24
+
25
+ const renderers: Record<
26
+ GrepOutputMode,
27
+ {
28
+ readonly itemNoun: string;
29
+ readonly toLines: (
30
+ matches: readonly GrepMatch[],
31
+ options: RenderOptions
32
+ ) => readonly string[];
33
+ }
34
+ > = {
35
+ files_with_matches: { itemNoun: "files", toLines: renderFiles },
36
+ content: { itemNoun: "lines", toLines: renderContent },
37
+ count: { itemNoun: "files", toLines: renderCounts },
38
+ };
39
+
40
+ export function renderMatches(
41
+ matches: readonly GrepMatch[],
42
+ outputMode: GrepOutputMode,
43
+ headLimit: number,
44
+ options: RenderOptions
45
+ ): RenderOutcome {
46
+ const totalMatches = matches.reduce(
47
+ (sum, match) => sum + matchCount(match),
48
+ 0
49
+ );
50
+ const { itemNoun, toLines } = renderers[outputMode];
51
+ const lines = toLines(matches, options);
52
+ const list = SearchRender.capList(lines, headLimit);
53
+ const empty = list.totalItems === 0;
54
+
55
+ return {
56
+ ...list,
57
+ fileCount: empty ? 0 : matches.length,
58
+ totalMatches: empty ? 0 : totalMatches,
59
+ itemNoun,
60
+ };
61
+ }
62
+
63
+ export type TitleOptions = {
64
+ readonly pattern: string | undefined;
65
+ readonly path: string | undefined;
66
+ readonly glob: string | undefined;
67
+ readonly cwd: string;
68
+ };
69
+
70
+ export function formatTitle(options: TitleOptions): string {
71
+ return SearchRender.subjectTitle({
72
+ subject: formatPattern(options.pattern),
73
+ path: options.path,
74
+ suffix: options.glob,
75
+ cwd: options.cwd,
76
+ });
77
+ }
78
+
79
+ export type GrepViewDetails = {
80
+ readonly outputMode?: GrepOutputMode;
81
+ readonly fileCount?: number;
82
+ };
83
+
84
+ export type GrepViewInput = {
85
+ /** Partially streamed while the call is in flight; every field is optional. */
86
+ readonly args: Partial<GrepInput>;
87
+ /** The result body the tool already rendered for the model; empty in flight. */
88
+ readonly body: string;
89
+ readonly details?: GrepViewDetails;
90
+ readonly cwd: string;
91
+ };
92
+
93
+ export function buildView({
94
+ args,
95
+ body,
96
+ details,
97
+ cwd,
98
+ }: GrepViewInput): ToolView {
99
+ const fileCount = details?.fileCount;
100
+ return {
101
+ label: "Grep",
102
+ icon: "search",
103
+ title: [
104
+ {
105
+ kind: "text",
106
+ text: formatTitle({
107
+ pattern: args.pattern,
108
+ path: args.path,
109
+ glob: args.glob,
110
+ cwd,
111
+ }),
112
+ },
113
+ ...(fileCount === undefined
114
+ ? []
115
+ : ([
116
+ {
117
+ kind: "text",
118
+ tone: "muted",
119
+ text: Format.count(fileCount, "file"),
120
+ },
121
+ ] as const)),
122
+ ],
123
+ body: bodyBlocks(body, details?.outputMode),
124
+ };
125
+ }
126
+
127
+ function bodyBlocks(
128
+ body: string,
129
+ outputMode: GrepOutputMode | undefined
130
+ ): readonly ViewBlock[] {
131
+ if (
132
+ outputMode === "files_with_matches" &&
133
+ body !== "" &&
134
+ body !== NO_MATCHES
135
+ ) {
136
+ return body.split("\n").map((path) => ({ kind: "file", path }) as const);
137
+ }
138
+ return [{ kind: "text", text: body }];
139
+ }
140
+
141
+ function formatPattern(pattern: string | undefined): string {
142
+ return pattern === undefined ? "..." : `/${pattern}/`;
143
+ }
144
+
145
+ function renderFiles(
146
+ matches: readonly GrepMatch[],
147
+ options: RenderOptions
148
+ ): readonly string[] {
149
+ return byRecency(matches).map((match) =>
150
+ SearchRender.formatPath(match.filePath, options)
151
+ );
152
+ }
153
+
154
+ function renderContent(
155
+ matches: readonly GrepMatch[],
156
+ options: RenderOptions
157
+ ): readonly string[] {
158
+ if (options.context > 0) {
159
+ return byRecency(matches).flatMap((match) =>
160
+ renderContextContent(match, options)
161
+ );
162
+ }
163
+
164
+ return byRecency(matches).flatMap((match) =>
165
+ match.lines.map(
166
+ (line) =>
167
+ `${SearchRender.formatPath(match.filePath, options)}:${line.lineNumber}:${OutputBudget.truncateLine(line.text)}`
168
+ )
169
+ );
170
+ }
171
+
172
+ function renderContextContent(
173
+ match: GrepMatch,
174
+ options: RenderOptions
175
+ ): readonly string[] {
176
+ const path = SearchRender.formatPath(match.filePath, options);
177
+ const blocks = contextBlocks(
178
+ match.ranges,
179
+ match.fileLines.length,
180
+ options.context
181
+ );
182
+ const lines: string[] = [];
183
+
184
+ for (const [blockIndex, block] of blocks.entries()) {
185
+ if (blockIndex > 0) {
186
+ lines.push("--");
187
+ }
188
+
189
+ for (
190
+ let lineNumber = block.startLineNumber;
191
+ lineNumber <= block.endLineNumber;
192
+ lineNumber += 1
193
+ ) {
194
+ const marker = isMatchLine(match.ranges, lineNumber) ? ">" : " ";
195
+ const text = OutputBudget.truncateLine(
196
+ match.fileLines[lineNumber - 1] ?? ""
197
+ );
198
+ lines.push(`${marker} ${path}:${lineNumber}:${text}`);
199
+ }
200
+ }
201
+
202
+ return lines;
203
+ }
204
+
205
+ function renderCounts(
206
+ matches: readonly GrepMatch[],
207
+ options: RenderOptions
208
+ ): readonly string[] {
209
+ return [...matches]
210
+ .sort(
211
+ (left, right) =>
212
+ matchCount(right) - matchCount(left) ||
213
+ right.mtime - left.mtime ||
214
+ Paths.compare(left.filePath, right.filePath)
215
+ )
216
+ .map(
217
+ (match) =>
218
+ `${SearchRender.formatPath(match.filePath, options)}:${matchCount(match)}`
219
+ );
220
+ }
221
+
222
+ function contextBlocks(
223
+ ranges: readonly GrepLineRange[],
224
+ lineCount: number,
225
+ context: number
226
+ ): readonly GrepLineRange[] {
227
+ const blocks: GrepLineRange[] = [];
228
+
229
+ for (const range of ranges) {
230
+ const expanded = {
231
+ startLineNumber: Math.max(1, range.startLineNumber - context),
232
+ endLineNumber: Math.min(lineCount, range.endLineNumber + context),
233
+ };
234
+ const previous = blocks.at(-1);
235
+
236
+ if (
237
+ previous !== undefined &&
238
+ expanded.startLineNumber <= previous.endLineNumber + 1
239
+ ) {
240
+ blocks[blocks.length - 1] = {
241
+ startLineNumber: previous.startLineNumber,
242
+ endLineNumber: Math.max(previous.endLineNumber, expanded.endLineNumber),
243
+ };
244
+ } else {
245
+ blocks.push(expanded);
246
+ }
247
+ }
248
+
249
+ return blocks;
250
+ }
251
+
252
+ function matchCount(match: GrepMatch): number {
253
+ return match.ranges.length;
254
+ }
255
+
256
+ function isMatchLine(
257
+ ranges: readonly GrepLineRange[],
258
+ lineNumber: number
259
+ ): boolean {
260
+ return ranges.some(
261
+ (range) =>
262
+ lineNumber >= range.startLineNumber && lineNumber <= range.endLineNumber
263
+ );
264
+ }
265
+
266
+ function byRecency(matches: readonly GrepMatch[]): readonly GrepMatch[] {
267
+ return [...matches].sort(
268
+ (left, right) =>
269
+ right.mtime - left.mtime || Paths.compare(left.filePath, right.filePath)
270
+ );
271
+ }