@oh-my-pi/pi-coding-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/CHANGELOG.md +1629 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/config-usage.md +113 -0
  5. package/docs/custom-tools.md +541 -0
  6. package/docs/extension-loading.md +1004 -0
  7. package/docs/hooks.md +867 -0
  8. package/docs/rpc.md +1040 -0
  9. package/docs/sdk.md +994 -0
  10. package/docs/session-tree-plan.md +441 -0
  11. package/docs/session.md +240 -0
  12. package/docs/skills.md +290 -0
  13. package/docs/theme.md +670 -0
  14. package/docs/tree.md +197 -0
  15. package/docs/tui.md +341 -0
  16. package/examples/README.md +21 -0
  17. package/examples/custom-tools/README.md +124 -0
  18. package/examples/custom-tools/hello/index.ts +20 -0
  19. package/examples/custom-tools/question/index.ts +84 -0
  20. package/examples/custom-tools/subagent/README.md +172 -0
  21. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +89 -0
  57. package/src/bun-imports.d.ts +16 -0
  58. package/src/capability/context-file.ts +40 -0
  59. package/src/capability/extension.ts +48 -0
  60. package/src/capability/hook.ts +40 -0
  61. package/src/capability/index.ts +616 -0
  62. package/src/capability/instruction.ts +37 -0
  63. package/src/capability/mcp.ts +52 -0
  64. package/src/capability/prompt.ts +35 -0
  65. package/src/capability/rule.ts +56 -0
  66. package/src/capability/settings.ts +35 -0
  67. package/src/capability/skill.ts +49 -0
  68. package/src/capability/slash-command.ts +40 -0
  69. package/src/capability/system-prompt.ts +35 -0
  70. package/src/capability/tool.ts +38 -0
  71. package/src/capability/types.ts +166 -0
  72. package/src/cli/args.ts +259 -0
  73. package/src/cli/file-processor.ts +121 -0
  74. package/src/cli/list-models.ts +104 -0
  75. package/src/cli/plugin-cli.ts +661 -0
  76. package/src/cli/session-picker.ts +41 -0
  77. package/src/cli/update-cli.ts +274 -0
  78. package/src/cli.ts +10 -0
  79. package/src/config.ts +391 -0
  80. package/src/core/agent-session.ts +2178 -0
  81. package/src/core/auth-storage.ts +258 -0
  82. package/src/core/bash-executor.ts +197 -0
  83. package/src/core/compaction/branch-summarization.ts +315 -0
  84. package/src/core/compaction/compaction.ts +664 -0
  85. package/src/core/compaction/index.ts +7 -0
  86. package/src/core/compaction/utils.ts +153 -0
  87. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  88. package/src/core/custom-commands/index.ts +15 -0
  89. package/src/core/custom-commands/loader.ts +226 -0
  90. package/src/core/custom-commands/types.ts +112 -0
  91. package/src/core/custom-tools/index.ts +22 -0
  92. package/src/core/custom-tools/loader.ts +248 -0
  93. package/src/core/custom-tools/types.ts +185 -0
  94. package/src/core/custom-tools/wrapper.ts +29 -0
  95. package/src/core/exec.ts +139 -0
  96. package/src/core/export-html/index.ts +159 -0
  97. package/src/core/export-html/template.css +774 -0
  98. package/src/core/export-html/template.generated.ts +2 -0
  99. package/src/core/export-html/template.html +45 -0
  100. package/src/core/export-html/template.js +1185 -0
  101. package/src/core/export-html/template.macro.ts +24 -0
  102. package/src/core/file-mentions.ts +54 -0
  103. package/src/core/hooks/index.ts +16 -0
  104. package/src/core/hooks/loader.ts +288 -0
  105. package/src/core/hooks/runner.ts +434 -0
  106. package/src/core/hooks/tool-wrapper.ts +98 -0
  107. package/src/core/hooks/types.ts +770 -0
  108. package/src/core/index.ts +53 -0
  109. package/src/core/logger.ts +112 -0
  110. package/src/core/mcp/client.ts +185 -0
  111. package/src/core/mcp/config.ts +248 -0
  112. package/src/core/mcp/index.ts +45 -0
  113. package/src/core/mcp/loader.ts +99 -0
  114. package/src/core/mcp/manager.ts +235 -0
  115. package/src/core/mcp/tool-bridge.ts +156 -0
  116. package/src/core/mcp/transports/http.ts +316 -0
  117. package/src/core/mcp/transports/index.ts +6 -0
  118. package/src/core/mcp/transports/stdio.ts +252 -0
  119. package/src/core/mcp/types.ts +228 -0
  120. package/src/core/messages.ts +211 -0
  121. package/src/core/model-registry.ts +334 -0
  122. package/src/core/model-resolver.ts +494 -0
  123. package/src/core/plugins/doctor.ts +67 -0
  124. package/src/core/plugins/index.ts +38 -0
  125. package/src/core/plugins/installer.ts +189 -0
  126. package/src/core/plugins/loader.ts +339 -0
  127. package/src/core/plugins/manager.ts +672 -0
  128. package/src/core/plugins/parser.ts +105 -0
  129. package/src/core/plugins/paths.ts +37 -0
  130. package/src/core/plugins/types.ts +190 -0
  131. package/src/core/sdk.ts +900 -0
  132. package/src/core/session-manager.ts +1837 -0
  133. package/src/core/settings-manager.ts +860 -0
  134. package/src/core/skills.ts +352 -0
  135. package/src/core/slash-commands.ts +132 -0
  136. package/src/core/system-prompt.ts +442 -0
  137. package/src/core/timings.ts +25 -0
  138. package/src/core/title-generator.ts +110 -0
  139. package/src/core/tools/ask.ts +193 -0
  140. package/src/core/tools/bash-interceptor.ts +120 -0
  141. package/src/core/tools/bash.ts +91 -0
  142. package/src/core/tools/context.ts +32 -0
  143. package/src/core/tools/edit-diff.ts +487 -0
  144. package/src/core/tools/edit.ts +140 -0
  145. package/src/core/tools/exa/company.ts +59 -0
  146. package/src/core/tools/exa/index.ts +63 -0
  147. package/src/core/tools/exa/linkedin.ts +59 -0
  148. package/src/core/tools/exa/mcp-client.ts +368 -0
  149. package/src/core/tools/exa/render.ts +200 -0
  150. package/src/core/tools/exa/researcher.ts +90 -0
  151. package/src/core/tools/exa/search.ts +338 -0
  152. package/src/core/tools/exa/types.ts +167 -0
  153. package/src/core/tools/exa/websets.ts +248 -0
  154. package/src/core/tools/find.ts +244 -0
  155. package/src/core/tools/grep.ts +584 -0
  156. package/src/core/tools/index.ts +283 -0
  157. package/src/core/tools/ls.ts +142 -0
  158. package/src/core/tools/lsp/client.ts +767 -0
  159. package/src/core/tools/lsp/clients/biome-client.ts +207 -0
  160. package/src/core/tools/lsp/clients/index.ts +49 -0
  161. package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
  162. package/src/core/tools/lsp/config.ts +845 -0
  163. package/src/core/tools/lsp/edits.ts +110 -0
  164. package/src/core/tools/lsp/index.ts +1364 -0
  165. package/src/core/tools/lsp/render.ts +560 -0
  166. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  167. package/src/core/tools/lsp/types.ts +495 -0
  168. package/src/core/tools/lsp/utils.ts +526 -0
  169. package/src/core/tools/notebook.ts +182 -0
  170. package/src/core/tools/output.ts +198 -0
  171. package/src/core/tools/path-utils.ts +61 -0
  172. package/src/core/tools/read.ts +507 -0
  173. package/src/core/tools/renderers.ts +820 -0
  174. package/src/core/tools/review.ts +275 -0
  175. package/src/core/tools/rulebook.ts +124 -0
  176. package/src/core/tools/task/agents.ts +158 -0
  177. package/src/core/tools/task/artifacts.ts +114 -0
  178. package/src/core/tools/task/commands.ts +157 -0
  179. package/src/core/tools/task/discovery.ts +217 -0
  180. package/src/core/tools/task/executor.ts +531 -0
  181. package/src/core/tools/task/index.ts +548 -0
  182. package/src/core/tools/task/model-resolver.ts +176 -0
  183. package/src/core/tools/task/parallel.ts +38 -0
  184. package/src/core/tools/task/render.ts +502 -0
  185. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  186. package/src/core/tools/task/types.ts +142 -0
  187. package/src/core/tools/truncate.ts +265 -0
  188. package/src/core/tools/web-fetch.ts +2511 -0
  189. package/src/core/tools/web-search/auth.ts +199 -0
  190. package/src/core/tools/web-search/index.ts +583 -0
  191. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  192. package/src/core/tools/web-search/providers/exa.ts +196 -0
  193. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  194. package/src/core/tools/web-search/render.ts +372 -0
  195. package/src/core/tools/web-search/types.ts +180 -0
  196. package/src/core/tools/write.ts +63 -0
  197. package/src/core/ttsr.ts +211 -0
  198. package/src/core/utils.ts +187 -0
  199. package/src/discovery/agents-md.ts +75 -0
  200. package/src/discovery/builtin.ts +647 -0
  201. package/src/discovery/claude.ts +623 -0
  202. package/src/discovery/cline.ts +104 -0
  203. package/src/discovery/codex.ts +571 -0
  204. package/src/discovery/cursor.ts +266 -0
  205. package/src/discovery/gemini.ts +368 -0
  206. package/src/discovery/github.ts +120 -0
  207. package/src/discovery/helpers.test.ts +127 -0
  208. package/src/discovery/helpers.ts +249 -0
  209. package/src/discovery/index.ts +84 -0
  210. package/src/discovery/mcp-json.ts +127 -0
  211. package/src/discovery/vscode.ts +99 -0
  212. package/src/discovery/windsurf.ts +219 -0
  213. package/src/index.ts +192 -0
  214. package/src/main.ts +507 -0
  215. package/src/migrations.ts +156 -0
  216. package/src/modes/cleanup.ts +23 -0
  217. package/src/modes/index.ts +48 -0
  218. package/src/modes/interactive/components/armin.ts +382 -0
  219. package/src/modes/interactive/components/assistant-message.ts +86 -0
  220. package/src/modes/interactive/components/bash-execution.ts +199 -0
  221. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  222. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  223. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  224. package/src/modes/interactive/components/custom-editor.ts +122 -0
  225. package/src/modes/interactive/components/diff.ts +147 -0
  226. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  227. package/src/modes/interactive/components/extensions/extension-dashboard.ts +296 -0
  228. package/src/modes/interactive/components/extensions/extension-list.ts +479 -0
  229. package/src/modes/interactive/components/extensions/index.ts +9 -0
  230. package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
  231. package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
  232. package/src/modes/interactive/components/extensions/types.ts +191 -0
  233. package/src/modes/interactive/components/hook-editor.ts +117 -0
  234. package/src/modes/interactive/components/hook-input.ts +64 -0
  235. package/src/modes/interactive/components/hook-message.ts +96 -0
  236. package/src/modes/interactive/components/hook-selector.ts +91 -0
  237. package/src/modes/interactive/components/model-selector.ts +560 -0
  238. package/src/modes/interactive/components/oauth-selector.ts +136 -0
  239. package/src/modes/interactive/components/plugin-settings.ts +481 -0
  240. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  241. package/src/modes/interactive/components/session-selector.ts +220 -0
  242. package/src/modes/interactive/components/settings-defs.ts +597 -0
  243. package/src/modes/interactive/components/settings-selector.ts +545 -0
  244. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  245. package/src/modes/interactive/components/status-line/index.ts +4 -0
  246. package/src/modes/interactive/components/status-line/presets.ts +94 -0
  247. package/src/modes/interactive/components/status-line/segments.ts +350 -0
  248. package/src/modes/interactive/components/status-line/separators.ts +55 -0
  249. package/src/modes/interactive/components/status-line/types.ts +81 -0
  250. package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
  251. package/src/modes/interactive/components/status-line.ts +384 -0
  252. package/src/modes/interactive/components/theme-selector.ts +62 -0
  253. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  254. package/src/modes/interactive/components/tool-execution.ts +946 -0
  255. package/src/modes/interactive/components/tree-selector.ts +877 -0
  256. package/src/modes/interactive/components/ttsr-notification.ts +82 -0
  257. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  258. package/src/modes/interactive/components/user-message.ts +18 -0
  259. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  260. package/src/modes/interactive/components/welcome.ts +228 -0
  261. package/src/modes/interactive/interactive-mode.ts +2669 -0
  262. package/src/modes/interactive/theme/dark.json +102 -0
  263. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  264. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  265. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  266. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  267. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  268. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  269. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  270. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  271. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  272. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  273. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  274. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  275. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  276. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  277. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  278. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  279. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  280. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  281. package/src/modes/interactive/theme/defaults/index.ts +67 -0
  282. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  283. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  284. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  285. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  286. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  287. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  288. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  289. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  290. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  291. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  292. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  293. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  294. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  295. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  296. package/src/modes/interactive/theme/light.json +99 -0
  297. package/src/modes/interactive/theme/theme-schema.json +424 -0
  298. package/src/modes/interactive/theme/theme.ts +2211 -0
  299. package/src/modes/print-mode.ts +163 -0
  300. package/src/modes/rpc/rpc-client.ts +527 -0
  301. package/src/modes/rpc/rpc-mode.ts +494 -0
  302. package/src/modes/rpc/rpc-types.ts +203 -0
  303. package/src/prompts/architect-plan.md +10 -0
  304. package/src/prompts/branch-summary-preamble.md +3 -0
  305. package/src/prompts/branch-summary.md +28 -0
  306. package/src/prompts/browser.md +71 -0
  307. package/src/prompts/compaction-summary.md +34 -0
  308. package/src/prompts/compaction-turn-prefix.md +16 -0
  309. package/src/prompts/compaction-update-summary.md +41 -0
  310. package/src/prompts/explore.md +82 -0
  311. package/src/prompts/implement-with-critic.md +11 -0
  312. package/src/prompts/implement.md +11 -0
  313. package/src/prompts/init.md +30 -0
  314. package/src/prompts/plan.md +54 -0
  315. package/src/prompts/reviewer.md +81 -0
  316. package/src/prompts/summarization-system.md +3 -0
  317. package/src/prompts/system-prompt.md +27 -0
  318. package/src/prompts/task.md +56 -0
  319. package/src/prompts/title-system.md +8 -0
  320. package/src/prompts/tools/ask.md +24 -0
  321. package/src/prompts/tools/bash.md +23 -0
  322. package/src/prompts/tools/edit.md +9 -0
  323. package/src/prompts/tools/find.md +6 -0
  324. package/src/prompts/tools/grep.md +12 -0
  325. package/src/prompts/tools/lsp.md +14 -0
  326. package/src/prompts/tools/output.md +23 -0
  327. package/src/prompts/tools/read.md +25 -0
  328. package/src/prompts/tools/web-fetch.md +8 -0
  329. package/src/prompts/tools/web-search.md +10 -0
  330. package/src/prompts/tools/write.md +10 -0
  331. package/src/utils/changelog.ts +99 -0
  332. package/src/utils/clipboard.ts +265 -0
  333. package/src/utils/fuzzy.ts +108 -0
  334. package/src/utils/mime.ts +30 -0
  335. package/src/utils/shell-snapshot.ts +218 -0
  336. package/src/utils/shell.ts +364 -0
  337. package/src/utils/tools-manager.ts +265 -0
@@ -0,0 +1,584 @@
1
+ import { readFileSync, type Stats, statSync } from "node:fs";
2
+ import nodePath from "node:path";
3
+ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
4
+ import { Type } from "@sinclair/typebox";
5
+ import type { Subprocess } from "bun";
6
+ import grepDescription from "../../prompts/tools/grep.md" with { type: "text" };
7
+ import { ensureTool } from "../../utils/tools-manager";
8
+ import { resolveToCwd } from "./path-utils";
9
+ import {
10
+ DEFAULT_MAX_BYTES,
11
+ formatSize,
12
+ GREP_MAX_LINE_LENGTH,
13
+ type TruncationResult,
14
+ truncateHead,
15
+ truncateLine,
16
+ } from "./truncate";
17
+
18
+ const grepSchema = Type.Object({
19
+ pattern: Type.String({ description: "Search pattern (regex or literal string)" }),
20
+ path: Type.Optional(Type.String({ description: "Directory or file to search (default: current directory)" })),
21
+ glob: Type.Optional(Type.String({ description: "Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'" })),
22
+ type: Type.Optional(Type.String({ description: "File type filter (e.g., 'ts', 'rust', 'py')" })),
23
+ ignoreCase: Type.Optional(
24
+ Type.Boolean({ description: "Force case-insensitive search (default: false, uses smart-case otherwise)" }),
25
+ ),
26
+ caseSensitive: Type.Optional(
27
+ Type.Boolean({ description: "Force case-sensitive search (default: false, disables smart-case)" }),
28
+ ),
29
+ literal: Type.Optional(
30
+ Type.Boolean({ description: "Treat pattern as literal string instead of regex (default: false)" }),
31
+ ),
32
+ multiline: Type.Optional(
33
+ Type.Boolean({ description: "Enable multiline matching for cross-line patterns (default: false)" }),
34
+ ),
35
+ context: Type.Optional(
36
+ Type.Number({ description: "Number of lines to show before and after each match (default: 0)" }),
37
+ ),
38
+ limit: Type.Optional(Type.Number({ description: "Maximum number of matches to return (default: 100)" })),
39
+ outputMode: Type.Optional(
40
+ Type.Union([Type.Literal("content"), Type.Literal("files_with_matches"), Type.Literal("count")], {
41
+ description:
42
+ "Output mode: 'content' shows matching lines, 'files_with_matches' shows only file paths, 'count' shows match counts per file (default: 'content')",
43
+ }),
44
+ ),
45
+ headLimit: Type.Optional(Type.Number({ description: "Limit output to first N results (default: unlimited)" })),
46
+ offset: Type.Optional(Type.Number({ description: "Skip first N results before applying headLimit (default: 0)" })),
47
+ });
48
+
49
+ const DEFAULT_LIMIT = 100;
50
+
51
+ export interface GrepToolDetails {
52
+ truncation?: TruncationResult;
53
+ matchLimitReached?: number;
54
+ headLimitReached?: number;
55
+ linesTruncated?: boolean;
56
+ // Fields for TUI rendering
57
+ scopePath?: string;
58
+ matchCount?: number;
59
+ fileCount?: number;
60
+ files?: string[];
61
+ fileMatches?: Array<{ path: string; count: number }>;
62
+ mode?: "content" | "files_with_matches" | "count";
63
+ truncated?: boolean;
64
+ error?: string;
65
+ }
66
+
67
+ export function createGrepTool(cwd: string): AgentTool<typeof grepSchema> {
68
+ return {
69
+ name: "grep",
70
+ label: "Grep",
71
+ description: grepDescription,
72
+ parameters: grepSchema,
73
+ execute: async (
74
+ _toolCallId: string,
75
+ {
76
+ pattern,
77
+ path: searchDir,
78
+ glob,
79
+ type,
80
+ ignoreCase,
81
+ caseSensitive,
82
+ literal,
83
+ multiline,
84
+ context,
85
+ limit,
86
+ outputMode,
87
+ headLimit,
88
+ offset,
89
+ }: {
90
+ pattern: string;
91
+ path?: string;
92
+ glob?: string;
93
+ type?: string;
94
+ ignoreCase?: boolean;
95
+ caseSensitive?: boolean;
96
+ literal?: boolean;
97
+ multiline?: boolean;
98
+ context?: number;
99
+ limit?: number;
100
+ outputMode?: "content" | "files_with_matches" | "count";
101
+ headLimit?: number;
102
+ offset?: number;
103
+ },
104
+ signal?: AbortSignal,
105
+ ) => {
106
+ if (signal?.aborted) {
107
+ throw new Error("Operation aborted");
108
+ }
109
+
110
+ const rgPath = await ensureTool("rg", true);
111
+ if (!rgPath) {
112
+ throw new Error("ripgrep (rg) is not available and could not be downloaded");
113
+ }
114
+
115
+ const searchPath = resolveToCwd(searchDir || ".", cwd);
116
+ const scopePath = (() => {
117
+ const relative = nodePath.relative(cwd, searchPath).replace(/\\/g, "/");
118
+ return relative.length === 0 ? "." : relative;
119
+ })();
120
+ let searchStat: Stats;
121
+ try {
122
+ searchStat = statSync(searchPath);
123
+ } catch (_err) {
124
+ throw new Error(`Path not found: ${searchPath}`);
125
+ }
126
+
127
+ const isDirectory = searchStat.isDirectory();
128
+ const contextValue = context && context > 0 ? context : 0;
129
+ const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);
130
+ const effectiveOutputMode = outputMode ?? "content";
131
+ const effectiveOffset = offset && offset > 0 ? offset : 0;
132
+ const hasHeadLimit = headLimit !== undefined && headLimit > 0;
133
+
134
+ const formatPath = (filePath: string): string => {
135
+ if (isDirectory) {
136
+ const relative = nodePath.relative(searchPath, filePath);
137
+ if (relative && !relative.startsWith("..")) {
138
+ return relative.replace(/\\/g, "/");
139
+ }
140
+ }
141
+ return nodePath.basename(filePath);
142
+ };
143
+
144
+ const fileCache = new Map<string, string[]>();
145
+ const getFileLines = (filePath: string): string[] => {
146
+ let lines = fileCache.get(filePath);
147
+ if (!lines) {
148
+ try {
149
+ const content = readFileSync(filePath, "utf-8");
150
+ lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
151
+ } catch {
152
+ lines = [];
153
+ }
154
+ fileCache.set(filePath, lines);
155
+ }
156
+ return lines;
157
+ };
158
+
159
+ const args: string[] = [];
160
+
161
+ // Base arguments depend on output mode
162
+ if (effectiveOutputMode === "files_with_matches") {
163
+ args.push("--files-with-matches", "--color=never", "--hidden");
164
+ } else if (effectiveOutputMode === "count") {
165
+ args.push("--count", "--color=never", "--hidden");
166
+ } else {
167
+ args.push("--json", "--line-number", "--color=never", "--hidden");
168
+ }
169
+
170
+ if (caseSensitive) {
171
+ args.push("--case-sensitive");
172
+ } else if (ignoreCase) {
173
+ args.push("--ignore-case");
174
+ } else {
175
+ args.push("--smart-case");
176
+ }
177
+
178
+ if (multiline) {
179
+ args.push("--multiline");
180
+ }
181
+
182
+ if (literal) {
183
+ args.push("--fixed-strings");
184
+ }
185
+
186
+ if (glob) {
187
+ args.push("--glob", glob);
188
+ }
189
+
190
+ if (type) {
191
+ args.push("--type", type);
192
+ }
193
+
194
+ args.push(pattern, searchPath);
195
+
196
+ const child: Subprocess = Bun.spawn([rgPath, ...args], {
197
+ stdin: "ignore",
198
+ stdout: "pipe",
199
+ stderr: "pipe",
200
+ });
201
+
202
+ let stderr = "";
203
+ let matchCount = 0;
204
+ let matchLimitReached = false;
205
+ let linesTruncated = false;
206
+ let aborted = false;
207
+ let killedDueToLimit = false;
208
+ const outputLines: string[] = [];
209
+ const files = new Set<string>();
210
+ const fileList: string[] = [];
211
+ const fileMatchCounts = new Map<string, number>();
212
+
213
+ const recordFile = (filePath: string) => {
214
+ const relative = formatPath(filePath);
215
+ if (!files.has(relative)) {
216
+ files.add(relative);
217
+ fileList.push(relative);
218
+ }
219
+ };
220
+
221
+ const recordFileMatch = (filePath: string) => {
222
+ const relative = formatPath(filePath);
223
+ fileMatchCounts.set(relative, (fileMatchCounts.get(relative) ?? 0) + 1);
224
+ };
225
+
226
+ const stopChild = (dueToLimit: boolean = false) => {
227
+ killedDueToLimit = dueToLimit;
228
+ child.kill();
229
+ };
230
+
231
+ const onAbort = () => {
232
+ aborted = true;
233
+ stopChild();
234
+ };
235
+
236
+ if (signal) {
237
+ signal.addEventListener("abort", onAbort, { once: true });
238
+ }
239
+
240
+ // For simple output modes (files_with_matches, count), process text directly
241
+ if (effectiveOutputMode === "files_with_matches" || effectiveOutputMode === "count") {
242
+ const stdoutReader = (child.stdout as ReadableStream<Uint8Array>).getReader();
243
+ const stderrReader = (child.stderr as ReadableStream<Uint8Array>).getReader();
244
+ const decoder = new TextDecoder();
245
+ let stdout = "";
246
+
247
+ await Promise.all([
248
+ (async () => {
249
+ while (true) {
250
+ const { done, value } = await stdoutReader.read();
251
+ if (done) break;
252
+ stdout += decoder.decode(value, { stream: true });
253
+ }
254
+ })(),
255
+ (async () => {
256
+ while (true) {
257
+ const { done, value } = await stderrReader.read();
258
+ if (done) break;
259
+ stderr += decoder.decode(value, { stream: true });
260
+ }
261
+ })(),
262
+ ]);
263
+
264
+ const exitCode = await child.exited;
265
+
266
+ if (signal) {
267
+ signal.removeEventListener("abort", onAbort);
268
+ }
269
+
270
+ if (aborted) {
271
+ throw new Error("Operation aborted");
272
+ }
273
+
274
+ if (exitCode !== 0 && exitCode !== 1) {
275
+ const errorMsg = stderr.trim() || `ripgrep exited with code ${exitCode}`;
276
+ throw new Error(errorMsg);
277
+ }
278
+
279
+ const lines = stdout
280
+ .trim()
281
+ .split("\n")
282
+ .filter((line) => line.length > 0);
283
+
284
+ if (lines.length === 0) {
285
+ return {
286
+ content: [{ type: "text", text: "No matches found" }],
287
+ details: {
288
+ scopePath,
289
+ matchCount: 0,
290
+ fileCount: 0,
291
+ files: [],
292
+ mode: effectiveOutputMode,
293
+ truncated: false,
294
+ },
295
+ };
296
+ }
297
+
298
+ // Apply offset and headLimit
299
+ let processedLines = lines;
300
+ if (effectiveOffset > 0) {
301
+ processedLines = processedLines.slice(effectiveOffset);
302
+ }
303
+ if (hasHeadLimit) {
304
+ processedLines = processedLines.slice(0, headLimit);
305
+ }
306
+
307
+ let simpleMatchCount = 0;
308
+ let fileCount = 0;
309
+ const simpleFiles = new Set<string>();
310
+ const simpleFileList: string[] = [];
311
+ const simpleFileMatchCounts = new Map<string, number>();
312
+
313
+ const recordSimpleFile = (filePath: string) => {
314
+ const relative = formatPath(filePath);
315
+ if (!simpleFiles.has(relative)) {
316
+ simpleFiles.add(relative);
317
+ simpleFileList.push(relative);
318
+ }
319
+ };
320
+
321
+ const recordSimpleFileMatch = (filePath: string, count: number) => {
322
+ const relative = formatPath(filePath);
323
+ simpleFileMatchCounts.set(relative, count);
324
+ };
325
+
326
+ if (effectiveOutputMode === "files_with_matches") {
327
+ for (const line of lines) {
328
+ recordSimpleFile(line);
329
+ }
330
+ fileCount = simpleFiles.size;
331
+ simpleMatchCount = fileCount;
332
+ } else {
333
+ for (const line of lines) {
334
+ const separatorIndex = line.lastIndexOf(":");
335
+ const filePart = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
336
+ const countPart = separatorIndex === -1 ? "" : line.slice(separatorIndex + 1);
337
+ const count = Number.parseInt(countPart, 10);
338
+ recordSimpleFile(filePart);
339
+ if (!Number.isNaN(count)) {
340
+ simpleMatchCount += count;
341
+ recordSimpleFileMatch(filePart, count);
342
+ }
343
+ }
344
+ fileCount = simpleFiles.size;
345
+ }
346
+
347
+ const truncatedByHeadLimit = hasHeadLimit && processedLines.length < lines.length;
348
+
349
+ // For count mode, format as "path:count"
350
+ if (effectiveOutputMode === "count") {
351
+ const formatted = processedLines.map((line) => {
352
+ const separatorIndex = line.lastIndexOf(":");
353
+ const relative = formatPath(separatorIndex === -1 ? line : line.slice(0, separatorIndex));
354
+ const count = separatorIndex === -1 ? "0" : line.slice(separatorIndex + 1);
355
+ return `${relative}:${count}`;
356
+ });
357
+ const output = formatted.join("\n");
358
+ return {
359
+ content: [{ type: "text", text: output }],
360
+ details: {
361
+ scopePath,
362
+ matchCount: simpleMatchCount,
363
+ fileCount,
364
+ files: simpleFileList,
365
+ fileMatches: simpleFileList.map((path) => ({
366
+ path,
367
+ count: simpleFileMatchCounts.get(path) ?? 0,
368
+ })),
369
+ mode: effectiveOutputMode,
370
+ truncated: truncatedByHeadLimit,
371
+ headLimitReached: truncatedByHeadLimit ? headLimit : undefined,
372
+ },
373
+ };
374
+ }
375
+
376
+ // For files_with_matches, format paths
377
+ const formatted = processedLines.map((line) => formatPath(line));
378
+ const output = formatted.join("\n");
379
+ return {
380
+ content: [{ type: "text", text: output }],
381
+ details: {
382
+ scopePath,
383
+ matchCount: simpleMatchCount,
384
+ fileCount,
385
+ files: simpleFileList,
386
+ mode: effectiveOutputMode,
387
+ truncated: truncatedByHeadLimit,
388
+ headLimitReached: truncatedByHeadLimit ? headLimit : undefined,
389
+ },
390
+ };
391
+ }
392
+
393
+ // Content mode - existing JSON processing
394
+ const formatBlock = (filePath: string, lineNumber: number): string[] => {
395
+ const relativePath = formatPath(filePath);
396
+ const lines = getFileLines(filePath);
397
+ if (!lines.length) {
398
+ return [`${relativePath}:${lineNumber}: (unable to read file)`];
399
+ }
400
+
401
+ const block: string[] = [];
402
+ const start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;
403
+ const end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;
404
+
405
+ for (let current = start; current <= end; current++) {
406
+ const lineText = lines[current - 1] ?? "";
407
+ const sanitized = lineText.replace(/\r/g, "");
408
+ const isMatchLine = current === lineNumber;
409
+
410
+ const { text: truncatedText, wasTruncated } = truncateLine(sanitized);
411
+ if (wasTruncated) {
412
+ linesTruncated = true;
413
+ }
414
+
415
+ if (isMatchLine) {
416
+ block.push(`${relativePath}:${current}: ${truncatedText}`);
417
+ } else {
418
+ block.push(`${relativePath}-${current}- ${truncatedText}`);
419
+ }
420
+ }
421
+
422
+ return block;
423
+ };
424
+
425
+ const processLine = (line: string) => {
426
+ if (!line.trim() || matchCount >= effectiveLimit) {
427
+ return;
428
+ }
429
+
430
+ let event: { type: string; data?: { path?: { text?: string }; line_number?: number } };
431
+ try {
432
+ event = JSON.parse(line);
433
+ } catch {
434
+ return;
435
+ }
436
+
437
+ if (event.type === "match") {
438
+ matchCount++;
439
+ const filePath = event.data?.path?.text;
440
+ const lineNumber = event.data?.line_number;
441
+
442
+ if (filePath && typeof lineNumber === "number") {
443
+ recordFile(filePath);
444
+ recordFileMatch(filePath);
445
+ outputLines.push(...formatBlock(filePath, lineNumber));
446
+ }
447
+
448
+ if (matchCount >= effectiveLimit) {
449
+ matchLimitReached = true;
450
+ stopChild(true);
451
+ }
452
+ }
453
+ };
454
+
455
+ // Read streams using Bun's ReadableStream API
456
+ const stdoutReader = (child.stdout as ReadableStream<Uint8Array>).getReader();
457
+ const stderrReader = (child.stderr as ReadableStream<Uint8Array>).getReader();
458
+ const decoder = new TextDecoder();
459
+ let stdoutBuffer = "";
460
+
461
+ await Promise.all([
462
+ // Process stdout line by line
463
+ (async () => {
464
+ while (true) {
465
+ const { done, value } = await stdoutReader.read();
466
+ if (done) break;
467
+
468
+ stdoutBuffer += decoder.decode(value, { stream: true });
469
+ const lines = stdoutBuffer.split("\n");
470
+ // Keep the last incomplete line in the buffer
471
+ stdoutBuffer = lines.pop() ?? "";
472
+
473
+ for (const line of lines) {
474
+ processLine(line);
475
+ }
476
+ }
477
+ // Process any remaining content
478
+ if (stdoutBuffer.trim()) {
479
+ processLine(stdoutBuffer);
480
+ }
481
+ })(),
482
+ // Collect stderr
483
+ (async () => {
484
+ while (true) {
485
+ const { done, value } = await stderrReader.read();
486
+ if (done) break;
487
+ stderr += decoder.decode(value, { stream: true });
488
+ }
489
+ })(),
490
+ ]);
491
+
492
+ const exitCode = await child.exited;
493
+
494
+ // Cleanup
495
+ if (signal) {
496
+ signal.removeEventListener("abort", onAbort);
497
+ }
498
+
499
+ if (aborted) {
500
+ throw new Error("Operation aborted");
501
+ }
502
+
503
+ if (!killedDueToLimit && exitCode !== 0 && exitCode !== 1) {
504
+ const errorMsg = stderr.trim() || `ripgrep exited with code ${exitCode}`;
505
+ throw new Error(errorMsg);
506
+ }
507
+
508
+ if (matchCount === 0) {
509
+ return {
510
+ content: [{ type: "text", text: "No matches found" }],
511
+ details: {
512
+ scopePath,
513
+ matchCount: 0,
514
+ fileCount: 0,
515
+ files: [],
516
+ mode: effectiveOutputMode,
517
+ truncated: false,
518
+ },
519
+ };
520
+ }
521
+
522
+ // Apply offset and headLimit to output lines
523
+ let processedLines = outputLines;
524
+ if (effectiveOffset > 0) {
525
+ processedLines = processedLines.slice(effectiveOffset);
526
+ }
527
+ if (hasHeadLimit) {
528
+ processedLines = processedLines.slice(0, headLimit);
529
+ }
530
+
531
+ // Apply byte truncation (no line limit since we already have match limit)
532
+ const rawOutput = processedLines.join("\n");
533
+ const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
534
+
535
+ let output = truncation.content;
536
+ const truncatedByHeadLimit = hasHeadLimit && processedLines.length < outputLines.length;
537
+ const details: GrepToolDetails = {
538
+ scopePath,
539
+ matchCount,
540
+ fileCount: files.size,
541
+ files: fileList,
542
+ fileMatches: fileList.map((path) => ({
543
+ path,
544
+ count: fileMatchCounts.get(path) ?? 0,
545
+ })),
546
+ mode: effectiveOutputMode,
547
+ truncated: matchLimitReached || truncation.truncated || truncatedByHeadLimit,
548
+ headLimitReached: truncatedByHeadLimit ? headLimit : undefined,
549
+ };
550
+
551
+ // Build notices
552
+ const notices: string[] = [];
553
+
554
+ if (matchLimitReached) {
555
+ notices.push(
556
+ `${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,
557
+ );
558
+ details.matchLimitReached = effectiveLimit;
559
+ }
560
+
561
+ if (truncation.truncated) {
562
+ notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
563
+ details.truncation = truncation;
564
+ }
565
+
566
+ if (linesTruncated) {
567
+ notices.push(`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`);
568
+ details.linesTruncated = true;
569
+ }
570
+
571
+ if (notices.length > 0) {
572
+ output += `\n\n[${notices.join(". ")}]`;
573
+ }
574
+
575
+ return {
576
+ content: [{ type: "text", text: output }],
577
+ details: Object.keys(details).length > 0 ? details : undefined,
578
+ };
579
+ },
580
+ };
581
+ }
582
+
583
+ /** Default grep tool using process.cwd() - for backwards compatibility */
584
+ export const grepTool = createGrepTool(process.cwd());