sweet-search 0.0.1 → 2.3.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 +190 -0
- package/NOTICE +23 -0
- package/core/cli.js +51 -0
- package/core/config.js +27 -0
- package/core/embedding/embedding-cache.js +467 -0
- package/core/embedding/embedding-local-model.js +845 -0
- package/core/embedding/embedding-remote.js +492 -0
- package/core/embedding/embedding-service.js +712 -0
- package/core/embedding/embedding-telemetry.js +219 -0
- package/core/embedding/index.js +40 -0
- package/core/graph/community-detector.js +294 -0
- package/core/graph/graph-expansion.js +839 -0
- package/core/graph/graph-extractor.js +2304 -0
- package/core/graph/graph-search.js +2148 -0
- package/core/graph/hcgs-generator.js +666 -0
- package/core/graph/index.js +16 -0
- package/core/graph/leiden-algorithm.js +547 -0
- package/core/graph/relationship-resolver.js +366 -0
- package/core/graph/repo-map.js +408 -0
- package/core/graph/summary-manager.js +549 -0
- package/core/indexing/artifact-builder.js +1054 -0
- package/core/indexing/ast-chunker.js +709 -0
- package/core/indexing/chunking/chunk-builder.js +170 -0
- package/core/indexing/chunking/markdown-chunker.js +503 -0
- package/core/indexing/chunking/plaintext-chunker.js +104 -0
- package/core/indexing/dedup/dedup-phase.js +159 -0
- package/core/indexing/dedup/exemplar-selector.js +65 -0
- package/core/indexing/document-chunker.js +56 -0
- package/core/indexing/incremental-parser.js +390 -0
- package/core/indexing/incremental-tracker.js +761 -0
- package/core/indexing/index-codebase-v21.js +472 -0
- package/core/indexing/index-maintainer.mjs +1674 -0
- package/core/indexing/index.js +90 -0
- package/core/indexing/indexer-ann.js +1077 -0
- package/core/indexing/indexer-build.js +742 -0
- package/core/indexing/indexer-phases.js +800 -0
- package/core/indexing/indexer-pool.js +764 -0
- package/core/indexing/indexer-sparse-gram.js +98 -0
- package/core/indexing/indexer-utils.js +536 -0
- package/core/indexing/indexer-worker.js +148 -0
- package/core/indexing/li-skip-policy.js +225 -0
- package/core/indexing/merkle-tracker.js +244 -0
- package/core/indexing/model-pool.js +166 -0
- package/core/infrastructure/code-graph-repository.js +120 -0
- package/core/infrastructure/codebase-repository.js +131 -0
- package/core/infrastructure/config/dedup.js +54 -0
- package/core/infrastructure/config/embedding.js +298 -0
- package/core/infrastructure/config/graph.js +80 -0
- package/core/infrastructure/config/index.js +82 -0
- package/core/infrastructure/config/indexing.js +8 -0
- package/core/infrastructure/config/platform.js +254 -0
- package/core/infrastructure/config/ranking.js +221 -0
- package/core/infrastructure/config/search.js +396 -0
- package/core/infrastructure/config/translation.js +89 -0
- package/core/infrastructure/config/vector-store.js +114 -0
- package/core/infrastructure/constants.js +86 -0
- package/core/infrastructure/coreml-cascade.js +909 -0
- package/core/infrastructure/coreml-cascade.json +46 -0
- package/core/infrastructure/coreml-provider.js +81 -0
- package/core/infrastructure/db-utils.js +69 -0
- package/core/infrastructure/dedup-hashing.js +83 -0
- package/core/infrastructure/hardware-capability.js +332 -0
- package/core/infrastructure/index.js +104 -0
- package/core/infrastructure/language-patterns/maps.js +121 -0
- package/core/infrastructure/language-patterns/registry-core.js +323 -0
- package/core/infrastructure/language-patterns/registry-data-query.js +155 -0
- package/core/infrastructure/language-patterns/registry-object-oriented.js +285 -0
- package/core/infrastructure/language-patterns/registry-tooling.js +240 -0
- package/core/infrastructure/language-patterns/registry-web-style.js +143 -0
- package/core/infrastructure/language-patterns/registry.js +19 -0
- package/core/infrastructure/language-patterns.js +141 -0
- package/core/infrastructure/llm-provider.js +733 -0
- package/core/infrastructure/manifest.json +46 -0
- package/core/infrastructure/maxsim.wasm +0 -0
- package/core/infrastructure/model-fetcher.js +423 -0
- package/core/infrastructure/model-registry.js +214 -0
- package/core/infrastructure/native-inference.js +587 -0
- package/core/infrastructure/native-resolver.js +187 -0
- package/core/infrastructure/native-sparse-gram.js +257 -0
- package/core/infrastructure/native-tokenizer.js +160 -0
- package/core/infrastructure/onnx-mutex.js +45 -0
- package/core/infrastructure/onnx-session-utils.js +261 -0
- package/core/infrastructure/ort-pipeline.js +111 -0
- package/core/infrastructure/project-detector.js +102 -0
- package/core/infrastructure/quantization.js +410 -0
- package/core/infrastructure/simd-distance.js +502 -0
- package/core/infrastructure/simd-distance.wasm +0 -0
- package/core/infrastructure/tree-sitter-provider.js +665 -0
- package/core/infrastructure/webgpu-maxsim.js +222 -0
- package/core/query/index.js +35 -0
- package/core/query/intent-detector.js +201 -0
- package/core/query/intent-router.js +156 -0
- package/core/query/query-router-catboost.js +222 -0
- package/core/query/query-router-ml.js +266 -0
- package/core/query/query-router.js +213 -0
- package/core/ranking/cascaded-scorer.js +379 -0
- package/core/ranking/flashrank.js +810 -0
- package/core/ranking/index.js +49 -0
- package/core/ranking/late-interaction-index.js +2383 -0
- package/core/ranking/late-interaction-model.js +812 -0
- package/core/ranking/local-reranker.js +374 -0
- package/core/ranking/mmr.js +379 -0
- package/core/ranking/quality-scorer.js +363 -0
- package/core/search/context-expander.js +1167 -0
- package/core/search/dedup/sibling-expander.js +327 -0
- package/core/search/index.js +16 -0
- package/core/search/search-boost.js +259 -0
- package/core/search/search-cli.js +544 -0
- package/core/search/search-format.js +282 -0
- package/core/search/search-fusion.js +327 -0
- package/core/search/search-hybrid.js +204 -0
- package/core/search/search-pattern-chunks.js +337 -0
- package/core/search/search-pattern-planner.js +439 -0
- package/core/search/search-pattern-prefilter.js +412 -0
- package/core/search/search-pattern-ripgrep.js +663 -0
- package/core/search/search-pattern.js +463 -0
- package/core/search/search-postprocess.js +452 -0
- package/core/search/search-semantic.js +706 -0
- package/core/search/search-server.js +554 -0
- package/core/search/session-daemon-prewarm.mjs +164 -0
- package/core/search/session-warmup.js +595 -0
- package/core/search/sweet-search.js +632 -0
- package/core/search/warmup-metrics.js +532 -0
- package/core/start-server.js +6 -0
- package/core/training/query-router/features/extractor.js +762 -0
- package/core/training/query-router/features/multilingual-patterns.js +431 -0
- package/core/training/query-router/features/text-segmenter.js +303 -0
- package/core/training/query-router/features/unicode-utils.js +383 -0
- package/core/training/query-router/output/v45_router_d4.js +11521 -0
- package/core/training/query-router/output/v46_router_d4.js +11498 -0
- package/core/vector-store/binary-heap.js +227 -0
- package/core/vector-store/binary-hnsw-index.js +1004 -0
- package/core/vector-store/float-vector-store.js +234 -0
- package/core/vector-store/hnsw-index.js +580 -0
- package/core/vector-store/index.js +39 -0
- package/core/vector-store/seismic-index.js +498 -0
- package/core/vocabulary/index.js +84 -0
- package/core/vocabulary/vocab-constants.js +20 -0
- package/core/vocabulary/vocab-miner-extractors.js +375 -0
- package/core/vocabulary/vocab-miner-nl.js +404 -0
- package/core/vocabulary/vocab-miner-utils.js +146 -0
- package/core/vocabulary/vocab-miner.js +574 -0
- package/core/vocabulary/vocab-prewarm-cli.js +110 -0
- package/core/vocabulary/vocab-ranker.js +492 -0
- package/core/vocabulary/vocab-warmer.js +523 -0
- package/core/vocabulary/vocab-warmup-orchestrator.js +425 -0
- package/core/vocabulary/vocabulary-utils.js +704 -0
- package/crates/wasm-router/pkg/package.json +13 -0
- package/crates/wasm-router/pkg/query_router_wasm.d.ts +36 -0
- package/crates/wasm-router/pkg/query_router_wasm.js +271 -0
- package/crates/wasm-router/pkg/query_router_wasm_bg.wasm +0 -0
- package/crates/wasm-router/pkg/query_router_wasm_bg.wasm.d.ts +19 -0
- package/mcp/config-gen.js +121 -0
- package/mcp/server.js +335 -0
- package/mcp/tool-handlers.js +476 -0
- package/package.json +131 -9
- package/scripts/benchmark-harness.js +794 -0
- package/scripts/init.js +1058 -0
- package/scripts/smoke-test.js +435 -0
- package/scripts/uninstall.js +478 -0
- package/scripts/verify-runtime.js +176 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ripgrep Integration Module — binary discovery, capability detection, and execution.
|
|
3
|
+
*
|
|
4
|
+
* Handles finding the ripgrep binary (including Claude Code's embedded multicall
|
|
5
|
+
* binary), detecting --and support, ARG_MAX-safe file batching, and the three
|
|
6
|
+
* core spawn wrappers: executeRipgrep (JSON), runRipgrepFilesWithMatches
|
|
7
|
+
* (files-with-matches), and runRipgrepJson (JSON output).
|
|
8
|
+
*
|
|
9
|
+
* Split from search-pattern.js for the 500-line-limit rule.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawn, execFileSync } from 'child_process';
|
|
13
|
+
import { existsSync, readdirSync } from 'fs';
|
|
14
|
+
import { StringDecoder } from 'string_decoder';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { RIPGREP_CODE_TYPE_GLOB } from '../infrastructure/constants.js';
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Module-level state — ripgrep binary cache and capabilities
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
let _rgCheckPromise = null;
|
|
23
|
+
let _rgBinary = null; // Resolved path to rg binary
|
|
24
|
+
let _rgCapabilities = null;
|
|
25
|
+
|
|
26
|
+
const RIPGREP_CODE_TYPE = RIPGREP_CODE_TYPE_GLOB;
|
|
27
|
+
const RIPGREP_MAX_BATCH_FILES = 500;
|
|
28
|
+
const RIPGREP_MAX_BATCH_ARG_BYTES = 96 * 1024;
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Ripgrep detection (race-safe: caches the promise, not just the result)
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Find the ripgrep binary. Tries:
|
|
36
|
+
* 1. 'rg' on PATH (direct spawn)
|
|
37
|
+
* 2. Known multicall binary locations (Claude Code embeds rg)
|
|
38
|
+
* 3. Common install paths (/opt/homebrew/bin/rg, /usr/local/bin/rg)
|
|
39
|
+
*
|
|
40
|
+
* Caches the result. Returns the binary path or null.
|
|
41
|
+
*/
|
|
42
|
+
function _findRg() {
|
|
43
|
+
if (_rgBinary !== null) return _rgBinary || null;
|
|
44
|
+
|
|
45
|
+
const candidates = [
|
|
46
|
+
'rg', // Direct PATH
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// Claude Code multicall binary: ARGV0=rg makes it act as ripgrep
|
|
50
|
+
const claudeVersionsDir = path.join(
|
|
51
|
+
process.env.HOME || '', '.local', 'share', 'claude', 'versions'
|
|
52
|
+
);
|
|
53
|
+
// Enumerate available versions instead of hardcoding a specific version
|
|
54
|
+
if (process.env.CLAUDE_VERSION) {
|
|
55
|
+
const versionedBin = path.join(claudeVersionsDir, process.env.CLAUDE_VERSION);
|
|
56
|
+
if (existsSync(versionedBin)) candidates.push(versionedBin);
|
|
57
|
+
} else if (existsSync(claudeVersionsDir)) {
|
|
58
|
+
try {
|
|
59
|
+
const entries = readdirSync(claudeVersionsDir).sort().reverse();
|
|
60
|
+
if (entries.length > 0) {
|
|
61
|
+
candidates.push(path.join(claudeVersionsDir, entries[0]));
|
|
62
|
+
}
|
|
63
|
+
} catch { /* versions dir unreadable */ }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Common homebrew / system paths
|
|
67
|
+
candidates.push('/opt/homebrew/bin/rg', '/usr/local/bin/rg', '/usr/bin/rg');
|
|
68
|
+
|
|
69
|
+
for (const bin of candidates) {
|
|
70
|
+
try {
|
|
71
|
+
execFileSync(bin, ['--version'], { stdio: 'pipe', timeout: 3000, env: { ...process.env, ARGV0: 'rg' } });
|
|
72
|
+
_rgBinary = bin;
|
|
73
|
+
return bin;
|
|
74
|
+
} catch {
|
|
75
|
+
// Try next
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_rgBinary = '';
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if ripgrep (rg) is installed and available.
|
|
85
|
+
* Concurrent callers share a single probe — no duplicate spawns.
|
|
86
|
+
*/
|
|
87
|
+
export function isRipgrepAvailable() {
|
|
88
|
+
if (_rgCheckPromise) return _rgCheckPromise;
|
|
89
|
+
_rgCheckPromise = Promise.resolve(_findRg() !== null);
|
|
90
|
+
return _rgCheckPromise;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Reset cached availability (for testing). */
|
|
94
|
+
export function _resetRgCache() {
|
|
95
|
+
_rgCheckPromise = null;
|
|
96
|
+
_rgBinary = null;
|
|
97
|
+
_rgCapabilities = null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// Ripgrep capability detection
|
|
102
|
+
// =============================================================================
|
|
103
|
+
|
|
104
|
+
export function _getRgCapabilities() {
|
|
105
|
+
const rgBin = _findRg();
|
|
106
|
+
if (!rgBin) {
|
|
107
|
+
throw new Error('ripgrep (rg) not found. Install: brew install ripgrep');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (_rgCapabilities) return _rgCapabilities;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const help = execFileSync(rgBin, ['--help'], {
|
|
114
|
+
stdio: 'pipe',
|
|
115
|
+
timeout: 3000,
|
|
116
|
+
env: { ...process.env, ARGV0: 'rg' },
|
|
117
|
+
}).toString('utf-8');
|
|
118
|
+
_rgCapabilities = {
|
|
119
|
+
supportsAnd: /\s--and\b/.test(help),
|
|
120
|
+
};
|
|
121
|
+
} catch {
|
|
122
|
+
_rgCapabilities = { supportsAnd: false };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return _rgCapabilities;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// =============================================================================
|
|
129
|
+
// Path normalization and file batching
|
|
130
|
+
// =============================================================================
|
|
131
|
+
|
|
132
|
+
export function normalizeSearchPath(searchDir, filePath) {
|
|
133
|
+
if (!filePath) return null;
|
|
134
|
+
const relative = path.isAbsolute(filePath)
|
|
135
|
+
? path.relative(searchDir, filePath)
|
|
136
|
+
: filePath;
|
|
137
|
+
return relative.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function chunkRipgrepFiles(files) {
|
|
141
|
+
const batches = [];
|
|
142
|
+
let current = [];
|
|
143
|
+
let currentBytes = 0;
|
|
144
|
+
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
const fileBytes = Buffer.byteLength(file) + 1;
|
|
147
|
+
const wouldOverflow =
|
|
148
|
+
current.length >= RIPGREP_MAX_BATCH_FILES ||
|
|
149
|
+
currentBytes + fileBytes > RIPGREP_MAX_BATCH_ARG_BYTES;
|
|
150
|
+
|
|
151
|
+
if (wouldOverflow && current.length > 0) {
|
|
152
|
+
batches.push(current);
|
|
153
|
+
current = [];
|
|
154
|
+
currentBytes = 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
current.push(file);
|
|
158
|
+
currentBytes += fileBytes;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (current.length > 0) {
|
|
162
|
+
batches.push(current);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return batches;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// =============================================================================
|
|
169
|
+
// Ripgrep runner — core spawn wrappers
|
|
170
|
+
// =============================================================================
|
|
171
|
+
|
|
172
|
+
async function executeRipgrep({
|
|
173
|
+
patterns,
|
|
174
|
+
searchDir,
|
|
175
|
+
files = null,
|
|
176
|
+
fixedString = false,
|
|
177
|
+
caseInsensitive = false,
|
|
178
|
+
globs = [],
|
|
179
|
+
outputMode = 'json',
|
|
180
|
+
timeout = 10000,
|
|
181
|
+
useAnd = false,
|
|
182
|
+
maxCount = 0,
|
|
183
|
+
lightweightParse = false,
|
|
184
|
+
}) {
|
|
185
|
+
const rgBin = _findRg();
|
|
186
|
+
if (!rgBin) {
|
|
187
|
+
throw new Error('ripgrep (rg) not found. Install: brew install ripgrep');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const effectivePatterns = Array.isArray(patterns) ? patterns.filter(Boolean) : [patterns].filter(Boolean);
|
|
191
|
+
if (effectivePatterns.length === 0) {
|
|
192
|
+
return outputMode === 'json' ? [] : [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
const args = [
|
|
197
|
+
outputMode === 'json' ? '--json' : '--files-with-matches',
|
|
198
|
+
'--type-add', RIPGREP_CODE_TYPE,
|
|
199
|
+
'--type', 'code',
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
if (maxCount > 0) args.push('--max-count', String(maxCount));
|
|
203
|
+
if (caseInsensitive) args.push('-i');
|
|
204
|
+
for (const glob of globs) {
|
|
205
|
+
if (glob) args.push('--glob', glob);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (useAnd && effectivePatterns.length > 1) {
|
|
209
|
+
args.push(fixedString ? '-F' : effectivePatterns[0]);
|
|
210
|
+
if (fixedString) args.push(effectivePatterns[0]);
|
|
211
|
+
for (const pattern of effectivePatterns.slice(1)) {
|
|
212
|
+
args.push('--and');
|
|
213
|
+
if (fixedString) args.push('-F');
|
|
214
|
+
args.push(pattern);
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
if (fixedString) args.push('-F');
|
|
218
|
+
args.push(effectivePatterns[0]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (Array.isArray(files) && files.length > 0) {
|
|
222
|
+
args.push('--', ...files);
|
|
223
|
+
} else {
|
|
224
|
+
args.push('.');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const proc = spawn(rgBin, args, {
|
|
228
|
+
cwd: searchDir,
|
|
229
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
230
|
+
timeout,
|
|
231
|
+
env: { ...process.env, ARGV0: 'rg' },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const stdoutChunks = [];
|
|
235
|
+
let stderr = '';
|
|
236
|
+
|
|
237
|
+
proc.stdout.on('data', (chunk) => { stdoutChunks.push(chunk); });
|
|
238
|
+
proc.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
239
|
+
|
|
240
|
+
proc.on('close', (code) => {
|
|
241
|
+
if (code !== 0 && code !== 1) {
|
|
242
|
+
reject(new Error(`ripgrep failed (code ${code}): ${stderr.slice(0, 200)}`));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const stdout = Buffer.concat(stdoutChunks).toString('utf-8');
|
|
247
|
+
|
|
248
|
+
if (outputMode === 'files') {
|
|
249
|
+
const matches = stdout
|
|
250
|
+
.split('\n')
|
|
251
|
+
.map(line => normalizeSearchPath(searchDir, line.trim()))
|
|
252
|
+
.filter(Boolean);
|
|
253
|
+
resolve(matches);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const matches = [];
|
|
258
|
+
if (lightweightParse) {
|
|
259
|
+
// Fast path: extract only file + line using indexOf (no JSON.parse).
|
|
260
|
+
// For patternSearch, mapMatchesToChunks only reads .file and .line —
|
|
261
|
+
// skipping JSON.parse saves ~22ms on 20K-match queries (30ms→8ms parse).
|
|
262
|
+
// rg --json key order is stable (serde derive): path.text then line_number.
|
|
263
|
+
const PATH_MARKER = '"path":{"text":"';
|
|
264
|
+
const PATH_MARKER_LEN = PATH_MARKER.length;
|
|
265
|
+
const LN_MARKER = '"line_number":';
|
|
266
|
+
const LN_MARKER_LEN = LN_MARKER.length;
|
|
267
|
+
const MATCH_PREFIX = '{"type":"match"';
|
|
268
|
+
const pathCache = new Map();
|
|
269
|
+
|
|
270
|
+
let pos = 0;
|
|
271
|
+
while (pos < stdout.length) {
|
|
272
|
+
const nl = stdout.indexOf('\n', pos);
|
|
273
|
+
const end = nl === -1 ? stdout.length : nl;
|
|
274
|
+
|
|
275
|
+
if (end - pos > 40 && stdout.startsWith(MATCH_PREFIX, pos)) {
|
|
276
|
+
const pathIdx = stdout.indexOf(PATH_MARKER, pos + 15);
|
|
277
|
+
if (pathIdx !== -1 && pathIdx < end) {
|
|
278
|
+
const pathStart = pathIdx + PATH_MARKER_LEN;
|
|
279
|
+
const pathEnd = stdout.indexOf('"', pathStart);
|
|
280
|
+
if (pathEnd !== -1 && pathEnd < end) {
|
|
281
|
+
const lnIdx = stdout.indexOf(LN_MARKER, pathEnd);
|
|
282
|
+
if (lnIdx !== -1 && lnIdx < end) {
|
|
283
|
+
const lnStart = lnIdx + LN_MARKER_LEN;
|
|
284
|
+
let lnEnd = lnStart;
|
|
285
|
+
while (lnEnd < end && stdout.charCodeAt(lnEnd) >= 48 && stdout.charCodeAt(lnEnd) <= 57) lnEnd++;
|
|
286
|
+
if (lnEnd > lnStart) {
|
|
287
|
+
const rawPath = stdout.substring(pathStart, pathEnd);
|
|
288
|
+
let file = pathCache.get(rawPath);
|
|
289
|
+
if (file === undefined) {
|
|
290
|
+
file = normalizeSearchPath(searchDir, rawPath);
|
|
291
|
+
pathCache.set(rawPath, file);
|
|
292
|
+
}
|
|
293
|
+
if (file) matches.push({ file, line: parseInt(stdout.substring(lnStart, lnEnd), 10) });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
pos = nl === -1 ? stdout.length : nl + 1;
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
for (const line of stdout.split('\n')) {
|
|
303
|
+
if (!line) continue;
|
|
304
|
+
try {
|
|
305
|
+
const obj = JSON.parse(line);
|
|
306
|
+
if (obj.type !== 'match') continue;
|
|
307
|
+
const file = normalizeSearchPath(searchDir, obj.data?.path?.text);
|
|
308
|
+
const lineNumber = obj.data?.line_number;
|
|
309
|
+
const text = obj.data?.lines?.text?.trimEnd() || '';
|
|
310
|
+
const firstSubmatch = obj.data?.submatches?.[0];
|
|
311
|
+
const column = typeof firstSubmatch?.start === 'number' ? firstSubmatch.start + 1 : null;
|
|
312
|
+
const matchText = firstSubmatch?.match?.text || '';
|
|
313
|
+
if (file && lineNumber != null) {
|
|
314
|
+
matches.push({ file, line: lineNumber, column, matchText, content: text });
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
// Skip malformed lines.
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
resolve(matches);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
proc.on('error', (err) => {
|
|
325
|
+
if (err.code === 'ENOENT') {
|
|
326
|
+
reject(new Error('ripgrep (rg) not found. Install: brew install ripgrep'));
|
|
327
|
+
} else {
|
|
328
|
+
reject(err);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// =============================================================================
|
|
335
|
+
// Streaming ripgrep JSON — incremental line-by-line parse with early-exit
|
|
336
|
+
// =============================================================================
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Streaming ripgrep JSON executor. Parses rg --json output line-by-line as it
|
|
340
|
+
* arrives on stdout, avoiding the Buffer.concat + toString + split overhead of
|
|
341
|
+
* executeRipgrep. Supports --max-count and an onMatch callback that can kill
|
|
342
|
+
* the rg process for early exit.
|
|
343
|
+
*
|
|
344
|
+
* Uses StringDecoder for correct multi-byte UTF-8 handling across chunk
|
|
345
|
+
* boundaries.
|
|
346
|
+
*/
|
|
347
|
+
async function executeRipgrepStreaming({
|
|
348
|
+
patterns,
|
|
349
|
+
searchDir,
|
|
350
|
+
files = null,
|
|
351
|
+
fixedString = false,
|
|
352
|
+
caseInsensitive = false,
|
|
353
|
+
globs = [],
|
|
354
|
+
timeout = 10000,
|
|
355
|
+
useAnd = false,
|
|
356
|
+
maxCount = 0,
|
|
357
|
+
onMatch = null,
|
|
358
|
+
}) {
|
|
359
|
+
const rgBin = _findRg();
|
|
360
|
+
if (!rgBin) {
|
|
361
|
+
throw new Error('ripgrep (rg) not found. Install: brew install ripgrep');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const effectivePatterns = Array.isArray(patterns)
|
|
365
|
+
? patterns.filter(Boolean)
|
|
366
|
+
: [patterns].filter(Boolean);
|
|
367
|
+
if (effectivePatterns.length === 0) return [];
|
|
368
|
+
|
|
369
|
+
return new Promise((resolve, reject) => {
|
|
370
|
+
const args = [
|
|
371
|
+
'--json',
|
|
372
|
+
'--type-add', RIPGREP_CODE_TYPE,
|
|
373
|
+
'--type', 'code',
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
if (maxCount > 0) args.push('--max-count', String(maxCount));
|
|
377
|
+
if (caseInsensitive) args.push('-i');
|
|
378
|
+
for (const glob of globs) {
|
|
379
|
+
if (glob) args.push('--glob', glob);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (useAnd && effectivePatterns.length > 1) {
|
|
383
|
+
args.push(fixedString ? '-F' : effectivePatterns[0]);
|
|
384
|
+
if (fixedString) args.push(effectivePatterns[0]);
|
|
385
|
+
for (const pattern of effectivePatterns.slice(1)) {
|
|
386
|
+
args.push('--and');
|
|
387
|
+
if (fixedString) args.push('-F');
|
|
388
|
+
args.push(pattern);
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
if (fixedString) args.push('-F');
|
|
392
|
+
args.push(effectivePatterns[0]);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (Array.isArray(files) && files.length > 0) {
|
|
396
|
+
args.push('--', ...files);
|
|
397
|
+
} else {
|
|
398
|
+
args.push('.');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const proc = spawn(rgBin, args, {
|
|
402
|
+
cwd: searchDir,
|
|
403
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
404
|
+
timeout,
|
|
405
|
+
env: { ...process.env, ARGV0: 'rg' },
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const matches = [];
|
|
409
|
+
let killed = false;
|
|
410
|
+
const decoder = new StringDecoder('utf-8');
|
|
411
|
+
let partial = '';
|
|
412
|
+
let stderr = '';
|
|
413
|
+
|
|
414
|
+
proc.stdout.on('data', (chunk) => {
|
|
415
|
+
if (killed) return;
|
|
416
|
+
|
|
417
|
+
partial += decoder.write(chunk);
|
|
418
|
+
const lines = partial.split('\n');
|
|
419
|
+
partial = lines.pop() || '';
|
|
420
|
+
|
|
421
|
+
for (const line of lines) {
|
|
422
|
+
if (!line || killed) continue;
|
|
423
|
+
try {
|
|
424
|
+
const obj = JSON.parse(line);
|
|
425
|
+
if (obj.type !== 'match') continue;
|
|
426
|
+
const file = normalizeSearchPath(searchDir, obj.data?.path?.text);
|
|
427
|
+
const lineNumber = obj.data?.line_number;
|
|
428
|
+
const text = obj.data?.lines?.text?.trimEnd() || '';
|
|
429
|
+
const firstSubmatch = obj.data?.submatches?.[0];
|
|
430
|
+
const column = typeof firstSubmatch?.start === 'number'
|
|
431
|
+
? firstSubmatch.start + 1
|
|
432
|
+
: null;
|
|
433
|
+
const matchText = firstSubmatch?.match?.text || '';
|
|
434
|
+
if (file && lineNumber != null) {
|
|
435
|
+
const match = { file, line: lineNumber, column, matchText, content: text };
|
|
436
|
+
matches.push(match);
|
|
437
|
+
if (onMatch && onMatch(match) === false) {
|
|
438
|
+
killed = true;
|
|
439
|
+
proc.kill('SIGTERM');
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
} catch {
|
|
444
|
+
// Skip malformed lines.
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
proc.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
450
|
+
|
|
451
|
+
proc.on('close', (code) => {
|
|
452
|
+
if (!killed) {
|
|
453
|
+
// Flush decoder and process any remaining partial line
|
|
454
|
+
partial += decoder.end();
|
|
455
|
+
if (partial) {
|
|
456
|
+
try {
|
|
457
|
+
const obj = JSON.parse(partial);
|
|
458
|
+
if (obj.type === 'match') {
|
|
459
|
+
const file = normalizeSearchPath(searchDir, obj.data?.path?.text);
|
|
460
|
+
const lineNumber = obj.data?.line_number;
|
|
461
|
+
const text = obj.data?.lines?.text?.trimEnd() || '';
|
|
462
|
+
const firstSubmatch = obj.data?.submatches?.[0];
|
|
463
|
+
const column = typeof firstSubmatch?.start === 'number'
|
|
464
|
+
? firstSubmatch.start + 1
|
|
465
|
+
: null;
|
|
466
|
+
const matchText = firstSubmatch?.match?.text || '';
|
|
467
|
+
if (file && lineNumber != null) {
|
|
468
|
+
matches.push({ file, line: lineNumber, column, matchText, content: text });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
} catch { /* ignore */ }
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (killed || code === 0 || code === 1) {
|
|
476
|
+
resolve(matches);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
reject(new Error(`ripgrep failed (code ${code}): ${stderr.slice(0, 200)}`));
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
proc.on('error', (err) => {
|
|
483
|
+
if (killed) {
|
|
484
|
+
resolve(matches);
|
|
485
|
+
} else if (err.code === 'ENOENT') {
|
|
486
|
+
reject(new Error('ripgrep (rg) not found. Install: brew install ripgrep'));
|
|
487
|
+
} else {
|
|
488
|
+
reject(err);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export async function runRipgrepFilesWithMatches(patterns, searchDir, opts = {}) {
|
|
495
|
+
const {
|
|
496
|
+
files = null,
|
|
497
|
+
fixedString = false,
|
|
498
|
+
caseInsensitive = false,
|
|
499
|
+
globs = [],
|
|
500
|
+
timeout = 10000,
|
|
501
|
+
useAnd = false,
|
|
502
|
+
} = opts;
|
|
503
|
+
|
|
504
|
+
if (Array.isArray(files) && files.length === 0) return [];
|
|
505
|
+
|
|
506
|
+
if (!Array.isArray(files)) {
|
|
507
|
+
return executeRipgrep({
|
|
508
|
+
patterns,
|
|
509
|
+
searchDir,
|
|
510
|
+
files: null,
|
|
511
|
+
fixedString,
|
|
512
|
+
caseInsensitive,
|
|
513
|
+
globs,
|
|
514
|
+
outputMode: 'files',
|
|
515
|
+
timeout,
|
|
516
|
+
useAnd,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const batches = chunkRipgrepFiles(files);
|
|
521
|
+
const batchResults = await Promise.all(batches.map(batch =>
|
|
522
|
+
executeRipgrep({
|
|
523
|
+
patterns,
|
|
524
|
+
searchDir,
|
|
525
|
+
files: batch,
|
|
526
|
+
fixedString,
|
|
527
|
+
caseInsensitive,
|
|
528
|
+
globs,
|
|
529
|
+
outputMode: 'files',
|
|
530
|
+
timeout,
|
|
531
|
+
useAnd,
|
|
532
|
+
})
|
|
533
|
+
));
|
|
534
|
+
const matched = new Set();
|
|
535
|
+
for (const batchMatches of batchResults) {
|
|
536
|
+
for (const file of batchMatches) matched.add(file);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return [...matched];
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export async function runRipgrepJson(regex, searchDir, opts = {}) {
|
|
543
|
+
const {
|
|
544
|
+
files = null,
|
|
545
|
+
fixedString = false,
|
|
546
|
+
globs = [],
|
|
547
|
+
timeout = 10000,
|
|
548
|
+
lightweightParse = false,
|
|
549
|
+
} = opts;
|
|
550
|
+
|
|
551
|
+
if (Array.isArray(files) && files.length === 0) return [];
|
|
552
|
+
|
|
553
|
+
const allMatches = [];
|
|
554
|
+
if (!Array.isArray(files)) {
|
|
555
|
+
return executeRipgrep({
|
|
556
|
+
patterns: [regex],
|
|
557
|
+
searchDir,
|
|
558
|
+
files: null,
|
|
559
|
+
fixedString,
|
|
560
|
+
globs,
|
|
561
|
+
outputMode: 'json',
|
|
562
|
+
timeout,
|
|
563
|
+
lightweightParse,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const batches = chunkRipgrepFiles(files);
|
|
568
|
+
const batchResults = await Promise.all(batches.map(batch =>
|
|
569
|
+
executeRipgrep({
|
|
570
|
+
patterns: [regex],
|
|
571
|
+
searchDir,
|
|
572
|
+
files: batch,
|
|
573
|
+
fixedString,
|
|
574
|
+
globs,
|
|
575
|
+
outputMode: 'json',
|
|
576
|
+
timeout,
|
|
577
|
+
lightweightParse,
|
|
578
|
+
})
|
|
579
|
+
));
|
|
580
|
+
for (const batchMatches of batchResults) {
|
|
581
|
+
allMatches.push(...batchMatches);
|
|
582
|
+
}
|
|
583
|
+
return allMatches;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// =============================================================================
|
|
587
|
+
// Streaming ripgrep JSON — parallel batches with early-exit support
|
|
588
|
+
// =============================================================================
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Streaming variant of runRipgrepJson. Parses rg output incrementally and
|
|
592
|
+
* supports an onMatch callback for early-exit. Batches run in parallel
|
|
593
|
+
* (unlike the original sequential loop in runRipgrepJson).
|
|
594
|
+
*
|
|
595
|
+
* @param {string} regex - Regex pattern
|
|
596
|
+
* @param {string} searchDir - Directory to search
|
|
597
|
+
* @param {Object} opts
|
|
598
|
+
* @param {string[]|null} opts.files - Explicit file list
|
|
599
|
+
* @param {boolean} opts.fixedString - Use -F mode
|
|
600
|
+
* @param {string[]} opts.globs - Glob filters
|
|
601
|
+
* @param {number} opts.timeout - Spawn timeout
|
|
602
|
+
* @param {number} opts.maxCount - --max-count per file (0 = unlimited)
|
|
603
|
+
* @param {function|null} opts.onMatch - Per-match callback; return false to kill rg
|
|
604
|
+
* @returns {Promise<Array<{file, line, column, matchText, content}>>}
|
|
605
|
+
*/
|
|
606
|
+
export async function runRipgrepJsonStreaming(regex, searchDir, opts = {}) {
|
|
607
|
+
const {
|
|
608
|
+
files = null,
|
|
609
|
+
fixedString = false,
|
|
610
|
+
globs = [],
|
|
611
|
+
timeout = 10000,
|
|
612
|
+
maxCount = 0,
|
|
613
|
+
onMatch = null,
|
|
614
|
+
} = opts;
|
|
615
|
+
|
|
616
|
+
if (Array.isArray(files) && files.length === 0) return [];
|
|
617
|
+
|
|
618
|
+
if (!Array.isArray(files)) {
|
|
619
|
+
return executeRipgrepStreaming({
|
|
620
|
+
patterns: [regex],
|
|
621
|
+
searchDir,
|
|
622
|
+
files: null,
|
|
623
|
+
fixedString,
|
|
624
|
+
globs,
|
|
625
|
+
timeout,
|
|
626
|
+
maxCount,
|
|
627
|
+
onMatch,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const batches = chunkRipgrepFiles(files);
|
|
632
|
+
|
|
633
|
+
// Shared stop flag: when one batch triggers early-exit, stop all batches.
|
|
634
|
+
let stopped = false;
|
|
635
|
+
const sharedOnMatch = onMatch ? (match) => {
|
|
636
|
+
if (stopped) return false;
|
|
637
|
+
const result = onMatch(match);
|
|
638
|
+
if (result === false) {
|
|
639
|
+
stopped = true;
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
return result;
|
|
643
|
+
} : null;
|
|
644
|
+
|
|
645
|
+
const batchResults = await Promise.all(batches.map(batch =>
|
|
646
|
+
executeRipgrepStreaming({
|
|
647
|
+
patterns: [regex],
|
|
648
|
+
searchDir,
|
|
649
|
+
files: batch,
|
|
650
|
+
fixedString,
|
|
651
|
+
globs,
|
|
652
|
+
timeout,
|
|
653
|
+
maxCount,
|
|
654
|
+
onMatch: sharedOnMatch,
|
|
655
|
+
})
|
|
656
|
+
));
|
|
657
|
+
|
|
658
|
+
const allMatches = [];
|
|
659
|
+
for (const batchMatches of batchResults) {
|
|
660
|
+
allMatches.push(...batchMatches);
|
|
661
|
+
}
|
|
662
|
+
return allMatches;
|
|
663
|
+
}
|