@animus-labs/cortex 0.2.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 (293) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/budget-guard.d.ts +75 -0
  4. package/dist/budget-guard.d.ts.map +1 -0
  5. package/dist/budget-guard.js +142 -0
  6. package/dist/budget-guard.js.map +1 -0
  7. package/dist/compaction/compaction.d.ts +99 -0
  8. package/dist/compaction/compaction.d.ts.map +1 -0
  9. package/dist/compaction/compaction.js +302 -0
  10. package/dist/compaction/compaction.js.map +1 -0
  11. package/dist/compaction/failsafe.d.ts +57 -0
  12. package/dist/compaction/failsafe.d.ts.map +1 -0
  13. package/dist/compaction/failsafe.js +135 -0
  14. package/dist/compaction/failsafe.js.map +1 -0
  15. package/dist/compaction/index.d.ts +381 -0
  16. package/dist/compaction/index.d.ts.map +1 -0
  17. package/dist/compaction/index.js +979 -0
  18. package/dist/compaction/index.js.map +1 -0
  19. package/dist/compaction/microcompaction.d.ts +219 -0
  20. package/dist/compaction/microcompaction.d.ts.map +1 -0
  21. package/dist/compaction/microcompaction.js +536 -0
  22. package/dist/compaction/microcompaction.js.map +1 -0
  23. package/dist/compaction/observational/buffering.d.ts +225 -0
  24. package/dist/compaction/observational/buffering.d.ts.map +1 -0
  25. package/dist/compaction/observational/buffering.js +354 -0
  26. package/dist/compaction/observational/buffering.js.map +1 -0
  27. package/dist/compaction/observational/constants.d.ts +70 -0
  28. package/dist/compaction/observational/constants.d.ts.map +1 -0
  29. package/dist/compaction/observational/constants.js +507 -0
  30. package/dist/compaction/observational/constants.js.map +1 -0
  31. package/dist/compaction/observational/index.d.ts +219 -0
  32. package/dist/compaction/observational/index.d.ts.map +1 -0
  33. package/dist/compaction/observational/index.js +641 -0
  34. package/dist/compaction/observational/index.js.map +1 -0
  35. package/dist/compaction/observational/observer.d.ts +97 -0
  36. package/dist/compaction/observational/observer.d.ts.map +1 -0
  37. package/dist/compaction/observational/observer.js +424 -0
  38. package/dist/compaction/observational/observer.js.map +1 -0
  39. package/dist/compaction/observational/recall-tool.d.ts +27 -0
  40. package/dist/compaction/observational/recall-tool.d.ts.map +1 -0
  41. package/dist/compaction/observational/recall-tool.js +93 -0
  42. package/dist/compaction/observational/recall-tool.js.map +1 -0
  43. package/dist/compaction/observational/reflector.d.ts +94 -0
  44. package/dist/compaction/observational/reflector.d.ts.map +1 -0
  45. package/dist/compaction/observational/reflector.js +167 -0
  46. package/dist/compaction/observational/reflector.js.map +1 -0
  47. package/dist/compaction/observational/types.d.ts +271 -0
  48. package/dist/compaction/observational/types.d.ts.map +1 -0
  49. package/dist/compaction/observational/types.js +15 -0
  50. package/dist/compaction/observational/types.js.map +1 -0
  51. package/dist/context-manager.d.ts +134 -0
  52. package/dist/context-manager.d.ts.map +1 -0
  53. package/dist/context-manager.js +170 -0
  54. package/dist/context-manager.js.map +1 -0
  55. package/dist/cortex-agent.d.ts +1020 -0
  56. package/dist/cortex-agent.d.ts.map +1 -0
  57. package/dist/cortex-agent.js +3589 -0
  58. package/dist/cortex-agent.js.map +1 -0
  59. package/dist/error-classifier.d.ts +48 -0
  60. package/dist/error-classifier.d.ts.map +1 -0
  61. package/dist/error-classifier.js +152 -0
  62. package/dist/error-classifier.js.map +1 -0
  63. package/dist/event-bridge.d.ts +166 -0
  64. package/dist/event-bridge.d.ts.map +1 -0
  65. package/dist/event-bridge.js +381 -0
  66. package/dist/event-bridge.js.map +1 -0
  67. package/dist/index.d.ts +55 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +57 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/mcp-client.d.ts +119 -0
  72. package/dist/mcp-client.d.ts.map +1 -0
  73. package/dist/mcp-client.js +474 -0
  74. package/dist/mcp-client.js.map +1 -0
  75. package/dist/model-wrapper.d.ts +58 -0
  76. package/dist/model-wrapper.d.ts.map +1 -0
  77. package/dist/model-wrapper.js +86 -0
  78. package/dist/model-wrapper.js.map +1 -0
  79. package/dist/noop-logger.d.ts +4 -0
  80. package/dist/noop-logger.d.ts.map +1 -0
  81. package/dist/noop-logger.js +8 -0
  82. package/dist/noop-logger.js.map +1 -0
  83. package/dist/prompt-diagnostics.d.ts +47 -0
  84. package/dist/prompt-diagnostics.d.ts.map +1 -0
  85. package/dist/prompt-diagnostics.js +230 -0
  86. package/dist/prompt-diagnostics.js.map +1 -0
  87. package/dist/provider-manager.d.ts +224 -0
  88. package/dist/provider-manager.d.ts.map +1 -0
  89. package/dist/provider-manager.js +563 -0
  90. package/dist/provider-manager.js.map +1 -0
  91. package/dist/provider-registry.d.ts +115 -0
  92. package/dist/provider-registry.d.ts.map +1 -0
  93. package/dist/provider-registry.js +305 -0
  94. package/dist/provider-registry.js.map +1 -0
  95. package/dist/schema-converter.d.ts +20 -0
  96. package/dist/schema-converter.d.ts.map +1 -0
  97. package/dist/schema-converter.js +48 -0
  98. package/dist/schema-converter.js.map +1 -0
  99. package/dist/skill-preprocessor.d.ts +46 -0
  100. package/dist/skill-preprocessor.d.ts.map +1 -0
  101. package/dist/skill-preprocessor.js +237 -0
  102. package/dist/skill-preprocessor.js.map +1 -0
  103. package/dist/skill-registry.d.ts +107 -0
  104. package/dist/skill-registry.d.ts.map +1 -0
  105. package/dist/skill-registry.js +330 -0
  106. package/dist/skill-registry.js.map +1 -0
  107. package/dist/skill-tool.d.ts +54 -0
  108. package/dist/skill-tool.d.ts.map +1 -0
  109. package/dist/skill-tool.js +88 -0
  110. package/dist/skill-tool.js.map +1 -0
  111. package/dist/sub-agent-manager.d.ts +90 -0
  112. package/dist/sub-agent-manager.d.ts.map +1 -0
  113. package/dist/sub-agent-manager.js +192 -0
  114. package/dist/sub-agent-manager.js.map +1 -0
  115. package/dist/token-estimator.d.ts +23 -0
  116. package/dist/token-estimator.d.ts.map +1 -0
  117. package/dist/token-estimator.js +27 -0
  118. package/dist/token-estimator.js.map +1 -0
  119. package/dist/tool-contract.d.ts +68 -0
  120. package/dist/tool-contract.d.ts.map +1 -0
  121. package/dist/tool-contract.js +35 -0
  122. package/dist/tool-contract.js.map +1 -0
  123. package/dist/tool-result-persistence.d.ts +89 -0
  124. package/dist/tool-result-persistence.d.ts.map +1 -0
  125. package/dist/tool-result-persistence.js +152 -0
  126. package/dist/tool-result-persistence.js.map +1 -0
  127. package/dist/tools/bash/index.d.ts +71 -0
  128. package/dist/tools/bash/index.d.ts.map +1 -0
  129. package/dist/tools/bash/index.js +485 -0
  130. package/dist/tools/bash/index.js.map +1 -0
  131. package/dist/tools/bash/interactive.d.ts +47 -0
  132. package/dist/tools/bash/interactive.d.ts.map +1 -0
  133. package/dist/tools/bash/interactive.js +262 -0
  134. package/dist/tools/bash/interactive.js.map +1 -0
  135. package/dist/tools/bash/safety.d.ts +149 -0
  136. package/dist/tools/bash/safety.d.ts.map +1 -0
  137. package/dist/tools/bash/safety.js +1116 -0
  138. package/dist/tools/bash/safety.js.map +1 -0
  139. package/dist/tools/edit.d.ts +57 -0
  140. package/dist/tools/edit.d.ts.map +1 -0
  141. package/dist/tools/edit.js +310 -0
  142. package/dist/tools/edit.js.map +1 -0
  143. package/dist/tools/glob.d.ts +34 -0
  144. package/dist/tools/glob.d.ts.map +1 -0
  145. package/dist/tools/glob.js +268 -0
  146. package/dist/tools/glob.js.map +1 -0
  147. package/dist/tools/grep.d.ts +53 -0
  148. package/dist/tools/grep.d.ts.map +1 -0
  149. package/dist/tools/grep.js +673 -0
  150. package/dist/tools/grep.js.map +1 -0
  151. package/dist/tools/index.d.ts +62 -0
  152. package/dist/tools/index.d.ts.map +1 -0
  153. package/dist/tools/index.js +52 -0
  154. package/dist/tools/index.js.map +1 -0
  155. package/dist/tools/read.d.ts +43 -0
  156. package/dist/tools/read.d.ts.map +1 -0
  157. package/dist/tools/read.js +459 -0
  158. package/dist/tools/read.js.map +1 -0
  159. package/dist/tools/runtime.d.ts +62 -0
  160. package/dist/tools/runtime.d.ts.map +1 -0
  161. package/dist/tools/runtime.js +116 -0
  162. package/dist/tools/runtime.js.map +1 -0
  163. package/dist/tools/shared/cwd-tracker.d.ts +32 -0
  164. package/dist/tools/shared/cwd-tracker.d.ts.map +1 -0
  165. package/dist/tools/shared/cwd-tracker.js +44 -0
  166. package/dist/tools/shared/cwd-tracker.js.map +1 -0
  167. package/dist/tools/shared/edit-history.d.ts +55 -0
  168. package/dist/tools/shared/edit-history.d.ts.map +1 -0
  169. package/dist/tools/shared/edit-history.js +72 -0
  170. package/dist/tools/shared/edit-history.js.map +1 -0
  171. package/dist/tools/shared/edit-matcher.d.ts +83 -0
  172. package/dist/tools/shared/edit-matcher.d.ts.map +1 -0
  173. package/dist/tools/shared/edit-matcher.js +359 -0
  174. package/dist/tools/shared/edit-matcher.js.map +1 -0
  175. package/dist/tools/shared/file-mutation-lock.d.ts +22 -0
  176. package/dist/tools/shared/file-mutation-lock.d.ts.map +1 -0
  177. package/dist/tools/shared/file-mutation-lock.js +35 -0
  178. package/dist/tools/shared/file-mutation-lock.js.map +1 -0
  179. package/dist/tools/shared/gitignore.d.ts +17 -0
  180. package/dist/tools/shared/gitignore.d.ts.map +1 -0
  181. package/dist/tools/shared/gitignore.js +59 -0
  182. package/dist/tools/shared/gitignore.js.map +1 -0
  183. package/dist/tools/shared/pdf-extractor.d.ts +96 -0
  184. package/dist/tools/shared/pdf-extractor.d.ts.map +1 -0
  185. package/dist/tools/shared/pdf-extractor.js +196 -0
  186. package/dist/tools/shared/pdf-extractor.js.map +1 -0
  187. package/dist/tools/shared/read-registry.d.ts +66 -0
  188. package/dist/tools/shared/read-registry.d.ts.map +1 -0
  189. package/dist/tools/shared/read-registry.js +65 -0
  190. package/dist/tools/shared/read-registry.js.map +1 -0
  191. package/dist/tools/shared/safe-env.d.ts +18 -0
  192. package/dist/tools/shared/safe-env.d.ts.map +1 -0
  193. package/dist/tools/shared/safe-env.js +70 -0
  194. package/dist/tools/shared/safe-env.js.map +1 -0
  195. package/dist/tools/sub-agent.d.ts +91 -0
  196. package/dist/tools/sub-agent.d.ts.map +1 -0
  197. package/dist/tools/sub-agent.js +89 -0
  198. package/dist/tools/sub-agent.js.map +1 -0
  199. package/dist/tools/task-output.d.ts +38 -0
  200. package/dist/tools/task-output.d.ts.map +1 -0
  201. package/dist/tools/task-output.js +186 -0
  202. package/dist/tools/task-output.js.map +1 -0
  203. package/dist/tools/tool-search/index.d.ts +40 -0
  204. package/dist/tools/tool-search/index.d.ts.map +1 -0
  205. package/dist/tools/tool-search/index.js +110 -0
  206. package/dist/tools/tool-search/index.js.map +1 -0
  207. package/dist/tools/tool-search/registry.d.ts +82 -0
  208. package/dist/tools/tool-search/registry.d.ts.map +1 -0
  209. package/dist/tools/tool-search/registry.js +238 -0
  210. package/dist/tools/tool-search/registry.js.map +1 -0
  211. package/dist/tools/undo-edit.d.ts +51 -0
  212. package/dist/tools/undo-edit.d.ts.map +1 -0
  213. package/dist/tools/undo-edit.js +231 -0
  214. package/dist/tools/undo-edit.js.map +1 -0
  215. package/dist/tools/web-fetch/cache.d.ts +49 -0
  216. package/dist/tools/web-fetch/cache.d.ts.map +1 -0
  217. package/dist/tools/web-fetch/cache.js +89 -0
  218. package/dist/tools/web-fetch/cache.js.map +1 -0
  219. package/dist/tools/web-fetch/index.d.ts +53 -0
  220. package/dist/tools/web-fetch/index.d.ts.map +1 -0
  221. package/dist/tools/web-fetch/index.js +513 -0
  222. package/dist/tools/web-fetch/index.js.map +1 -0
  223. package/dist/tools/write.d.ts +59 -0
  224. package/dist/tools/write.d.ts.map +1 -0
  225. package/dist/tools/write.js +316 -0
  226. package/dist/tools/write.js.map +1 -0
  227. package/dist/types.d.ts +881 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +16 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/working-tags.d.ts +44 -0
  232. package/dist/working-tags.d.ts.map +1 -0
  233. package/dist/working-tags.js +103 -0
  234. package/dist/working-tags.js.map +1 -0
  235. package/package.json +87 -0
  236. package/src/budget-guard.ts +170 -0
  237. package/src/compaction/compaction.ts +386 -0
  238. package/src/compaction/failsafe.ts +185 -0
  239. package/src/compaction/index.ts +1199 -0
  240. package/src/compaction/microcompaction.ts +709 -0
  241. package/src/compaction/observational/buffering.ts +430 -0
  242. package/src/compaction/observational/constants.ts +532 -0
  243. package/src/compaction/observational/index.ts +837 -0
  244. package/src/compaction/observational/observer.ts +510 -0
  245. package/src/compaction/observational/recall-tool.ts +130 -0
  246. package/src/compaction/observational/reflector.ts +221 -0
  247. package/src/compaction/observational/types.ts +343 -0
  248. package/src/context-manager.ts +237 -0
  249. package/src/cortex-agent.ts +4297 -0
  250. package/src/error-classifier.ts +199 -0
  251. package/src/event-bridge.ts +508 -0
  252. package/src/index.ts +292 -0
  253. package/src/mcp-client.ts +582 -0
  254. package/src/model-wrapper.ts +128 -0
  255. package/src/noop-logger.ts +9 -0
  256. package/src/prompt-diagnostics.ts +296 -0
  257. package/src/provider-manager.ts +823 -0
  258. package/src/provider-registry.ts +386 -0
  259. package/src/schema-converter.ts +51 -0
  260. package/src/skill-preprocessor.ts +314 -0
  261. package/src/skill-registry.ts +378 -0
  262. package/src/skill-tool.ts +130 -0
  263. package/src/sub-agent-manager.ts +236 -0
  264. package/src/token-estimator.ts +26 -0
  265. package/src/tool-contract.ts +113 -0
  266. package/src/tool-result-persistence.ts +197 -0
  267. package/src/tools/bash/index.ts +633 -0
  268. package/src/tools/bash/interactive.ts +302 -0
  269. package/src/tools/bash/safety.ts +1297 -0
  270. package/src/tools/edit.ts +422 -0
  271. package/src/tools/glob.ts +330 -0
  272. package/src/tools/grep.ts +819 -0
  273. package/src/tools/index.ts +110 -0
  274. package/src/tools/read.ts +580 -0
  275. package/src/tools/runtime.ts +173 -0
  276. package/src/tools/shared/cwd-tracker.ts +50 -0
  277. package/src/tools/shared/edit-history.ts +96 -0
  278. package/src/tools/shared/edit-matcher.ts +457 -0
  279. package/src/tools/shared/file-mutation-lock.ts +40 -0
  280. package/src/tools/shared/gitignore.ts +61 -0
  281. package/src/tools/shared/pdf-extractor.ts +290 -0
  282. package/src/tools/shared/read-registry.ts +93 -0
  283. package/src/tools/shared/safe-env.ts +82 -0
  284. package/src/tools/sub-agent.ts +171 -0
  285. package/src/tools/task-output.ts +236 -0
  286. package/src/tools/tool-search/index.ts +167 -0
  287. package/src/tools/tool-search/registry.ts +278 -0
  288. package/src/tools/undo-edit.ts +314 -0
  289. package/src/tools/web-fetch/cache.ts +112 -0
  290. package/src/tools/web-fetch/index.ts +604 -0
  291. package/src/tools/write.ts +385 -0
  292. package/src/types.ts +1057 -0
  293. package/src/working-tags.ts +118 -0
@@ -0,0 +1,819 @@
1
+ /**
2
+ * Grep tool: search file contents using regex.
3
+ *
4
+ * Uses the bundled ripgrep (rg) binary from @vscode/ripgrep as the
5
+ * primary search engine. Falls back to a pure Node.js regex search
6
+ * if the rg binary is unavailable (e.g., postinstall failed).
7
+ *
8
+ * Three output modes: files_with_matches, content, count.
9
+ * Pagination via offset + head_limit.
10
+ *
11
+ * Reference: docs/cortex/tools/grep.md
12
+ */
13
+
14
+ import * as child_process from 'node:child_process';
15
+ import * as fs from 'node:fs';
16
+ import { createRequire } from 'node:module';
17
+ import * as path from 'node:path';
18
+ import { Type, type Static } from 'typebox';
19
+ import type { ToolContentDetails } from '../types.js';
20
+ import {
21
+ readGitignorePatterns,
22
+ DEFAULT_IGNORE_PATTERNS,
23
+ } from './shared/gitignore.js';
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Schema
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export const GrepParams = Type.Object({
30
+ pattern: Type.String({ description: 'Regex pattern to search for' }),
31
+ path: Type.Optional(
32
+ Type.String({ description: 'File or directory to search in. Default: current working directory.' }),
33
+ ),
34
+ glob: Type.Optional(
35
+ Type.String({ description: 'Glob pattern to filter files (e.g., "*.ts", "**/*.{js,jsx}")' }),
36
+ ),
37
+ type: Type.Optional(
38
+ Type.String({ description: 'File type filter (e.g., "js", "py", "rust")' }),
39
+ ),
40
+ output_mode: Type.Optional(
41
+ Type.Union([
42
+ Type.Literal('files_with_matches'),
43
+ Type.Literal('content'),
44
+ Type.Literal('count'),
45
+ ], { description: 'Output mode. Default: files_with_matches.' }),
46
+ ),
47
+ context: Type.Optional(
48
+ Type.Number({ description: 'Lines of context before and after each match. Only in content mode.' }),
49
+ ),
50
+ '-i': Type.Optional(
51
+ Type.Boolean({ description: 'Case insensitive search. Default: false.' }),
52
+ ),
53
+ head_limit: Type.Optional(
54
+ Type.Number({ description: 'Limit number of results. Default: 250. Pass 0 for maximum (1000).' }),
55
+ ),
56
+ offset: Type.Optional(
57
+ Type.Number({ description: 'Skip first N results. Default: 0.' }),
58
+ ),
59
+ multiline: Type.Optional(
60
+ Type.Boolean({ description: 'Enable multiline mode where . matches newlines. Default: false.' }),
61
+ ),
62
+ });
63
+
64
+ export type GrepParamsType = Static<typeof GrepParams>;
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Details type
68
+ // ---------------------------------------------------------------------------
69
+
70
+ export interface GrepDetails {
71
+ totalFiles: number;
72
+ totalMatches: number;
73
+ durationMs: number;
74
+ /** True when results were capped by head_limit. Output size limiting is handled by the agent's result-persistence interceptor. */
75
+ truncated: boolean;
76
+ usingFallback: boolean;
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Constants
81
+ // ---------------------------------------------------------------------------
82
+
83
+ const DEFAULT_IGNORE = new Set(DEFAULT_IGNORE_PATTERNS);
84
+ const DEFAULT_HEAD_LIMIT = 250;
85
+
86
+ /** Ceiling for head_limit=0 ("unlimited"). */
87
+ const MAX_HEAD_LIMIT = 1000;
88
+
89
+ /** VCS directories to exclude from ripgrep searches. */
90
+ const VCS_DIRECTORIES = ['.git', '.svn', '.hg', '.bzr', '.jj', '.sl'];
91
+
92
+ /** File type to extension mapping (mimics ripgrep --type). */
93
+ const TYPE_EXTENSIONS: Record<string, string[]> = {
94
+ js: ['.js', '.jsx', '.mjs', '.cjs'],
95
+ ts: ['.ts', '.tsx', '.mts', '.cts'],
96
+ py: ['.py', '.pyi'],
97
+ rust: ['.rs'],
98
+ go: ['.go'],
99
+ java: ['.java'],
100
+ c: ['.c', '.h'],
101
+ cpp: ['.cpp', '.cc', '.cxx', '.hpp', '.hh', '.hxx', '.h'],
102
+ css: ['.css', '.scss', '.sass', '.less'],
103
+ html: ['.html', '.htm'],
104
+ json: ['.json'],
105
+ yaml: ['.yml', '.yaml'],
106
+ md: ['.md', '.markdown'],
107
+ xml: ['.xml'],
108
+ sql: ['.sql'],
109
+ sh: ['.sh', '.bash', '.zsh'],
110
+ ruby: ['.rb'],
111
+ php: ['.php'],
112
+ swift: ['.swift'],
113
+ kotlin: ['.kt', '.kts'],
114
+ toml: ['.toml'],
115
+ };
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Config
119
+ // ---------------------------------------------------------------------------
120
+
121
+ export interface GrepToolConfig {
122
+ /** Default search directory when no path param is given. */
123
+ defaultCwd: string;
124
+ /** Whether to respect .gitignore. Default: true. */
125
+ respectGitignore?: boolean | undefined;
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Ripgrep binary resolution
130
+ // ---------------------------------------------------------------------------
131
+
132
+ let resolvedRgPath: string | false | undefined;
133
+ const require = createRequire(import.meta.url);
134
+
135
+ /**
136
+ * Get the path to the bundled ripgrep binary from @vscode/ripgrep.
137
+ * Caches the result for the process lifetime.
138
+ * Returns the path to rg, or false if unavailable.
139
+ */
140
+ function getRipgrepPath(): string | false {
141
+ if (resolvedRgPath !== undefined) return resolvedRgPath;
142
+
143
+ try {
144
+ // @vscode/ripgrep exports { rgPath } pointing to the downloaded binary
145
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
146
+ const { rgPath } = require('@vscode/ripgrep') as { rgPath: string };
147
+ fs.accessSync(rgPath, fs.constants.X_OK);
148
+ resolvedRgPath = rgPath;
149
+ return rgPath;
150
+ } catch {
151
+ // Package not installed or binary not downloaded (postinstall failed)
152
+ resolvedRgPath = false;
153
+ return false;
154
+ }
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Ripgrep execution
159
+ // ---------------------------------------------------------------------------
160
+
161
+ /**
162
+ * Execute ripgrep and return the output lines.
163
+ */
164
+ function execRipgrep(
165
+ args: string[],
166
+ cwd: string,
167
+ ): Promise<string[]> {
168
+ const rgPath = getRipgrepPath();
169
+ if (!rgPath) return Promise.reject(new Error('rg binary not available'));
170
+
171
+ return new Promise((resolve, reject) => {
172
+ child_process.execFile(
173
+ rgPath,
174
+ args,
175
+ {
176
+ cwd,
177
+ maxBuffer: 10 * 1024 * 1024, // 10 MB
178
+ timeout: 30_000,
179
+ encoding: 'utf8',
180
+ },
181
+ (error, stdout) => {
182
+ if (error) {
183
+ // rg exits with code 1 when no matches found (not an error)
184
+ const exitCode = (error as { code?: number | string }).code;
185
+ if (exitCode === 1) {
186
+ resolve([]);
187
+ return;
188
+ }
189
+ reject(error);
190
+ return;
191
+ }
192
+ const lines = stdout ? stdout.split('\n').filter(Boolean) : [];
193
+ resolve(lines);
194
+ },
195
+ );
196
+ });
197
+ }
198
+
199
+ /**
200
+ * Convert an absolute path to relative (from cwd) to save tokens.
201
+ */
202
+ function toRelativePath(absPath: string, cwd: string): string {
203
+ if (!absPath.startsWith('/')) return absPath; // already relative
204
+ const rel = path.relative(cwd, absPath);
205
+ // Only use relative if it's shorter and doesn't escape too far
206
+ if (rel.startsWith('../../..')) return absPath;
207
+ return rel.length < absPath.length ? rel : absPath;
208
+ }
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // Pagination helper
212
+ // ---------------------------------------------------------------------------
213
+
214
+ function applyHeadLimit<T>(
215
+ items: T[],
216
+ limit: number | undefined,
217
+ offset: number = 0,
218
+ ): { items: T[]; truncated: boolean } {
219
+ // Explicit 0 = use maximum ceiling (not truly unlimited)
220
+ const effectiveLimit = limit === 0
221
+ ? MAX_HEAD_LIMIT
222
+ : (limit ?? DEFAULT_HEAD_LIMIT);
223
+ const afterOffset = items.slice(offset);
224
+ const truncated = afterOffset.length > effectiveLimit;
225
+ return {
226
+ items: afterOffset.slice(0, effectiveLimit),
227
+ truncated,
228
+ };
229
+ }
230
+
231
+ // ---------------------------------------------------------------------------
232
+ // Ripgrep-based search
233
+ // ---------------------------------------------------------------------------
234
+
235
+ async function searchWithRipgrep(
236
+ params: GrepParamsType,
237
+ searchPath: string,
238
+ config: GrepToolConfig,
239
+ respectGitignore: boolean,
240
+ ): Promise<ToolContentDetails<GrepDetails>> {
241
+ const startTime = Date.now();
242
+ const outputMode = params.output_mode ?? 'files_with_matches';
243
+ const caseInsensitive = params['-i'] ?? false;
244
+ const multiline = params.multiline ?? false;
245
+ const contextLines = params.context ?? 0;
246
+ const cwd = config.defaultCwd;
247
+
248
+ const args: string[] = ['--hidden', '--no-require-git'];
249
+
250
+ // When gitignore respect is disabled, tell rg to skip all ignore files
251
+ if (!respectGitignore) {
252
+ args.push('--no-ignore');
253
+ }
254
+
255
+ // Exclude VCS directories
256
+ for (const dir of VCS_DIRECTORIES) {
257
+ args.push('--glob', `!${dir}`);
258
+ }
259
+
260
+ // Apply default ignore patterns (node_modules, dist, __pycache__, etc.)
261
+ for (const pattern of DEFAULT_IGNORE_PATTERNS) {
262
+ args.push('--glob', `!${pattern}`);
263
+ }
264
+
265
+ // Limit line length to prevent base64/minified content from cluttering output
266
+ args.push('--max-columns', '500');
267
+
268
+ if (multiline) {
269
+ args.push('-U', '--multiline-dotall');
270
+ }
271
+
272
+ if (caseInsensitive) {
273
+ args.push('-i');
274
+ }
275
+
276
+ // Output mode flags
277
+ if (outputMode === 'files_with_matches') {
278
+ args.push('-l');
279
+ } else if (outputMode === 'count') {
280
+ args.push('-c');
281
+ }
282
+
283
+ // Line numbers for content mode
284
+ if (outputMode === 'content') {
285
+ args.push('-n');
286
+ if (contextLines > 0) {
287
+ args.push('-C', String(contextLines));
288
+ }
289
+ }
290
+
291
+ // Pattern (use -e for dash-prefixed patterns)
292
+ if (params.pattern.startsWith('-')) {
293
+ args.push('-e', params.pattern);
294
+ } else {
295
+ args.push(params.pattern);
296
+ }
297
+
298
+ // Type filter
299
+ if (params.type) {
300
+ args.push('--type', params.type);
301
+ }
302
+
303
+ // Glob filter
304
+ if (params.glob) {
305
+ const rawPatterns = params.glob.split(/\s+/);
306
+ for (const rawPattern of rawPatterns) {
307
+ if (rawPattern.includes('{') && rawPattern.includes('}')) {
308
+ args.push('--glob', rawPattern);
309
+ } else {
310
+ for (const p of rawPattern.split(',').filter(Boolean)) {
311
+ args.push('--glob', p);
312
+ }
313
+ }
314
+ }
315
+ }
316
+
317
+ // Always pass search path explicitly so rg returns absolute-resolvable paths
318
+ args.push(searchPath);
319
+
320
+ // Use the parent dir of searchPath as cwd; rg will resolve searchPath argument
321
+ const rgCwd = path.dirname(searchPath);
322
+
323
+ if (outputMode === 'content') {
324
+ const rawLines = await execRipgrep(args, rgCwd);
325
+ const durationMs = Date.now() - startTime;
326
+
327
+ const { items: limited, truncated } = applyHeadLimit(
328
+ rawLines,
329
+ params.head_limit,
330
+ params.offset ?? 0,
331
+ );
332
+
333
+ // Convert absolute paths in content lines to relative
334
+ const finalLines = limited.map(line => {
335
+ const colonIdx = line.indexOf(':');
336
+ if (colonIdx > 0) {
337
+ const filePart = line.substring(0, colonIdx);
338
+ if (filePart.startsWith('/')) {
339
+ return toRelativePath(filePart, cwd) + line.substring(colonIdx);
340
+ }
341
+ }
342
+ return line;
343
+ });
344
+
345
+ const text = finalLines.length > 0 ? finalLines.join('\n') : 'No matches found.';
346
+
347
+ return {
348
+ content: [{ type: 'text', text }],
349
+ details: {
350
+ totalFiles: 0,
351
+ totalMatches: finalLines.length,
352
+ durationMs,
353
+ truncated,
354
+ usingFallback: false,
355
+ },
356
+ };
357
+ }
358
+
359
+ if (outputMode === 'count') {
360
+ const rawLines = await execRipgrep(args, rgCwd);
361
+ const durationMs = Date.now() - startTime;
362
+
363
+ const { items: limited, truncated } = applyHeadLimit(
364
+ rawLines,
365
+ params.head_limit,
366
+ params.offset ?? 0,
367
+ );
368
+
369
+ let totalMatches = 0;
370
+ const finalLines = limited.map(line => {
371
+ const colonIdx = line.lastIndexOf(':');
372
+ if (colonIdx > 0) {
373
+ const filePart = line.substring(0, colonIdx);
374
+ const countStr = line.substring(colonIdx + 1);
375
+ const count = parseInt(countStr, 10);
376
+ if (!isNaN(count)) totalMatches += count;
377
+ return toRelativePath(filePart, cwd) + ':' + countStr;
378
+ }
379
+ return line;
380
+ });
381
+
382
+ const text = finalLines.length > 0 ? finalLines.join('\n') : 'No matches found.';
383
+
384
+ return {
385
+ content: [{ type: 'text', text }],
386
+ details: {
387
+ totalFiles: finalLines.length,
388
+ totalMatches,
389
+ durationMs,
390
+ truncated,
391
+ usingFallback: false,
392
+ },
393
+ };
394
+ }
395
+
396
+ // files_with_matches: rg returns absolute paths, sort by mtime (newest first)
397
+ const results = await execRipgrep(args, rgCwd);
398
+ const durationMs = Date.now() - startTime;
399
+
400
+ const stats = await Promise.allSettled(
401
+ results.map(f => fs.promises.stat(f)),
402
+ );
403
+ const sorted = results
404
+ .map((f, i) => {
405
+ const r = stats[i]!;
406
+ return [f, r.status === 'fulfilled' ? (r.value.mtimeMs ?? 0) : 0] as const;
407
+ })
408
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
409
+ .map(([f]) => f);
410
+
411
+ const { items: limited, truncated } = applyHeadLimit(
412
+ sorted,
413
+ params.head_limit,
414
+ params.offset ?? 0,
415
+ );
416
+
417
+ const relativeMatches = limited.map(f => toRelativePath(f, cwd));
418
+ const text = relativeMatches.length > 0
419
+ ? relativeMatches.join('\n')
420
+ : 'No matches found.';
421
+
422
+ return {
423
+ content: [{ type: 'text', text }],
424
+ details: {
425
+ totalFiles: relativeMatches.length,
426
+ totalMatches: results.length,
427
+ durationMs,
428
+ truncated,
429
+ usingFallback: false,
430
+ },
431
+ };
432
+ }
433
+
434
+ // ---------------------------------------------------------------------------
435
+ // JS fallback helpers
436
+ // ---------------------------------------------------------------------------
437
+
438
+ function fileGlobToRegex(pattern: string): RegExp {
439
+ let regex = '';
440
+ for (let i = 0; i < pattern.length; i++) {
441
+ const char = pattern[i]!;
442
+ if (char === '*') {
443
+ if (pattern[i + 1] === '*') {
444
+ regex += '.*';
445
+ i++;
446
+ if (pattern[i + 1] === '/') i++;
447
+ } else {
448
+ regex += '[^/]*';
449
+ }
450
+ } else if (char === '?') {
451
+ regex += '[^/]';
452
+ } else if (char === '{') {
453
+ const closeIdx = pattern.indexOf('}', i);
454
+ if (closeIdx !== -1) {
455
+ const alternatives = pattern.slice(i + 1, closeIdx).split(',');
456
+ regex += '(?:' + alternatives.map((a) => a.replace(/[.*+?^$|[\]\\()]/g, '\\$&')).join('|') + ')';
457
+ i = closeIdx;
458
+ } else {
459
+ regex += '\\{';
460
+ }
461
+ } else if (char === '.') {
462
+ regex += '\\.';
463
+ } else {
464
+ regex += char;
465
+ }
466
+ }
467
+ return new RegExp(`^${regex}$`);
468
+ }
469
+
470
+ function matchesGitignorePattern(name: string, relativePath: string, patterns: string[]): boolean {
471
+ for (const pattern of patterns) {
472
+ const cleanPattern = pattern.endsWith('/') ? pattern.slice(0, -1) : pattern;
473
+ if (!cleanPattern.includes('/')) {
474
+ if (cleanPattern.includes('*') || cleanPattern.includes('?')) {
475
+ if (fileGlobToRegex(cleanPattern).test(name)) return true;
476
+ } else {
477
+ if (name === cleanPattern) return true;
478
+ }
479
+ } else {
480
+ if (fileGlobToRegex(cleanPattern).test(relativePath)) return true;
481
+ }
482
+ }
483
+ return false;
484
+ }
485
+
486
+ async function collectFiles(
487
+ dir: string,
488
+ fileFilter?: (relativePath: string, ext: string) => boolean,
489
+ gitignorePatterns?: string[],
490
+ baseDir?: string,
491
+ ): Promise<string[]> {
492
+ const results: string[] = [];
493
+ const root = baseDir ?? dir;
494
+
495
+ let entries: fs.Dirent[];
496
+ try {
497
+ entries = await fs.promises.readdir(dir, { withFileTypes: true });
498
+ } catch {
499
+ return results;
500
+ }
501
+
502
+ for (const entry of entries) {
503
+ if (DEFAULT_IGNORE.has(entry.name)) continue;
504
+
505
+ const fullPath = path.join(dir, entry.name);
506
+ const relativePath = path.relative(root, fullPath).split(path.sep).join('/');
507
+
508
+ if (gitignorePatterns && gitignorePatterns.length > 0) {
509
+ if (matchesGitignorePattern(entry.name, relativePath, gitignorePatterns)) continue;
510
+ }
511
+
512
+ if (entry.isDirectory()) {
513
+ const subResults = await collectFiles(fullPath, fileFilter, gitignorePatterns, root);
514
+ results.push(...subResults);
515
+ } else if (entry.isFile()) {
516
+ if (fileFilter) {
517
+ const ext = path.extname(entry.name).toLowerCase();
518
+ const relName = entry.name;
519
+ if (!fileFilter(relName, ext)) continue;
520
+ }
521
+ results.push(fullPath);
522
+ }
523
+ }
524
+
525
+ return results;
526
+ }
527
+
528
+ async function isBinaryFile(filePath: string): Promise<boolean> {
529
+ try {
530
+ const fd = await fs.promises.open(filePath, 'r');
531
+ try {
532
+ const buffer = Buffer.alloc(8192);
533
+ const { bytesRead } = await fd.read(buffer, 0, 8192, 0);
534
+ for (let i = 0; i < bytesRead; i++) {
535
+ if (buffer[i] === 0) return true;
536
+ }
537
+ return false;
538
+ } finally {
539
+ await fd.close();
540
+ }
541
+ } catch {
542
+ return false;
543
+ }
544
+ }
545
+
546
+ interface ContentMatch {
547
+ file: string;
548
+ lineNumber: number;
549
+ line: string;
550
+ }
551
+
552
+ interface FileCount {
553
+ file: string;
554
+ count: number;
555
+ }
556
+
557
+ // ---------------------------------------------------------------------------
558
+ // JS fallback search
559
+ // ---------------------------------------------------------------------------
560
+
561
+ async function searchWithFallback(
562
+ params: GrepParamsType,
563
+ searchPath: string,
564
+ config: GrepToolConfig,
565
+ respectGitignore: boolean,
566
+ ): Promise<ToolContentDetails<GrepDetails>> {
567
+ const outputMode = params.output_mode ?? 'files_with_matches';
568
+ const caseInsensitive = params['-i'] ?? false;
569
+ const headLimit = params.head_limit;
570
+ const offset = params.offset ?? 0;
571
+ const multiline = params.multiline ?? false;
572
+ const contextLines = params.context ?? 0;
573
+ const startTime = Date.now();
574
+ const cwd = config.defaultCwd;
575
+
576
+ // Build regex
577
+ let regex: RegExp;
578
+ try {
579
+ let flags = 'g';
580
+ if (caseInsensitive) flags += 'i';
581
+ if (multiline) flags += 'ms';
582
+ regex = new RegExp(params.pattern, flags);
583
+ } catch (err) {
584
+ const msg = err instanceof Error ? err.message : String(err);
585
+ return {
586
+ content: [{ type: 'text', text: `Invalid regex: ${params.pattern}. ${msg}` }],
587
+ details: {
588
+ totalFiles: 0,
589
+ totalMatches: 0,
590
+ durationMs: Date.now() - startTime,
591
+ truncated: false,
592
+ usingFallback: true,
593
+ },
594
+ };
595
+ }
596
+
597
+ // Build file filter
598
+ let fileFilter: ((relativePath: string, ext: string) => boolean) | undefined;
599
+
600
+ if (params.type) {
601
+ const typeExts = TYPE_EXTENSIONS[params.type];
602
+ if (typeExts) {
603
+ const extSet = new Set(typeExts);
604
+ fileFilter = (_rel: string, ext: string) => extSet.has(ext);
605
+ }
606
+ }
607
+
608
+ if (params.glob) {
609
+ const globRegex = fileGlobToRegex(params.glob);
610
+ const existingFilter = fileFilter;
611
+ fileFilter = (rel: string, ext: string) => {
612
+ if (existingFilter && !existingFilter(rel, ext)) return false;
613
+ return globRegex.test(rel);
614
+ };
615
+ }
616
+
617
+ // Collect files
618
+ let filesToSearch: string[];
619
+ try {
620
+ const stat = await fs.promises.stat(searchPath);
621
+ if (stat.isFile()) {
622
+ filesToSearch = [searchPath];
623
+ } else if (stat.isDirectory()) {
624
+ let gitignorePatterns: string[] | undefined;
625
+ if (respectGitignore) {
626
+ const patterns = await readGitignorePatterns(searchPath);
627
+ if (patterns.length > 0) {
628
+ gitignorePatterns = patterns;
629
+ }
630
+ }
631
+ filesToSearch = await collectFiles(searchPath, fileFilter, gitignorePatterns);
632
+ } else {
633
+ return {
634
+ content: [{ type: 'text', text: `Path does not exist: ${searchPath}` }],
635
+ details: { totalFiles: 0, totalMatches: 0, durationMs: Date.now() - startTime, truncated: false, usingFallback: true },
636
+ };
637
+ }
638
+ } catch {
639
+ return {
640
+ content: [{ type: 'text', text: `Path does not exist: ${searchPath}` }],
641
+ details: { totalFiles: 0, totalMatches: 0, durationMs: Date.now() - startTime, truncated: false, usingFallback: true },
642
+ };
643
+ }
644
+
645
+ // Search files
646
+ const matchingFiles: string[] = [];
647
+ const contentMatches: ContentMatch[] = [];
648
+ const fileCounts: FileCount[] = [];
649
+ let totalMatches = 0;
650
+
651
+ for (const file of filesToSearch) {
652
+ if (await isBinaryFile(file)) continue;
653
+
654
+ let content: string;
655
+ try {
656
+ content = await fs.promises.readFile(file, 'utf8');
657
+ } catch {
658
+ continue;
659
+ }
660
+
661
+ if (multiline) {
662
+ const matches = content.match(regex);
663
+ if (matches && matches.length > 0) {
664
+ totalMatches += matches.length;
665
+ matchingFiles.push(file);
666
+
667
+ if (outputMode === 'count') {
668
+ fileCounts.push({ file, count: matches.length });
669
+ } else if (outputMode === 'content') {
670
+ regex.lastIndex = 0;
671
+ let execMatch: RegExpExecArray | null;
672
+ while ((execMatch = regex.exec(content)) !== null) {
673
+ const matchIdx = execMatch.index;
674
+ const lineNum = content.slice(0, matchIdx).split('\n').length;
675
+ const matchText = execMatch[0];
676
+ contentMatches.push({
677
+ file,
678
+ lineNumber: lineNum,
679
+ line: matchText.length > 500 ? matchText.slice(0, 500) + '...' : matchText,
680
+ });
681
+ }
682
+ }
683
+ }
684
+ } else {
685
+ const lines = content.split('\n');
686
+ let fileMatchCount = 0;
687
+ let hasMatch = false;
688
+ const emittedLines = new Set<number>();
689
+
690
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
691
+ const line = lines[lineIdx]!;
692
+ regex.lastIndex = 0;
693
+ if (regex.test(line)) {
694
+ fileMatchCount++;
695
+ hasMatch = true;
696
+
697
+ if (outputMode === 'content') {
698
+ if (contextLines > 0) {
699
+ const startCtx = Math.max(0, lineIdx - contextLines);
700
+ const endCtx = Math.min(lines.length - 1, lineIdx + contextLines);
701
+ for (let ci = startCtx; ci <= endCtx; ci++) {
702
+ if (emittedLines.has(ci)) continue;
703
+ emittedLines.add(ci);
704
+ const prefix = ci === lineIdx ? ':' : '-';
705
+ contentMatches.push({
706
+ file,
707
+ lineNumber: ci + 1,
708
+ line: `${prefix}${lines[ci]}`,
709
+ });
710
+ }
711
+ } else {
712
+ contentMatches.push({
713
+ file,
714
+ lineNumber: lineIdx + 1,
715
+ line: lines[lineIdx]!,
716
+ });
717
+ }
718
+ }
719
+ }
720
+ }
721
+
722
+ if (hasMatch) {
723
+ totalMatches += fileMatchCount;
724
+ matchingFiles.push(file);
725
+ if (outputMode === 'count') {
726
+ fileCounts.push({ file, count: fileMatchCount });
727
+ }
728
+ }
729
+ }
730
+ }
731
+
732
+ const durationMs = Date.now() - startTime;
733
+
734
+ // Format output with relative paths and pagination
735
+ let text: string;
736
+ let truncated = false;
737
+
738
+ if (outputMode === 'files_with_matches') {
739
+ const result = applyHeadLimit(matchingFiles, headLimit, offset);
740
+ truncated = result.truncated;
741
+ const relPaths = result.items.map(f => toRelativePath(f, cwd));
742
+ text = relPaths.length > 0 ? relPaths.join('\n') : 'No matches found.';
743
+ } else if (outputMode === 'content') {
744
+ let lines: string[] = [];
745
+ let lastFile = '';
746
+
747
+ for (const match of contentMatches) {
748
+ if (match.file !== lastFile) {
749
+ if (lastFile) lines.push('');
750
+ lines.push(toRelativePath(match.file, cwd));
751
+ lastFile = match.file;
752
+ }
753
+ lines.push(`${match.lineNumber}:${match.line}`);
754
+ }
755
+
756
+ const result = applyHeadLimit(lines, headLimit, offset);
757
+ truncated = result.truncated;
758
+ text = result.items.length > 0 ? result.items.join('\n') : 'No matches found.';
759
+ } else {
760
+ const result = applyHeadLimit(fileCounts, headLimit, offset);
761
+ truncated = result.truncated;
762
+ text = result.items.length > 0
763
+ ? result.items.map((fc) => `${toRelativePath(fc.file, cwd)}:${fc.count}`).join('\n')
764
+ : 'No matches found.';
765
+ }
766
+
767
+ return {
768
+ content: [{ type: 'text', text }],
769
+ details: {
770
+ totalFiles: matchingFiles.length,
771
+ totalMatches,
772
+ durationMs,
773
+ truncated,
774
+ usingFallback: true,
775
+ },
776
+ };
777
+ }
778
+
779
+ // ---------------------------------------------------------------------------
780
+ // Tool factory
781
+ // ---------------------------------------------------------------------------
782
+
783
+ export function createGrepTool(config: GrepToolConfig): {
784
+ name: string;
785
+ description: string;
786
+ parameters: typeof GrepParams;
787
+ execute: (params: GrepParamsType) => Promise<ToolContentDetails<GrepDetails>>;
788
+ } {
789
+ const respectGitignore = config.respectGitignore ?? true;
790
+
791
+ return {
792
+ name: 'Grep',
793
+ description: 'Search file contents using regex patterns. Three output modes: files_with_matches (default), content (matching lines), count (match counts). Use glob, type, or a more specific pattern to narrow large result sets.',
794
+ parameters: GrepParams,
795
+
796
+ async execute(params: GrepParamsType): Promise<ToolContentDetails<GrepDetails>> {
797
+ const searchPath = params.path ? path.resolve(params.path) : path.resolve(config.defaultCwd);
798
+
799
+ // Use bundled ripgrep as primary engine, fall back to pure JS
800
+ if (getRipgrepPath()) {
801
+ try {
802
+ return await searchWithRipgrep(params, searchPath, config, respectGitignore);
803
+ } catch {
804
+ // rg failed (timeout, bad args, etc.), fall back to JS
805
+ }
806
+ }
807
+
808
+ return searchWithFallback(params, searchPath, config, respectGitignore);
809
+ },
810
+ };
811
+ }
812
+
813
+ /**
814
+ * Reset the cached ripgrep path. Used by tests to force re-detection.
815
+ * @internal
816
+ */
817
+ export function _resetRipgrepCache(): void {
818
+ resolvedRgPath = undefined;
819
+ }