codesift-mcp 0.1.0 → 0.2.1
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 +66 -21
- package/README.md +402 -56
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +11 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +177 -67
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +157 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/hooks.d.ts +3 -0
- package/dist/cli/hooks.d.ts.map +1 -0
- package/dist/cli/hooks.js +163 -0
- package/dist/cli/hooks.js.map +1 -0
- package/dist/cli/setup.d.ts +25 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +400 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/formatters-shortening.d.ts +7 -0
- package/dist/formatters-shortening.d.ts.map +1 -0
- package/dist/formatters-shortening.js +68 -0
- package/dist/formatters-shortening.js.map +1 -0
- package/dist/formatters.d.ts +314 -0
- package/dist/formatters.d.ts.map +1 -0
- package/dist/formatters.js +396 -0
- package/dist/formatters.js.map +1 -0
- package/dist/instructions.d.ts +6 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +72 -0
- package/dist/instructions.js.map +1 -0
- package/dist/lsp/lsp-client.d.ts +21 -0
- package/dist/lsp/lsp-client.d.ts.map +1 -0
- package/dist/lsp/lsp-client.js +122 -0
- package/dist/lsp/lsp-client.js.map +1 -0
- package/dist/lsp/lsp-manager.d.ts +12 -0
- package/dist/lsp/lsp-manager.d.ts.map +1 -0
- package/dist/lsp/lsp-manager.js +82 -0
- package/dist/lsp/lsp-manager.js.map +1 -0
- package/dist/lsp/lsp-servers.d.ts +13 -0
- package/dist/lsp/lsp-servers.d.ts.map +1 -0
- package/dist/lsp/lsp-servers.js +57 -0
- package/dist/lsp/lsp-servers.js.map +1 -0
- package/dist/lsp/lsp-tools.d.ts +67 -0
- package/dist/lsp/lsp-tools.d.ts.map +1 -0
- package/dist/lsp/lsp-tools.js +359 -0
- package/dist/lsp/lsp-tools.js.map +1 -0
- package/dist/parser/extractors/_shared.d.ts +11 -0
- package/dist/parser/extractors/_shared.d.ts.map +1 -0
- package/dist/parser/extractors/_shared.js +38 -0
- package/dist/parser/extractors/_shared.js.map +1 -0
- package/dist/parser/extractors/astro.d.ts +15 -0
- package/dist/parser/extractors/astro.d.ts.map +1 -0
- package/dist/parser/extractors/astro.js +104 -0
- package/dist/parser/extractors/astro.js.map +1 -0
- package/dist/parser/extractors/conversation.d.ts +16 -0
- package/dist/parser/extractors/conversation.d.ts.map +1 -0
- package/dist/parser/extractors/conversation.js +196 -0
- package/dist/parser/extractors/conversation.js.map +1 -0
- package/dist/parser/extractors/go.d.ts.map +1 -1
- package/dist/parser/extractors/go.js +22 -45
- package/dist/parser/extractors/go.js.map +1 -1
- package/dist/parser/extractors/python.d.ts +1 -1
- package/dist/parser/extractors/python.d.ts.map +1 -1
- package/dist/parser/extractors/python.js +19 -50
- package/dist/parser/extractors/python.js.map +1 -1
- package/dist/parser/extractors/rust.d.ts +1 -1
- package/dist/parser/extractors/rust.d.ts.map +1 -1
- package/dist/parser/extractors/rust.js +7 -34
- package/dist/parser/extractors/rust.js.map +1 -1
- package/dist/parser/extractors/typescript.d.ts +1 -1
- package/dist/parser/extractors/typescript.d.ts.map +1 -1
- package/dist/parser/extractors/typescript.js +99 -68
- package/dist/parser/extractors/typescript.js.map +1 -1
- package/dist/parser/parser-manager.d.ts.map +1 -1
- package/dist/parser/parser-manager.js +12 -2
- package/dist/parser/parser-manager.js.map +1 -1
- package/dist/parser/symbol-extractor.d.ts +2 -0
- package/dist/parser/symbol-extractor.d.ts.map +1 -1
- package/dist/parser/symbol-extractor.js +2 -0
- package/dist/parser/symbol-extractor.js.map +1 -1
- package/dist/register-tools.d.ts +127 -0
- package/dist/register-tools.d.ts.map +1 -0
- package/dist/register-tools.js +1453 -0
- package/dist/register-tools.js.map +1 -0
- package/dist/retrieval/codebase-retrieval.d.ts +4 -26
- package/dist/retrieval/codebase-retrieval.d.ts.map +1 -1
- package/dist/retrieval/codebase-retrieval.js +105 -403
- package/dist/retrieval/codebase-retrieval.js.map +1 -1
- package/dist/retrieval/retrieval-constants.d.ts +27 -0
- package/dist/retrieval/retrieval-constants.d.ts.map +1 -0
- package/dist/retrieval/retrieval-constants.js +27 -0
- package/dist/retrieval/retrieval-constants.js.map +1 -0
- package/dist/retrieval/retrieval-schemas.d.ts +107 -0
- package/dist/retrieval/retrieval-schemas.d.ts.map +1 -0
- package/dist/retrieval/retrieval-schemas.js +102 -0
- package/dist/retrieval/retrieval-schemas.js.map +1 -0
- package/dist/retrieval/retrieval-utils.d.ts +40 -0
- package/dist/retrieval/retrieval-utils.d.ts.map +1 -0
- package/dist/retrieval/retrieval-utils.js +139 -0
- package/dist/retrieval/retrieval-utils.js.map +1 -0
- package/dist/retrieval/semantic-handlers.d.ts +8 -0
- package/dist/retrieval/semantic-handlers.d.ts.map +1 -0
- package/dist/retrieval/semantic-handlers.js +152 -0
- package/dist/retrieval/semantic-handlers.js.map +1 -0
- package/dist/search/bm25.d.ts +6 -1
- package/dist/search/bm25.d.ts.map +1 -1
- package/dist/search/bm25.js +95 -32
- package/dist/search/bm25.js.map +1 -1
- package/dist/search/chunker.d.ts +10 -0
- package/dist/search/chunker.d.ts.map +1 -1
- package/dist/search/chunker.js +63 -11
- package/dist/search/chunker.js.map +1 -1
- package/dist/search/reranker.d.ts +15 -0
- package/dist/search/reranker.d.ts.map +1 -0
- package/dist/search/reranker.js +126 -0
- package/dist/search/reranker.js.map +1 -0
- package/dist/search/semantic.d.ts +1 -1
- package/dist/search/semantic.d.ts.map +1 -1
- package/dist/search/semantic.js +40 -45
- package/dist/search/semantic.js.map +1 -1
- package/dist/server-helpers.d.ts +29 -0
- package/dist/server-helpers.d.ts.map +1 -0
- package/dist/server-helpers.js +312 -0
- package/dist/server-helpers.js.map +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +11 -271
- package/dist/server.js.map +1 -1
- package/dist/storage/_shared.d.ts +9 -0
- package/dist/storage/_shared.d.ts.map +1 -0
- package/dist/storage/_shared.js +26 -0
- package/dist/storage/_shared.js.map +1 -0
- package/dist/storage/chunk-store.d.ts.map +1 -1
- package/dist/storage/chunk-store.js +23 -63
- package/dist/storage/chunk-store.js.map +1 -1
- package/dist/storage/embedding-store.d.ts +6 -3
- package/dist/storage/embedding-store.d.ts.map +1 -1
- package/dist/storage/embedding-store.js +54 -30
- package/dist/storage/embedding-store.js.map +1 -1
- package/dist/storage/graph-store.d.ts +48 -0
- package/dist/storage/graph-store.d.ts.map +1 -0
- package/dist/storage/graph-store.js +52 -0
- package/dist/storage/graph-store.js.map +1 -0
- package/dist/storage/index-store.d.ts +5 -0
- package/dist/storage/index-store.d.ts.map +1 -1
- package/dist/storage/index-store.js +28 -16
- package/dist/storage/index-store.js.map +1 -1
- package/dist/storage/registry.d.ts +4 -0
- package/dist/storage/registry.d.ts.map +1 -1
- package/dist/storage/registry.js +16 -16
- package/dist/storage/registry.js.map +1 -1
- package/dist/storage/usage-stats.d.ts +6 -0
- package/dist/storage/usage-stats.d.ts.map +1 -1
- package/dist/storage/usage-stats.js +59 -11
- package/dist/storage/usage-stats.js.map +1 -1
- package/dist/storage/usage-tracker.d.ts +3 -0
- package/dist/storage/usage-tracker.d.ts.map +1 -1
- package/dist/storage/usage-tracker.js +50 -132
- package/dist/storage/usage-tracker.js.map +1 -1
- package/dist/storage/watcher.d.ts +2 -1
- package/dist/storage/watcher.d.ts.map +1 -1
- package/dist/storage/watcher.js +16 -16
- package/dist/storage/watcher.js.map +1 -1
- package/dist/tools/ast-query-tools.d.ts +29 -0
- package/dist/tools/ast-query-tools.d.ts.map +1 -0
- package/dist/tools/ast-query-tools.js +110 -0
- package/dist/tools/ast-query-tools.js.map +1 -0
- package/dist/tools/boundary-tools.d.ts +31 -0
- package/dist/tools/boundary-tools.d.ts.map +1 -0
- package/dist/tools/boundary-tools.js +62 -0
- package/dist/tools/boundary-tools.js.map +1 -0
- package/dist/tools/clone-tools.d.ts +35 -0
- package/dist/tools/clone-tools.d.ts.map +1 -0
- package/dist/tools/clone-tools.js +181 -0
- package/dist/tools/clone-tools.js.map +1 -0
- package/dist/tools/community-tools.d.ts +23 -0
- package/dist/tools/community-tools.d.ts.map +1 -0
- package/dist/tools/community-tools.js +297 -0
- package/dist/tools/community-tools.js.map +1 -0
- package/dist/tools/complexity-tools.d.ts +34 -0
- package/dist/tools/complexity-tools.d.ts.map +1 -0
- package/dist/tools/complexity-tools.js +135 -0
- package/dist/tools/complexity-tools.js.map +1 -0
- package/dist/tools/context-tools.d.ts +44 -3
- package/dist/tools/context-tools.d.ts.map +1 -1
- package/dist/tools/context-tools.js +329 -99
- package/dist/tools/context-tools.js.map +1 -1
- package/dist/tools/conversation-tools.d.ts +107 -0
- package/dist/tools/conversation-tools.d.ts.map +1 -0
- package/dist/tools/conversation-tools.js +419 -0
- package/dist/tools/conversation-tools.js.map +1 -0
- package/dist/tools/coordinator-tools.d.ts +73 -0
- package/dist/tools/coordinator-tools.d.ts.map +1 -0
- package/dist/tools/coordinator-tools.js +153 -0
- package/dist/tools/coordinator-tools.js.map +1 -0
- package/dist/tools/cross-repo-tools.d.ts +43 -0
- package/dist/tools/cross-repo-tools.d.ts.map +1 -0
- package/dist/tools/cross-repo-tools.js +55 -0
- package/dist/tools/cross-repo-tools.js.map +1 -0
- package/dist/tools/diff-tools.d.ts +4 -1
- package/dist/tools/diff-tools.d.ts.map +1 -1
- package/dist/tools/diff-tools.js +23 -5
- package/dist/tools/diff-tools.js.map +1 -1
- package/dist/tools/frequency-tools.d.ts +46 -0
- package/dist/tools/frequency-tools.d.ts.map +1 -0
- package/dist/tools/frequency-tools.js +184 -0
- package/dist/tools/frequency-tools.js.map +1 -0
- package/dist/tools/generate-tools.d.ts.map +1 -1
- package/dist/tools/generate-tools.js +13 -2
- package/dist/tools/generate-tools.js.map +1 -1
- package/dist/tools/graph-tools.d.ts +44 -11
- package/dist/tools/graph-tools.d.ts.map +1 -1
- package/dist/tools/graph-tools.js +147 -104
- package/dist/tools/graph-tools.js.map +1 -1
- package/dist/tools/hotspot-tools.d.ts +24 -0
- package/dist/tools/hotspot-tools.d.ts.map +1 -0
- package/dist/tools/hotspot-tools.js +122 -0
- package/dist/tools/hotspot-tools.js.map +1 -0
- package/dist/tools/impact-tools.d.ts +13 -0
- package/dist/tools/impact-tools.d.ts.map +1 -0
- package/dist/tools/impact-tools.js +238 -0
- package/dist/tools/impact-tools.js.map +1 -0
- package/dist/tools/index-tools.d.ts +44 -3
- package/dist/tools/index-tools.d.ts.map +1 -1
- package/dist/tools/index-tools.js +530 -222
- package/dist/tools/index-tools.js.map +1 -1
- package/dist/tools/memory-tools.d.ts +35 -0
- package/dist/tools/memory-tools.d.ts.map +1 -0
- package/dist/tools/memory-tools.js +229 -0
- package/dist/tools/memory-tools.js.map +1 -0
- package/dist/tools/outline-tools.d.ts +24 -13
- package/dist/tools/outline-tools.d.ts.map +1 -1
- package/dist/tools/outline-tools.js +113 -87
- package/dist/tools/outline-tools.js.map +1 -1
- package/dist/tools/pattern-tools.d.ts +32 -0
- package/dist/tools/pattern-tools.d.ts.map +1 -0
- package/dist/tools/pattern-tools.js +116 -0
- package/dist/tools/pattern-tools.js.map +1 -0
- package/dist/tools/report-tools.d.ts +5 -0
- package/dist/tools/report-tools.d.ts.map +1 -0
- package/dist/tools/report-tools.js +167 -0
- package/dist/tools/report-tools.js.map +1 -0
- package/dist/tools/review-diff-tools.d.ts +148 -0
- package/dist/tools/review-diff-tools.d.ts.map +1 -0
- package/dist/tools/review-diff-tools.js +852 -0
- package/dist/tools/review-diff-tools.js.map +1 -0
- package/dist/tools/route-tools.d.ts +32 -0
- package/dist/tools/route-tools.d.ts.map +1 -0
- package/dist/tools/route-tools.js +276 -0
- package/dist/tools/route-tools.js.map +1 -0
- package/dist/tools/search-ranker.d.ts +5 -0
- package/dist/tools/search-ranker.d.ts.map +1 -0
- package/dist/tools/search-ranker.js +142 -0
- package/dist/tools/search-ranker.js.map +1 -0
- package/dist/tools/search-tools.d.ts +24 -1
- package/dist/tools/search-tools.d.ts.map +1 -1
- package/dist/tools/search-tools.js +459 -225
- package/dist/tools/search-tools.js.map +1 -1
- package/dist/tools/secret-tools.d.ts +104 -0
- package/dist/tools/secret-tools.d.ts.map +1 -0
- package/dist/tools/secret-tools.js +410 -0
- package/dist/tools/secret-tools.js.map +1 -0
- package/dist/tools/symbol-tools.d.ts +90 -2
- package/dist/tools/symbol-tools.d.ts.map +1 -1
- package/dist/tools/symbol-tools.js +576 -42
- package/dist/tools/symbol-tools.js.map +1 -1
- package/dist/types.d.ts +34 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/framework-detect.d.ts +5 -0
- package/dist/utils/framework-detect.d.ts.map +1 -0
- package/dist/utils/framework-detect.js +36 -0
- package/dist/utils/framework-detect.js.map +1 -0
- package/dist/utils/glob.d.ts +19 -0
- package/dist/utils/glob.d.ts.map +1 -0
- package/dist/utils/glob.js +74 -0
- package/dist/utils/glob.js.map +1 -0
- package/dist/utils/import-graph.d.ts +29 -0
- package/dist/utils/import-graph.d.ts.map +1 -0
- package/dist/utils/import-graph.js +125 -0
- package/dist/utils/import-graph.js.map +1 -0
- package/dist/utils/test-file.d.ts.map +1 -1
- package/dist/utils/test-file.js +1 -0
- package/dist/utils/test-file.js.map +1 -1
- package/dist/utils/walk.d.ts +45 -0
- package/dist/utils/walk.d.ts.map +1 -0
- package/dist/utils/walk.js +87 -0
- package/dist/utils/walk.js.map +1 -0
- package/package.json +12 -5
- package/rules/codesift.md +187 -0
- package/rules/codesift.mdc +192 -0
- package/rules/codex.md +187 -0
- package/rules/gemini.md +187 -0
|
@@ -1,17 +1,82 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { readFile, open } from "node:fs/promises";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { searchBM25 } from "../search/bm25.js";
|
|
5
|
+
import { findReferencesLsp } from "../lsp/lsp-tools.js";
|
|
4
6
|
import { loadConfig } from "../config.js";
|
|
7
|
+
import { isTestFileStrict as isTestFile } from "../utils/test-file.js";
|
|
8
|
+
import { detectFrameworks, isFrameworkEntryPoint } from "../utils/framework-detect.js";
|
|
5
9
|
import { getCodeIndex, getBM25Index } from "./index-tools.js";
|
|
6
|
-
const MAX_REFERENCES =
|
|
10
|
+
const MAX_REFERENCES = 100;
|
|
11
|
+
const MAX_DEAD_CODE_RESULTS = 100;
|
|
7
12
|
const MAX_CONTEXT_LENGTH = 200; // Truncate context lines to prevent huge output from minified files
|
|
13
|
+
/** Skip build artifacts and binary files — docs/audits are intentionally kept */
|
|
14
|
+
const NOISE_PATH_PREFIXES = [".next/", "dist/", "build/", "coverage/", "node_modules/", "__snapshots__/"];
|
|
15
|
+
const NOISE_EXTENSIONS = new Set([".snap", ".lock", ".map", ".svg", ".png", ".jpg", ".ico", ".woff", ".woff2"]);
|
|
16
|
+
function isNoisePath(filePath) {
|
|
17
|
+
if (NOISE_PATH_PREFIXES.some((p) => filePath.startsWith(p)))
|
|
18
|
+
return true;
|
|
19
|
+
const dot = filePath.lastIndexOf(".");
|
|
20
|
+
if (dot >= 0 && NOISE_EXTENSIONS.has(filePath.slice(dot)))
|
|
21
|
+
return true;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
async function requireCodeIndex(repo) {
|
|
25
|
+
const index = await getCodeIndex(repo);
|
|
26
|
+
if (!index) {
|
|
27
|
+
throw new Error(`Repository "${repo}" not found. Index it first with index_folder.`);
|
|
28
|
+
}
|
|
29
|
+
return index;
|
|
30
|
+
}
|
|
31
|
+
async function requireBM25Index(repo) {
|
|
32
|
+
const index = await getBM25Index(repo);
|
|
33
|
+
if (!index) {
|
|
34
|
+
throw new Error(`Repository "${repo}" not found. Index it first with index_folder.`);
|
|
35
|
+
}
|
|
36
|
+
return index;
|
|
37
|
+
}
|
|
38
|
+
function wordBoundaryPattern(name) {
|
|
39
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
40
|
+
return new RegExp(`\\b${escaped}\\b`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Strip internal/BM25 fields from CodeSymbol for leaner output.
|
|
44
|
+
* Removes: repo, tokens, start_col, end_col. Shortens id (strips repo prefix).
|
|
45
|
+
*/
|
|
46
|
+
function stripSymbol(sym) {
|
|
47
|
+
const { repo: _repo, tokens: _tokens, start_col: _sc, end_col: _ec, start_byte: _sb, end_byte: _eb, id, ...rest } = sym;
|
|
48
|
+
// Strip "local/reponame:" prefix from id
|
|
49
|
+
const shortId = id.includes(":") ? id.slice(id.indexOf(":") + 1) : id;
|
|
50
|
+
return { ...rest, id: shortId };
|
|
51
|
+
}
|
|
8
52
|
/**
|
|
9
53
|
* Read a source file and extract lines for a symbol (1-based, inclusive).
|
|
54
|
+
* Uses byte offsets when available for precise reads without loading full file.
|
|
10
55
|
* Returns undefined if the file cannot be read.
|
|
11
56
|
*/
|
|
12
|
-
async function extractSource(repoRoot, file, startLine, endLine) {
|
|
57
|
+
async function extractSource(repoRoot, file, startLine, endLine, startByte, endByte) {
|
|
58
|
+
const filePath = join(repoRoot, file);
|
|
59
|
+
// Fast path: use byte offsets to read exact range
|
|
60
|
+
if (startByte != null && endByte != null && endByte > startByte) {
|
|
61
|
+
try {
|
|
62
|
+
const fh = await open(filePath, "r");
|
|
63
|
+
try {
|
|
64
|
+
const length = endByte - startByte;
|
|
65
|
+
const buf = Buffer.alloc(length);
|
|
66
|
+
await fh.read(buf, 0, length, startByte);
|
|
67
|
+
return buf.toString("utf-8");
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
await fh.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Fall through to line-based extraction
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Fallback: line-based extraction
|
|
13
78
|
try {
|
|
14
|
-
const content = await readFile(
|
|
79
|
+
const content = await readFile(filePath, "utf-8");
|
|
15
80
|
const lines = content.split("\n");
|
|
16
81
|
return lines.slice(startLine - 1, endLine).join("\n");
|
|
17
82
|
}
|
|
@@ -21,35 +86,49 @@ async function extractSource(repoRoot, file, startLine, endLine) {
|
|
|
21
86
|
}
|
|
22
87
|
/**
|
|
23
88
|
* Retrieve a single symbol by ID with fresh source from disk.
|
|
89
|
+
* When include_related is true (default), auto-prefetches:
|
|
90
|
+
* - children (for classes/interfaces) — saves follow-up get_symbols call
|
|
91
|
+
* - symbols in the same file that reference this symbol — saves find_references call
|
|
24
92
|
*/
|
|
25
|
-
export async function getSymbol(repo, symbolId) {
|
|
26
|
-
const index = await
|
|
27
|
-
|
|
28
|
-
throw new Error(`Repository "${repo}" not found. Index it first with index_folder.`);
|
|
29
|
-
}
|
|
93
|
+
export async function getSymbol(repo, symbolId, options) {
|
|
94
|
+
const index = await requireCodeIndex(repo);
|
|
95
|
+
const includeRelated = options?.include_related ?? true;
|
|
30
96
|
const symbol = index.symbols.find((s) => s.id === symbolId);
|
|
31
97
|
if (!symbol)
|
|
32
98
|
return null;
|
|
33
|
-
const source = await extractSource(index.root, symbol.file, symbol.start_line, symbol.end_line);
|
|
99
|
+
const source = await extractSource(index.root, symbol.file, symbol.start_line, symbol.end_line, symbol.start_byte, symbol.end_byte);
|
|
34
100
|
const result = { ...symbol };
|
|
35
101
|
if (source !== undefined) {
|
|
36
102
|
result.source = source;
|
|
37
103
|
}
|
|
38
|
-
|
|
104
|
+
const stripped = stripSymbol(result);
|
|
105
|
+
if (!includeRelated) {
|
|
106
|
+
return { symbol: stripped };
|
|
107
|
+
}
|
|
108
|
+
// Prefetch children for classes/interfaces
|
|
109
|
+
const related = [];
|
|
110
|
+
if (symbol.kind === "class" || symbol.kind === "interface") {
|
|
111
|
+
const children = index.symbols.filter((s) => s.parent === symbol.id);
|
|
112
|
+
for (const child of children.slice(0, 20)) {
|
|
113
|
+
related.push(stripSymbol(child));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const out = { symbol: stripped };
|
|
117
|
+
if (related.length > 0)
|
|
118
|
+
out.related = related;
|
|
119
|
+
return out;
|
|
39
120
|
}
|
|
40
121
|
/**
|
|
41
122
|
* Retrieve multiple symbols by ID with fresh source from disk.
|
|
42
123
|
* Groups reads by file to minimize disk I/O.
|
|
43
124
|
*/
|
|
44
125
|
export async function getSymbols(repo, symbolIds) {
|
|
45
|
-
const index = await
|
|
46
|
-
if (!index) {
|
|
47
|
-
throw new Error(`Repository "${repo}" not found. Index it first with index_folder.`);
|
|
48
|
-
}
|
|
126
|
+
const index = await requireCodeIndex(repo);
|
|
49
127
|
// Build lookup map for requested symbols
|
|
128
|
+
const requestedIds = new Set(symbolIds);
|
|
50
129
|
const symbolMap = new Map();
|
|
51
130
|
for (const sym of index.symbols) {
|
|
52
|
-
if (
|
|
131
|
+
if (requestedIds.has(sym.id)) {
|
|
53
132
|
symbolMap.set(sym.id, sym);
|
|
54
133
|
}
|
|
55
134
|
}
|
|
@@ -66,17 +145,13 @@ export async function getSymbols(repo, symbolIds) {
|
|
|
66
145
|
}
|
|
67
146
|
group.push(sym);
|
|
68
147
|
}
|
|
69
|
-
// Read
|
|
148
|
+
// Read all files in parallel, extract source for all symbols in each file
|
|
70
149
|
const results = new Map();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
catch {
|
|
77
|
-
// File may have been deleted since indexing
|
|
78
|
-
}
|
|
79
|
-
const lines = fileContent?.split("\n");
|
|
150
|
+
const fileEntries = [...byFile.entries()];
|
|
151
|
+
const fileContents = await Promise.all(fileEntries.map(([file]) => readFile(join(index.root, file), "utf-8").catch(() => undefined)));
|
|
152
|
+
for (let i = 0; i < fileEntries.length; i++) {
|
|
153
|
+
const [, symbols] = fileEntries[i];
|
|
154
|
+
const lines = fileContents[i]?.split("\n");
|
|
80
155
|
for (const sym of symbols) {
|
|
81
156
|
const result = { ...sym };
|
|
82
157
|
if (lines) {
|
|
@@ -90,7 +165,7 @@ export async function getSymbols(repo, symbolIds) {
|
|
|
90
165
|
for (const id of symbolIds) {
|
|
91
166
|
const sym = results.get(id);
|
|
92
167
|
if (sym)
|
|
93
|
-
ordered.push(sym);
|
|
168
|
+
ordered.push(stripSymbol(sym));
|
|
94
169
|
}
|
|
95
170
|
return ordered;
|
|
96
171
|
}
|
|
@@ -98,15 +173,176 @@ export async function getSymbols(repo, symbolIds) {
|
|
|
98
173
|
* Find references to a symbol name across indexed files.
|
|
99
174
|
* Matches whole words only using word-boundary regex.
|
|
100
175
|
*/
|
|
176
|
+
/**
|
|
177
|
+
* Batch find references for multiple symbols in one pass.
|
|
178
|
+
* Reads each file once instead of N times — critical for large repos.
|
|
179
|
+
*/
|
|
180
|
+
export async function findReferencesBatch(repo, symbolNames, filePattern) {
|
|
181
|
+
const index = await requireCodeIndex(repo);
|
|
182
|
+
const patterns = symbolNames.map((name) => ({
|
|
183
|
+
name,
|
|
184
|
+
regex: wordBoundaryPattern(name),
|
|
185
|
+
}));
|
|
186
|
+
const fileFilter = filePattern
|
|
187
|
+
? new RegExp(filePattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*"))
|
|
188
|
+
: null;
|
|
189
|
+
const result = {};
|
|
190
|
+
for (const name of symbolNames)
|
|
191
|
+
result[name] = [];
|
|
192
|
+
for (const fileEntry of index.files) {
|
|
193
|
+
if (fileFilter && !fileFilter.test(fileEntry.path))
|
|
194
|
+
continue;
|
|
195
|
+
if (!filePattern && isNoisePath(fileEntry.path))
|
|
196
|
+
continue;
|
|
197
|
+
let content;
|
|
198
|
+
try {
|
|
199
|
+
content = await readFile(join(index.root, fileEntry.path), "utf-8");
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const lines = content.split("\n");
|
|
205
|
+
for (let i = 0; i < lines.length; i++) {
|
|
206
|
+
const line = lines[i];
|
|
207
|
+
if (line === undefined)
|
|
208
|
+
continue;
|
|
209
|
+
for (const { name, regex } of patterns) {
|
|
210
|
+
const refs = result[name];
|
|
211
|
+
if (refs.length >= MAX_REFERENCES)
|
|
212
|
+
continue;
|
|
213
|
+
const match = regex.exec(line);
|
|
214
|
+
if (match) {
|
|
215
|
+
const rawContext = line.trimEnd();
|
|
216
|
+
refs.push({
|
|
217
|
+
file: fileEntry.path,
|
|
218
|
+
line: i + 1,
|
|
219
|
+
col: match.index + 1,
|
|
220
|
+
context: rawContext.length > MAX_CONTEXT_LENGTH
|
|
221
|
+
? rawContext.slice(0, MAX_CONTEXT_LENGTH) + "..."
|
|
222
|
+
: rawContext,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
const SEARCH_TIMEOUT_MS = 30_000;
|
|
231
|
+
/** Directories to exclude from ripgrep reference search */
|
|
232
|
+
const RG_EXCLUDE_DIRS = [
|
|
233
|
+
"node_modules", ".git", ".next", "dist", ".codesift", "coverage",
|
|
234
|
+
".playwright-mcp", "__pycache__", "__snapshots__",
|
|
235
|
+
];
|
|
236
|
+
/** Detect whether `rg` (ripgrep) is available. Cached at module level. */
|
|
237
|
+
let rgAvailable = null;
|
|
238
|
+
function hasRipgrep() {
|
|
239
|
+
if (rgAvailable !== null)
|
|
240
|
+
return rgAvailable;
|
|
241
|
+
try {
|
|
242
|
+
execFileSync("rg", ["--version"], { stdio: "pipe", timeout: 2000 });
|
|
243
|
+
rgAvailable = true;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
rgAvailable = false;
|
|
247
|
+
}
|
|
248
|
+
return rgAvailable;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Find references using ripgrep with word-boundary matching.
|
|
252
|
+
* Returns compact `file:line: context` string when results ≤ threshold.
|
|
253
|
+
*/
|
|
254
|
+
function findReferencesWithRipgrep(root, symbolName, maxResults, filePattern) {
|
|
255
|
+
const args = [
|
|
256
|
+
"-n", "--no-heading", "-w",
|
|
257
|
+
"--max-columns", String(MAX_CONTEXT_LENGTH),
|
|
258
|
+
"--max-columns-preview",
|
|
259
|
+
"--max-count", String(Math.min(maxResults * 2, 5000)),
|
|
260
|
+
];
|
|
261
|
+
// Exclude noise dirs
|
|
262
|
+
for (const dir of RG_EXCLUDE_DIRS) {
|
|
263
|
+
args.push("--glob", `!${dir}`);
|
|
264
|
+
}
|
|
265
|
+
// Exclude noise extensions
|
|
266
|
+
for (const ext of [".snap", ".lock", ".map", ".svg", ".png", ".jpg", ".ico", ".woff", ".woff2", ".md", ".json", ".yaml", ".yml", ".toml", ".css", ".scss", ".html"]) {
|
|
267
|
+
args.push("--glob", `!*${ext}`);
|
|
268
|
+
}
|
|
269
|
+
if (filePattern) {
|
|
270
|
+
args.push("--glob", filePattern);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
// Default to code files only (matches what agent would grep for)
|
|
274
|
+
args.push("--type-add", "code:*.{ts,tsx,js,jsx,py,go,rs,java,rb,php,vue,svelte}");
|
|
275
|
+
args.push("--type", "code");
|
|
276
|
+
}
|
|
277
|
+
args.push("--", symbolName, root);
|
|
278
|
+
let stdout;
|
|
279
|
+
try {
|
|
280
|
+
stdout = execFileSync("rg", args, {
|
|
281
|
+
encoding: "utf-8",
|
|
282
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
283
|
+
timeout: SEARCH_TIMEOUT_MS,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
if (err && typeof err === "object" && "status" in err) {
|
|
288
|
+
if (err.status === 1)
|
|
289
|
+
return []; // no matches
|
|
290
|
+
if ("stdout" in err && typeof err.stdout === "string") {
|
|
291
|
+
stdout = err.stdout;
|
|
292
|
+
if (!stdout)
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const rootPrefix = root.endsWith("/") ? root : root + "/";
|
|
304
|
+
const lines = stdout.split("\n").filter(Boolean);
|
|
305
|
+
const refs = [];
|
|
306
|
+
for (const rawLine of lines) {
|
|
307
|
+
if (refs.length >= maxResults)
|
|
308
|
+
break;
|
|
309
|
+
const match = rawLine.match(/^(.+?):(\d+):(.*)/);
|
|
310
|
+
if (!match || !match[1] || !match[2] || match[3] === undefined)
|
|
311
|
+
continue;
|
|
312
|
+
const absPath = match[1];
|
|
313
|
+
const relPath = absPath.startsWith(rootPrefix) ? absPath.slice(rootPrefix.length) : absPath;
|
|
314
|
+
if (isNoisePath(relPath))
|
|
315
|
+
continue;
|
|
316
|
+
refs.push({
|
|
317
|
+
file: relPath,
|
|
318
|
+
line: parseInt(match[2], 10),
|
|
319
|
+
context: match[3].length > MAX_CONTEXT_LENGTH ? match[3].slice(0, MAX_CONTEXT_LENGTH) + "..." : match[3],
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return refs;
|
|
323
|
+
}
|
|
101
324
|
export async function findReferences(repo, symbolName, filePattern) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
325
|
+
// Try LSP first (type-safe, no false positives)
|
|
326
|
+
const lspRefs = await findReferencesLsp(repo, symbolName);
|
|
327
|
+
if (lspRefs !== null)
|
|
328
|
+
return lspRefs;
|
|
329
|
+
// Use ripgrep when available (10x+ faster than Node.js file walk)
|
|
330
|
+
if (hasRipgrep()) {
|
|
331
|
+
const index = await requireCodeIndex(repo);
|
|
332
|
+
const result = findReferencesWithRipgrep(index.root, symbolName, MAX_REFERENCES, filePattern);
|
|
333
|
+
// ripgrep helper may return compact string; convert back to Reference[]
|
|
334
|
+
if (typeof result === "string") {
|
|
335
|
+
return result.split("\n").filter(Boolean).map((line) => {
|
|
336
|
+
const m = line.match(/^(.+?):(\d+): (.*)/);
|
|
337
|
+
return m ? { file: m[1], line: parseInt(m[2], 10), context: m[3] } : { file: "", line: 0, context: line };
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return result;
|
|
105
341
|
}
|
|
106
|
-
//
|
|
107
|
-
const
|
|
108
|
-
const pattern =
|
|
109
|
-
|
|
342
|
+
// Node.js fallback
|
|
343
|
+
const index = await requireCodeIndex(repo);
|
|
344
|
+
const pattern = wordBoundaryPattern(symbolName);
|
|
345
|
+
const searchStart = Date.now();
|
|
110
346
|
const fileFilter = filePattern
|
|
111
347
|
? new RegExp(filePattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*"))
|
|
112
348
|
: null;
|
|
@@ -114,14 +350,18 @@ export async function findReferences(repo, symbolName, filePattern) {
|
|
|
114
350
|
for (const fileEntry of index.files) {
|
|
115
351
|
if (refs.length >= MAX_REFERENCES)
|
|
116
352
|
break;
|
|
353
|
+
if (Date.now() - searchStart > SEARCH_TIMEOUT_MS)
|
|
354
|
+
break;
|
|
117
355
|
if (fileFilter && !fileFilter.test(fileEntry.path))
|
|
118
356
|
continue;
|
|
357
|
+
if (!filePattern && isNoisePath(fileEntry.path))
|
|
358
|
+
continue;
|
|
119
359
|
let content;
|
|
120
360
|
try {
|
|
121
361
|
content = await readFile(join(index.root, fileEntry.path), "utf-8");
|
|
122
362
|
}
|
|
123
363
|
catch {
|
|
124
|
-
continue;
|
|
364
|
+
continue;
|
|
125
365
|
}
|
|
126
366
|
const lines = content.split("\n");
|
|
127
367
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -136,7 +376,6 @@ export async function findReferences(repo, symbolName, filePattern) {
|
|
|
136
376
|
refs.push({
|
|
137
377
|
file: fileEntry.path,
|
|
138
378
|
line: i + 1,
|
|
139
|
-
col: match.index + 1,
|
|
140
379
|
context: rawContext.length > MAX_CONTEXT_LENGTH
|
|
141
380
|
? rawContext.slice(0, MAX_CONTEXT_LENGTH) + "..."
|
|
142
381
|
: rawContext,
|
|
@@ -146,27 +385,322 @@ export async function findReferences(repo, symbolName, filePattern) {
|
|
|
146
385
|
}
|
|
147
386
|
return refs;
|
|
148
387
|
}
|
|
388
|
+
/** Format references as compact string for MCP output. Groups by file to avoid repeating paths. */
|
|
389
|
+
export function formatRefsCompact(refs) {
|
|
390
|
+
if (refs.length === 0)
|
|
391
|
+
return "";
|
|
392
|
+
// Group by file
|
|
393
|
+
const groups = new Map();
|
|
394
|
+
for (const r of refs) {
|
|
395
|
+
let g = groups.get(r.file);
|
|
396
|
+
if (!g) {
|
|
397
|
+
g = [];
|
|
398
|
+
groups.set(r.file, g);
|
|
399
|
+
}
|
|
400
|
+
g.push(` ${r.line}: ${r.context}`);
|
|
401
|
+
}
|
|
402
|
+
if (groups.size === refs.length) {
|
|
403
|
+
// Each file has 1 ref — flat is fine
|
|
404
|
+
return refs.map((r) => `${r.file}:${r.line}: ${r.context}`).join("\n");
|
|
405
|
+
}
|
|
406
|
+
const parts = [];
|
|
407
|
+
for (const [file, lines] of groups) {
|
|
408
|
+
parts.push(`${file}\n${lines.join("\n")}`);
|
|
409
|
+
}
|
|
410
|
+
return parts.join("\n");
|
|
411
|
+
}
|
|
412
|
+
/** Format a CodeSymbol as compact text: header line + source. ~70% less tokens than JSON. */
|
|
413
|
+
export function formatSymbolCompact(sym) {
|
|
414
|
+
const loc = `${sym.file}:${sym.start_line}-${sym.end_line}`;
|
|
415
|
+
const sig = sym.signature ? ` ${sym.signature}` : "";
|
|
416
|
+
const header = `${loc} ${sym.kind} ${sym.name}${sig}`;
|
|
417
|
+
if (!sym.source)
|
|
418
|
+
return header;
|
|
419
|
+
return `${header}\n${sym.source}`;
|
|
420
|
+
}
|
|
421
|
+
/** Format multiple CodeSymbols as compact text, separated by blank lines. */
|
|
422
|
+
export function formatSymbolsCompact(syms) {
|
|
423
|
+
return syms.map(formatSymbolCompact).join("\n\n");
|
|
424
|
+
}
|
|
425
|
+
/** Format ContextBundle as compact text. */
|
|
426
|
+
export function formatBundleCompact(bundle) {
|
|
427
|
+
const parts = [];
|
|
428
|
+
parts.push(formatSymbolCompact(bundle.symbol));
|
|
429
|
+
if (bundle.imports.length > 0) {
|
|
430
|
+
parts.push(`\n--- imports ---\n${bundle.imports.join("\n")}`);
|
|
431
|
+
}
|
|
432
|
+
if (bundle.siblings.length > 0) {
|
|
433
|
+
const sibLines = bundle.siblings.map((s) => ` ${s.kind} ${s.name} :${s.start_line}-${s.end_line}`);
|
|
434
|
+
parts.push(`\n--- siblings ---\n${sibLines.join("\n")}`);
|
|
435
|
+
}
|
|
436
|
+
if (bundle.types_used.length > 0) {
|
|
437
|
+
parts.push(`\n--- types used ---\n${bundle.types_used.join(", ")}`);
|
|
438
|
+
}
|
|
439
|
+
return parts.join("");
|
|
440
|
+
}
|
|
149
441
|
/**
|
|
150
442
|
* Search for a symbol by query and return it with full source.
|
|
151
443
|
* Optionally includes references across the codebase.
|
|
152
444
|
*/
|
|
153
445
|
export async function findAndShow(repo, query, includeRefs) {
|
|
154
|
-
const bm25Index = await
|
|
155
|
-
if (!bm25Index) {
|
|
156
|
-
throw new Error(`Repository "${repo}" not found. Index it first with index_folder.`);
|
|
157
|
-
}
|
|
446
|
+
const bm25Index = await requireBM25Index(repo);
|
|
158
447
|
const config = loadConfig();
|
|
159
448
|
const results = searchBM25(bm25Index, query, 1, config.bm25FieldWeights);
|
|
160
449
|
const topResult = results[0];
|
|
161
450
|
if (!topResult)
|
|
162
451
|
return null;
|
|
163
|
-
const
|
|
164
|
-
if (!
|
|
452
|
+
const fullResult = await getSymbol(repo, topResult.symbol.id, { include_related: false });
|
|
453
|
+
if (!fullResult)
|
|
165
454
|
return null;
|
|
455
|
+
const fullSymbol = fullResult.symbol;
|
|
166
456
|
if (includeRefs) {
|
|
167
457
|
const references = await findReferences(repo, fullSymbol.name);
|
|
168
458
|
return { symbol: fullSymbol, references };
|
|
169
459
|
}
|
|
170
460
|
return { symbol: fullSymbol };
|
|
171
461
|
}
|
|
462
|
+
/**
|
|
463
|
+
* Extract full import lines from file source.
|
|
464
|
+
*/
|
|
465
|
+
function extractImportLines(source) {
|
|
466
|
+
const lines = source.split("\n");
|
|
467
|
+
return lines.filter((line) => {
|
|
468
|
+
const trimmed = line.trim();
|
|
469
|
+
return trimmed.startsWith("import ") || (trimmed.startsWith("const ") && trimmed.includes("require("));
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Get a symbol with its file's imports and sibling symbols in one call.
|
|
474
|
+
* Saves 2-3 round-trips vs get_symbol + search_text(imports) + get_file_outline.
|
|
475
|
+
*/
|
|
476
|
+
export async function getContextBundle(repo, symbolName) {
|
|
477
|
+
const bm25Index = await requireBM25Index(repo);
|
|
478
|
+
const config = loadConfig();
|
|
479
|
+
const results = searchBM25(bm25Index, symbolName, 1, config.bm25FieldWeights);
|
|
480
|
+
const topResult = results[0];
|
|
481
|
+
if (!topResult)
|
|
482
|
+
return null;
|
|
483
|
+
const index = await requireCodeIndex(repo);
|
|
484
|
+
// Get full symbol with source
|
|
485
|
+
const fullResult = await getSymbol(repo, topResult.symbol.id, { include_related: false });
|
|
486
|
+
if (!fullResult)
|
|
487
|
+
return null;
|
|
488
|
+
const fullSymbol = fullResult.symbol;
|
|
489
|
+
// Read the file to extract imports
|
|
490
|
+
let fileSource;
|
|
491
|
+
try {
|
|
492
|
+
fileSource = await readFile(join(index.root, fullSymbol.file), "utf-8");
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return { symbol: fullSymbol, imports: [], siblings: [], types_used: [] };
|
|
496
|
+
}
|
|
497
|
+
const imports = extractImportLines(fileSource);
|
|
498
|
+
// Get sibling symbols (other symbols in the same file)
|
|
499
|
+
const siblings = index.symbols
|
|
500
|
+
.filter((s) => s.file === fullSymbol.file && s.id !== fullSymbol.id)
|
|
501
|
+
.map((s) => ({
|
|
502
|
+
name: s.name,
|
|
503
|
+
kind: s.kind,
|
|
504
|
+
start_line: s.start_line,
|
|
505
|
+
end_line: s.end_line,
|
|
506
|
+
}));
|
|
507
|
+
// Extract type names used in the symbol's source
|
|
508
|
+
const typesUsed = extractTypesUsed(fullSymbol.source ?? "", index.symbols);
|
|
509
|
+
return { symbol: fullSymbol, imports, siblings, types_used: typesUsed };
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Extract type/interface names referenced in source by matching against known symbols.
|
|
513
|
+
*/
|
|
514
|
+
function extractTypesUsed(source, allSymbols) {
|
|
515
|
+
const typeNames = allSymbols
|
|
516
|
+
.filter((s) => (s.kind === "interface" || s.kind === "type" || s.kind === "enum") && s.name.length >= 3)
|
|
517
|
+
.map((s) => s.name);
|
|
518
|
+
if (typeNames.length === 0)
|
|
519
|
+
return [];
|
|
520
|
+
// Single combined regex instead of N separate tests (O(n) vs O(n*m))
|
|
521
|
+
const escaped = typeNames.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
522
|
+
const combined = new RegExp(`\\b(${escaped.join("|")})\\b`, "g");
|
|
523
|
+
const used = new Set();
|
|
524
|
+
let m;
|
|
525
|
+
while ((m = combined.exec(source)) !== null) {
|
|
526
|
+
used.add(m[1]);
|
|
527
|
+
}
|
|
528
|
+
return [...used].sort();
|
|
529
|
+
}
|
|
530
|
+
// Kinds that are typically exported and should have external references
|
|
531
|
+
const EXPORTABLE_KINDS = new Set([
|
|
532
|
+
"function", "class", "interface", "type", "variable", "constant", "enum",
|
|
533
|
+
]);
|
|
534
|
+
/**
|
|
535
|
+
* Collect top-level symbols of exportable kinds, filtered by test/pattern options.
|
|
536
|
+
*/
|
|
537
|
+
function collectExportedSymbols(symbols, options) {
|
|
538
|
+
return symbols.filter((s) => {
|
|
539
|
+
if (!EXPORTABLE_KINDS.has(s.kind))
|
|
540
|
+
return false;
|
|
541
|
+
if (s.parent)
|
|
542
|
+
return false;
|
|
543
|
+
if (!options.includeTests && isTestFile(s.file))
|
|
544
|
+
return false;
|
|
545
|
+
if (options.filePattern && !s.file.includes(options.filePattern))
|
|
546
|
+
return false;
|
|
547
|
+
if (s.name.length < 3)
|
|
548
|
+
return false;
|
|
549
|
+
if (s.kind === "variable" && s.name === "default")
|
|
550
|
+
return false;
|
|
551
|
+
return true;
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Find potentially dead code: exported symbols with 0 references outside their own file.
|
|
556
|
+
* Scans all indexed files for word-boundary matches of each exported symbol name.
|
|
557
|
+
*/
|
|
558
|
+
export async function findDeadCode(repo, options) {
|
|
559
|
+
const index = await requireCodeIndex(repo);
|
|
560
|
+
const includeTests = options?.include_tests ?? false;
|
|
561
|
+
const filePattern = options?.file_pattern;
|
|
562
|
+
const exportedSymbols = collectExportedSymbols(index.symbols, { includeTests, filePattern });
|
|
563
|
+
const frameworks = detectFrameworks(index);
|
|
564
|
+
// Read non-test files into memory for scanning (capped to prevent OOM on large repos)
|
|
565
|
+
const MAX_SCAN_FILES = 2000;
|
|
566
|
+
const fileContents = new Map();
|
|
567
|
+
for (const file of index.files) {
|
|
568
|
+
if (fileContents.size >= MAX_SCAN_FILES)
|
|
569
|
+
break;
|
|
570
|
+
if (!includeTests && isTestFile(file.path))
|
|
571
|
+
continue;
|
|
572
|
+
try {
|
|
573
|
+
fileContents.set(file.path, await readFile(join(index.root, file.path), "utf-8"));
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
// File may have been deleted
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const candidates = [];
|
|
580
|
+
for (const sym of exportedSymbols) {
|
|
581
|
+
if (candidates.length >= MAX_DEAD_CODE_RESULTS)
|
|
582
|
+
break;
|
|
583
|
+
if (isFrameworkEntryPoint(sym, frameworks))
|
|
584
|
+
continue;
|
|
585
|
+
const pattern = wordBoundaryPattern(sym.name);
|
|
586
|
+
let externalRefs = 0;
|
|
587
|
+
for (const [filePath, content] of fileContents) {
|
|
588
|
+
if (filePath === sym.file)
|
|
589
|
+
continue; // Skip own file
|
|
590
|
+
if (pattern.test(content)) {
|
|
591
|
+
externalRefs++;
|
|
592
|
+
break; // One external ref is enough — not dead
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (externalRefs === 0) {
|
|
596
|
+
candidates.push({
|
|
597
|
+
name: sym.name,
|
|
598
|
+
kind: sym.kind,
|
|
599
|
+
file: sym.file,
|
|
600
|
+
start_line: sym.start_line,
|
|
601
|
+
end_line: sym.end_line,
|
|
602
|
+
reason: "exported but no references found outside defining file",
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return {
|
|
607
|
+
candidates,
|
|
608
|
+
scanned_symbols: exportedSymbols.length,
|
|
609
|
+
scanned_files: fileContents.size,
|
|
610
|
+
...(candidates.length >= MAX_DEAD_CODE_RESULTS ? { truncated: true } : {}),
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
// Unused import detection
|
|
615
|
+
// ---------------------------------------------------------------------------
|
|
616
|
+
const MAX_UNUSED_IMPORTS = 200;
|
|
617
|
+
/**
|
|
618
|
+
* Find imports whose imported names are never referenced in the file body.
|
|
619
|
+
* Supports ES module named imports: import { A, B } from '...'
|
|
620
|
+
*/
|
|
621
|
+
export async function findUnusedImports(repo, options) {
|
|
622
|
+
const index = await requireCodeIndex(repo);
|
|
623
|
+
const includeTests = options?.include_tests ?? false;
|
|
624
|
+
const unused = [];
|
|
625
|
+
let scannedFiles = 0;
|
|
626
|
+
for (const file of index.files) {
|
|
627
|
+
if (unused.length >= MAX_UNUSED_IMPORTS)
|
|
628
|
+
break;
|
|
629
|
+
if (!includeTests && isTestFile(file.path))
|
|
630
|
+
continue;
|
|
631
|
+
if (options?.file_pattern && !file.path.includes(options.file_pattern))
|
|
632
|
+
continue;
|
|
633
|
+
// Only analyze JS/TS files
|
|
634
|
+
if (!/\.(ts|tsx|js|jsx|mjs)$/.test(file.path))
|
|
635
|
+
continue;
|
|
636
|
+
let source;
|
|
637
|
+
try {
|
|
638
|
+
source = await readFile(join(index.root, file.path), "utf-8");
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
scannedFiles++;
|
|
644
|
+
const lines = source.split("\n");
|
|
645
|
+
// Find named import lines: import { A, B, C } from '...'
|
|
646
|
+
// Also: import A from '...' and import * as A from '...'
|
|
647
|
+
const importRegex = /^import\s+(?:type\s+)?(?:\{([^}]+)\}|(\*\s+as\s+\w+)|(\w+)).*from\s+['"][^'"]+['"]/;
|
|
648
|
+
for (let i = 0; i < lines.length; i++) {
|
|
649
|
+
const line = lines[i].trim();
|
|
650
|
+
if (!line.startsWith("import "))
|
|
651
|
+
continue;
|
|
652
|
+
// Stop scanning imports when we hit non-import code
|
|
653
|
+
if (i > 0 && !line.startsWith("import") && !line.startsWith("//") && !line.startsWith("/*") && line.length > 0 && !lines[i].trim().startsWith("*") && !lines[i].trim().startsWith("}")) {
|
|
654
|
+
// Could be multi-line import continuation, keep going
|
|
655
|
+
}
|
|
656
|
+
const match = importRegex.exec(line);
|
|
657
|
+
if (!match)
|
|
658
|
+
continue;
|
|
659
|
+
const names = [];
|
|
660
|
+
if (match[1]) {
|
|
661
|
+
// Named imports: { A, B as C, type D }
|
|
662
|
+
for (const part of match[1].split(",")) {
|
|
663
|
+
const trimmed = part.trim().replace(/^type\s+/, "");
|
|
664
|
+
if (!trimmed)
|
|
665
|
+
continue;
|
|
666
|
+
// Handle "A as B" — the local name is B
|
|
667
|
+
const asMatch = /(\w+)\s+as\s+(\w+)/.exec(trimmed);
|
|
668
|
+
names.push(asMatch ? asMatch[2] : trimmed);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
else if (match[2]) {
|
|
672
|
+
// Namespace import: * as A
|
|
673
|
+
const nsMatch = /\*\s+as\s+(\w+)/.exec(match[2]);
|
|
674
|
+
if (nsMatch)
|
|
675
|
+
names.push(nsMatch[1]);
|
|
676
|
+
}
|
|
677
|
+
else if (match[3]) {
|
|
678
|
+
// Default import: import A
|
|
679
|
+
names.push(match[3]);
|
|
680
|
+
}
|
|
681
|
+
// Check each imported name against rest of file
|
|
682
|
+
const bodyAfterImports = lines.slice(i + 1).join("\n");
|
|
683
|
+
for (const name of names) {
|
|
684
|
+
if (name.length < 2)
|
|
685
|
+
continue;
|
|
686
|
+
const nameRegex = new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
|
|
687
|
+
if (!nameRegex.test(bodyAfterImports)) {
|
|
688
|
+
unused.push({
|
|
689
|
+
file: file.path,
|
|
690
|
+
line: i + 1,
|
|
691
|
+
import_text: line,
|
|
692
|
+
imported_name: name,
|
|
693
|
+
});
|
|
694
|
+
if (unused.length >= MAX_UNUSED_IMPORTS)
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
unused,
|
|
702
|
+
scanned_files: scannedFiles,
|
|
703
|
+
...(unused.length >= MAX_UNUSED_IMPORTS ? { truncated: true } : {}),
|
|
704
|
+
};
|
|
705
|
+
}
|
|
172
706
|
//# sourceMappingURL=symbol-tools.js.map
|