@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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/budget-guard.d.ts +75 -0
- package/dist/budget-guard.d.ts.map +1 -0
- package/dist/budget-guard.js +142 -0
- package/dist/budget-guard.js.map +1 -0
- package/dist/compaction/compaction.d.ts +99 -0
- package/dist/compaction/compaction.d.ts.map +1 -0
- package/dist/compaction/compaction.js +302 -0
- package/dist/compaction/compaction.js.map +1 -0
- package/dist/compaction/failsafe.d.ts +57 -0
- package/dist/compaction/failsafe.d.ts.map +1 -0
- package/dist/compaction/failsafe.js +135 -0
- package/dist/compaction/failsafe.js.map +1 -0
- package/dist/compaction/index.d.ts +381 -0
- package/dist/compaction/index.d.ts.map +1 -0
- package/dist/compaction/index.js +979 -0
- package/dist/compaction/index.js.map +1 -0
- package/dist/compaction/microcompaction.d.ts +219 -0
- package/dist/compaction/microcompaction.d.ts.map +1 -0
- package/dist/compaction/microcompaction.js +536 -0
- package/dist/compaction/microcompaction.js.map +1 -0
- package/dist/compaction/observational/buffering.d.ts +225 -0
- package/dist/compaction/observational/buffering.d.ts.map +1 -0
- package/dist/compaction/observational/buffering.js +354 -0
- package/dist/compaction/observational/buffering.js.map +1 -0
- package/dist/compaction/observational/constants.d.ts +70 -0
- package/dist/compaction/observational/constants.d.ts.map +1 -0
- package/dist/compaction/observational/constants.js +507 -0
- package/dist/compaction/observational/constants.js.map +1 -0
- package/dist/compaction/observational/index.d.ts +219 -0
- package/dist/compaction/observational/index.d.ts.map +1 -0
- package/dist/compaction/observational/index.js +641 -0
- package/dist/compaction/observational/index.js.map +1 -0
- package/dist/compaction/observational/observer.d.ts +97 -0
- package/dist/compaction/observational/observer.d.ts.map +1 -0
- package/dist/compaction/observational/observer.js +424 -0
- package/dist/compaction/observational/observer.js.map +1 -0
- package/dist/compaction/observational/recall-tool.d.ts +27 -0
- package/dist/compaction/observational/recall-tool.d.ts.map +1 -0
- package/dist/compaction/observational/recall-tool.js +93 -0
- package/dist/compaction/observational/recall-tool.js.map +1 -0
- package/dist/compaction/observational/reflector.d.ts +94 -0
- package/dist/compaction/observational/reflector.d.ts.map +1 -0
- package/dist/compaction/observational/reflector.js +167 -0
- package/dist/compaction/observational/reflector.js.map +1 -0
- package/dist/compaction/observational/types.d.ts +271 -0
- package/dist/compaction/observational/types.d.ts.map +1 -0
- package/dist/compaction/observational/types.js +15 -0
- package/dist/compaction/observational/types.js.map +1 -0
- package/dist/context-manager.d.ts +134 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +170 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/cortex-agent.d.ts +1020 -0
- package/dist/cortex-agent.d.ts.map +1 -0
- package/dist/cortex-agent.js +3589 -0
- package/dist/cortex-agent.js.map +1 -0
- package/dist/error-classifier.d.ts +48 -0
- package/dist/error-classifier.d.ts.map +1 -0
- package/dist/error-classifier.js +152 -0
- package/dist/error-classifier.js.map +1 -0
- package/dist/event-bridge.d.ts +166 -0
- package/dist/event-bridge.d.ts.map +1 -0
- package/dist/event-bridge.js +381 -0
- package/dist/event-bridge.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts +119 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +474 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/model-wrapper.d.ts +58 -0
- package/dist/model-wrapper.d.ts.map +1 -0
- package/dist/model-wrapper.js +86 -0
- package/dist/model-wrapper.js.map +1 -0
- package/dist/noop-logger.d.ts +4 -0
- package/dist/noop-logger.d.ts.map +1 -0
- package/dist/noop-logger.js +8 -0
- package/dist/noop-logger.js.map +1 -0
- package/dist/prompt-diagnostics.d.ts +47 -0
- package/dist/prompt-diagnostics.d.ts.map +1 -0
- package/dist/prompt-diagnostics.js +230 -0
- package/dist/prompt-diagnostics.js.map +1 -0
- package/dist/provider-manager.d.ts +224 -0
- package/dist/provider-manager.d.ts.map +1 -0
- package/dist/provider-manager.js +563 -0
- package/dist/provider-manager.js.map +1 -0
- package/dist/provider-registry.d.ts +115 -0
- package/dist/provider-registry.d.ts.map +1 -0
- package/dist/provider-registry.js +305 -0
- package/dist/provider-registry.js.map +1 -0
- package/dist/schema-converter.d.ts +20 -0
- package/dist/schema-converter.d.ts.map +1 -0
- package/dist/schema-converter.js +48 -0
- package/dist/schema-converter.js.map +1 -0
- package/dist/skill-preprocessor.d.ts +46 -0
- package/dist/skill-preprocessor.d.ts.map +1 -0
- package/dist/skill-preprocessor.js +237 -0
- package/dist/skill-preprocessor.js.map +1 -0
- package/dist/skill-registry.d.ts +107 -0
- package/dist/skill-registry.d.ts.map +1 -0
- package/dist/skill-registry.js +330 -0
- package/dist/skill-registry.js.map +1 -0
- package/dist/skill-tool.d.ts +54 -0
- package/dist/skill-tool.d.ts.map +1 -0
- package/dist/skill-tool.js +88 -0
- package/dist/skill-tool.js.map +1 -0
- package/dist/sub-agent-manager.d.ts +90 -0
- package/dist/sub-agent-manager.d.ts.map +1 -0
- package/dist/sub-agent-manager.js +192 -0
- package/dist/sub-agent-manager.js.map +1 -0
- package/dist/token-estimator.d.ts +23 -0
- package/dist/token-estimator.d.ts.map +1 -0
- package/dist/token-estimator.js +27 -0
- package/dist/token-estimator.js.map +1 -0
- package/dist/tool-contract.d.ts +68 -0
- package/dist/tool-contract.d.ts.map +1 -0
- package/dist/tool-contract.js +35 -0
- package/dist/tool-contract.js.map +1 -0
- package/dist/tool-result-persistence.d.ts +89 -0
- package/dist/tool-result-persistence.d.ts.map +1 -0
- package/dist/tool-result-persistence.js +152 -0
- package/dist/tool-result-persistence.js.map +1 -0
- package/dist/tools/bash/index.d.ts +71 -0
- package/dist/tools/bash/index.d.ts.map +1 -0
- package/dist/tools/bash/index.js +485 -0
- package/dist/tools/bash/index.js.map +1 -0
- package/dist/tools/bash/interactive.d.ts +47 -0
- package/dist/tools/bash/interactive.d.ts.map +1 -0
- package/dist/tools/bash/interactive.js +262 -0
- package/dist/tools/bash/interactive.js.map +1 -0
- package/dist/tools/bash/safety.d.ts +149 -0
- package/dist/tools/bash/safety.d.ts.map +1 -0
- package/dist/tools/bash/safety.js +1116 -0
- package/dist/tools/bash/safety.js.map +1 -0
- package/dist/tools/edit.d.ts +57 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +310 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/glob.d.ts +34 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +268 -0
- package/dist/tools/glob.js.map +1 -0
- package/dist/tools/grep.d.ts +53 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/grep.js +673 -0
- package/dist/tools/grep.js.map +1 -0
- package/dist/tools/index.d.ts +62 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read.d.ts +43 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +459 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/runtime.d.ts +62 -0
- package/dist/tools/runtime.d.ts.map +1 -0
- package/dist/tools/runtime.js +116 -0
- package/dist/tools/runtime.js.map +1 -0
- package/dist/tools/shared/cwd-tracker.d.ts +32 -0
- package/dist/tools/shared/cwd-tracker.d.ts.map +1 -0
- package/dist/tools/shared/cwd-tracker.js +44 -0
- package/dist/tools/shared/cwd-tracker.js.map +1 -0
- package/dist/tools/shared/edit-history.d.ts +55 -0
- package/dist/tools/shared/edit-history.d.ts.map +1 -0
- package/dist/tools/shared/edit-history.js +72 -0
- package/dist/tools/shared/edit-history.js.map +1 -0
- package/dist/tools/shared/edit-matcher.d.ts +83 -0
- package/dist/tools/shared/edit-matcher.d.ts.map +1 -0
- package/dist/tools/shared/edit-matcher.js +359 -0
- package/dist/tools/shared/edit-matcher.js.map +1 -0
- package/dist/tools/shared/file-mutation-lock.d.ts +22 -0
- package/dist/tools/shared/file-mutation-lock.d.ts.map +1 -0
- package/dist/tools/shared/file-mutation-lock.js +35 -0
- package/dist/tools/shared/file-mutation-lock.js.map +1 -0
- package/dist/tools/shared/gitignore.d.ts +17 -0
- package/dist/tools/shared/gitignore.d.ts.map +1 -0
- package/dist/tools/shared/gitignore.js +59 -0
- package/dist/tools/shared/gitignore.js.map +1 -0
- package/dist/tools/shared/pdf-extractor.d.ts +96 -0
- package/dist/tools/shared/pdf-extractor.d.ts.map +1 -0
- package/dist/tools/shared/pdf-extractor.js +196 -0
- package/dist/tools/shared/pdf-extractor.js.map +1 -0
- package/dist/tools/shared/read-registry.d.ts +66 -0
- package/dist/tools/shared/read-registry.d.ts.map +1 -0
- package/dist/tools/shared/read-registry.js +65 -0
- package/dist/tools/shared/read-registry.js.map +1 -0
- package/dist/tools/shared/safe-env.d.ts +18 -0
- package/dist/tools/shared/safe-env.d.ts.map +1 -0
- package/dist/tools/shared/safe-env.js +70 -0
- package/dist/tools/shared/safe-env.js.map +1 -0
- package/dist/tools/sub-agent.d.ts +91 -0
- package/dist/tools/sub-agent.d.ts.map +1 -0
- package/dist/tools/sub-agent.js +89 -0
- package/dist/tools/sub-agent.js.map +1 -0
- package/dist/tools/task-output.d.ts +38 -0
- package/dist/tools/task-output.d.ts.map +1 -0
- package/dist/tools/task-output.js +186 -0
- package/dist/tools/task-output.js.map +1 -0
- package/dist/tools/tool-search/index.d.ts +40 -0
- package/dist/tools/tool-search/index.d.ts.map +1 -0
- package/dist/tools/tool-search/index.js +110 -0
- package/dist/tools/tool-search/index.js.map +1 -0
- package/dist/tools/tool-search/registry.d.ts +82 -0
- package/dist/tools/tool-search/registry.d.ts.map +1 -0
- package/dist/tools/tool-search/registry.js +238 -0
- package/dist/tools/tool-search/registry.js.map +1 -0
- package/dist/tools/undo-edit.d.ts +51 -0
- package/dist/tools/undo-edit.d.ts.map +1 -0
- package/dist/tools/undo-edit.js +231 -0
- package/dist/tools/undo-edit.js.map +1 -0
- package/dist/tools/web-fetch/cache.d.ts +49 -0
- package/dist/tools/web-fetch/cache.d.ts.map +1 -0
- package/dist/tools/web-fetch/cache.js +89 -0
- package/dist/tools/web-fetch/cache.js.map +1 -0
- package/dist/tools/web-fetch/index.d.ts +53 -0
- package/dist/tools/web-fetch/index.d.ts.map +1 -0
- package/dist/tools/web-fetch/index.js +513 -0
- package/dist/tools/web-fetch/index.js.map +1 -0
- package/dist/tools/write.d.ts +59 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +316 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/types.d.ts +881 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/working-tags.d.ts +44 -0
- package/dist/working-tags.d.ts.map +1 -0
- package/dist/working-tags.js +103 -0
- package/dist/working-tags.js.map +1 -0
- package/package.json +87 -0
- package/src/budget-guard.ts +170 -0
- package/src/compaction/compaction.ts +386 -0
- package/src/compaction/failsafe.ts +185 -0
- package/src/compaction/index.ts +1199 -0
- package/src/compaction/microcompaction.ts +709 -0
- package/src/compaction/observational/buffering.ts +430 -0
- package/src/compaction/observational/constants.ts +532 -0
- package/src/compaction/observational/index.ts +837 -0
- package/src/compaction/observational/observer.ts +510 -0
- package/src/compaction/observational/recall-tool.ts +130 -0
- package/src/compaction/observational/reflector.ts +221 -0
- package/src/compaction/observational/types.ts +343 -0
- package/src/context-manager.ts +237 -0
- package/src/cortex-agent.ts +4297 -0
- package/src/error-classifier.ts +199 -0
- package/src/event-bridge.ts +508 -0
- package/src/index.ts +292 -0
- package/src/mcp-client.ts +582 -0
- package/src/model-wrapper.ts +128 -0
- package/src/noop-logger.ts +9 -0
- package/src/prompt-diagnostics.ts +296 -0
- package/src/provider-manager.ts +823 -0
- package/src/provider-registry.ts +386 -0
- package/src/schema-converter.ts +51 -0
- package/src/skill-preprocessor.ts +314 -0
- package/src/skill-registry.ts +378 -0
- package/src/skill-tool.ts +130 -0
- package/src/sub-agent-manager.ts +236 -0
- package/src/token-estimator.ts +26 -0
- package/src/tool-contract.ts +113 -0
- package/src/tool-result-persistence.ts +197 -0
- package/src/tools/bash/index.ts +633 -0
- package/src/tools/bash/interactive.ts +302 -0
- package/src/tools/bash/safety.ts +1297 -0
- package/src/tools/edit.ts +422 -0
- package/src/tools/glob.ts +330 -0
- package/src/tools/grep.ts +819 -0
- package/src/tools/index.ts +110 -0
- package/src/tools/read.ts +580 -0
- package/src/tools/runtime.ts +173 -0
- package/src/tools/shared/cwd-tracker.ts +50 -0
- package/src/tools/shared/edit-history.ts +96 -0
- package/src/tools/shared/edit-matcher.ts +457 -0
- package/src/tools/shared/file-mutation-lock.ts +40 -0
- package/src/tools/shared/gitignore.ts +61 -0
- package/src/tools/shared/pdf-extractor.ts +290 -0
- package/src/tools/shared/read-registry.ts +93 -0
- package/src/tools/shared/safe-env.ts +82 -0
- package/src/tools/sub-agent.ts +171 -0
- package/src/tools/task-output.ts +236 -0
- package/src/tools/tool-search/index.ts +167 -0
- package/src/tools/tool-search/registry.ts +278 -0
- package/src/tools/undo-edit.ts +314 -0
- package/src/tools/web-fetch/cache.ts +112 -0
- package/src/tools/web-fetch/index.ts +604 -0
- package/src/tools/write.ts +385 -0
- package/src/types.ts +1057 -0
- 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
|
+
}
|