@maxgfr/codeindex 2.11.1 → 2.13.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/README.md +5 -1
- package/docs/MIGRATION.md +39 -1
- package/docs/SEMANTIC.md +249 -0
- package/package.json +3 -2
- package/scripts/engine.d.mts +25 -6
- package/scripts/engine.mjs +179 -76
- package/src/ast/extract.ts +7 -5
- package/src/callers.ts +0 -0
- package/src/embed/model.ts +73 -25
- package/src/engine-cli.ts +31 -21
- package/src/engine.ts +6 -2
- package/src/extract/code.ts +9 -5
- package/src/lang/common.ts +18 -7
- package/src/lang/js-ts.ts +1 -1
- package/src/mcp.ts +111 -15
- package/src/scan.ts +9 -1
- package/src/types.ts +13 -4
- package/src/walk.ts +15 -2
package/scripts/engine.mjs
CHANGED
|
@@ -14,9 +14,9 @@ var ENGINE_VERSION, SCHEMA_VERSION, EXTRACTOR_VERSION;
|
|
|
14
14
|
var init_types = __esm({
|
|
15
15
|
"src/types.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
ENGINE_VERSION = "2.
|
|
17
|
+
ENGINE_VERSION = "2.13.0";
|
|
18
18
|
SCHEMA_VERSION = 4;
|
|
19
|
-
EXTRACTOR_VERSION =
|
|
19
|
+
EXTRACTOR_VERSION = 10;
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
22
|
|
|
@@ -320,6 +320,7 @@ function walk(root, opts = {}) {
|
|
|
320
320
|
const maxFileBytes = opts.maxFileBytes ?? 1024 * 1024;
|
|
321
321
|
const maxFiles = opts.maxFiles ?? DEFAULT_MAX_FILES;
|
|
322
322
|
const useGitignore = opts.gitignore !== false;
|
|
323
|
+
const ignoreDirs = opts.ignoreDirs ? new Set(opts.ignoreDirs) : IGNORE_DIRS;
|
|
323
324
|
const out2 = [];
|
|
324
325
|
let capped = false;
|
|
325
326
|
let excluded = 0;
|
|
@@ -368,7 +369,7 @@ function walk(root, opts = {}) {
|
|
|
368
369
|
continue;
|
|
369
370
|
}
|
|
370
371
|
if (st.isDirectory()) {
|
|
371
|
-
if (
|
|
372
|
+
if (ignoreDirs.has(name2)) continue;
|
|
372
373
|
if (isLink) continue;
|
|
373
374
|
if (useGitignore && rules.length && isIgnored(rules, rel, true)) continue;
|
|
374
375
|
stack.push({ dir: abs, rel, rules });
|
|
@@ -463,6 +464,7 @@ var init_walk = __esm({
|
|
|
463
464
|
".cache",
|
|
464
465
|
"tmp",
|
|
465
466
|
".ultraindex",
|
|
467
|
+
".codeindex",
|
|
466
468
|
"Pods",
|
|
467
469
|
"DerivedData",
|
|
468
470
|
".terraform",
|
|
@@ -737,8 +739,8 @@ function extractReexports(rel, content, localSymbols) {
|
|
|
737
739
|
const out2 = [];
|
|
738
740
|
const seen = /* @__PURE__ */ new Set();
|
|
739
741
|
const lineAt = (idx) => content.slice(0, idx).split(/\r?\n/).length;
|
|
740
|
-
const
|
|
741
|
-
for (const s of localSymbols) if (!
|
|
742
|
+
const localDeclOf = /* @__PURE__ */ new Map();
|
|
743
|
+
for (const s of localSymbols) if (!localDeclOf.has(s.name)) localDeclOf.set(s.name, s);
|
|
742
744
|
const named = /export\s*\{([\s\S]*?)\}\s*(?:from\s*['"]([^'"]+)['"])?\s*;?/g;
|
|
743
745
|
let m;
|
|
744
746
|
while ((m = named.exec(content)) && out2.length < 60) {
|
|
@@ -750,12 +752,13 @@ function extractReexports(rel, content, localSymbols) {
|
|
|
750
752
|
const name2 = as ? as[2] : p;
|
|
751
753
|
if (!/^[A-Za-z_$][\w$]*$/.test(name2) || name2 === "default" || seen.has(name2)) continue;
|
|
752
754
|
seen.add(name2);
|
|
753
|
-
const
|
|
755
|
+
const decl = !from ? localDeclOf.get(orig) : void 0;
|
|
754
756
|
out2.push({
|
|
755
757
|
name: name2,
|
|
756
|
-
kind:
|
|
758
|
+
kind: decl?.kind ?? "reexport",
|
|
757
759
|
file: rel,
|
|
758
|
-
line: lineAt(m.index),
|
|
760
|
+
line: decl ? decl.line : lineAt(m.index),
|
|
761
|
+
...decl?.endLine !== void 0 ? { endLine: decl.endLine } : {},
|
|
759
762
|
signature: from ? `export { ${name2} } from "${from}"` : `export { ${name2} }`,
|
|
760
763
|
exported: true,
|
|
761
764
|
lang
|
|
@@ -900,7 +903,7 @@ var init_js_ts = __esm({
|
|
|
900
903
|
RULES = [
|
|
901
904
|
{ re: /^\s*export\s+(?:async\s+)?function\s+(?<name>[\w$]+)/, kind: "function", exported: true },
|
|
902
905
|
{ re: /^\s*export\s+default\s+(?:async\s+)?function\s+(?<name>[\w$]+)/, kind: "function", exported: true },
|
|
903
|
-
{ re: /^\s*export\s+default\s+(?:abstract\s+)?class\s+(?<name>[\w$]+)/, kind: "class", exported: true },
|
|
906
|
+
{ re: /^\s*export\s+default\s+(?:abstract\s+)?class\s+(?!extends\b)(?<name>[\w$]+)/, kind: "class", exported: true },
|
|
904
907
|
{ re: /^\s*(?:async\s+)?function\s+(?<name>[\w$]+)/, kind: "function", exported: false },
|
|
905
908
|
{ re: /^\s*export\s+(?:abstract\s+)?class\s+(?<name>[\w$]+)/, kind: "class", exported: true },
|
|
906
909
|
{ re: /^\s*(?:abstract\s+)?class\s+(?<name>[\w$]+)/, kind: "class", exported: false },
|
|
@@ -5717,7 +5720,7 @@ function readReceiver(node) {
|
|
|
5717
5720
|
const name2 = obj ? readName(obj) : void 0;
|
|
5718
5721
|
return name2 && /^[A-Za-z_]\w*$/.test(name2) ? name2 : void 0;
|
|
5719
5722
|
}
|
|
5720
|
-
function collectCalls(root, spec) {
|
|
5723
|
+
function collectCalls(root, spec, maxCalls = MAX_CALLS) {
|
|
5721
5724
|
if (!spec.calls) return [];
|
|
5722
5725
|
const out2 = [];
|
|
5723
5726
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -5748,7 +5751,7 @@ function collectCalls(root, spec) {
|
|
|
5748
5751
|
};
|
|
5749
5752
|
visit(root);
|
|
5750
5753
|
out2.sort((a, b) => byStr(a.name, b.name) || a.line - b.line);
|
|
5751
|
-
return out2.slice(0,
|
|
5754
|
+
return out2.slice(0, maxCalls);
|
|
5752
5755
|
}
|
|
5753
5756
|
function collectImportedNames(root, spec) {
|
|
5754
5757
|
if (!spec.imports?.import_statement) return [];
|
|
@@ -5775,7 +5778,7 @@ function collectImportedNames(root, spec) {
|
|
|
5775
5778
|
visit(root);
|
|
5776
5779
|
return [...found].sort(byStr).slice(0, MAX_IMPORTED_NAMES);
|
|
5777
5780
|
}
|
|
5778
|
-
function extractAst(rel, ext, content) {
|
|
5781
|
+
function extractAst(rel, ext, content, opts = {}) {
|
|
5779
5782
|
const key = grammarKeyForExt(ext);
|
|
5780
5783
|
if (!key || !grammarReady(key)) return void 0;
|
|
5781
5784
|
const spec = SPECS[key];
|
|
@@ -5961,7 +5964,7 @@ function extractAst(rel, ext, content) {
|
|
|
5961
5964
|
}
|
|
5962
5965
|
const refs = collectImports(root, spec);
|
|
5963
5966
|
const idents = collectRefIdents(root, new Set(symbols.map((s) => s.name)));
|
|
5964
|
-
const calls = collectCalls(root, spec);
|
|
5967
|
+
const calls = collectCalls(root, spec, opts.maxCalls);
|
|
5965
5968
|
const importedNames = collectImportedNames(root, spec);
|
|
5966
5969
|
let pkg;
|
|
5967
5970
|
if (spec.lang === "java") {
|
|
@@ -6397,12 +6400,12 @@ function extractImports(ext, content) {
|
|
|
6397
6400
|
}
|
|
6398
6401
|
return [...specs].map((spec) => ({ kind: "import", spec }));
|
|
6399
6402
|
}
|
|
6400
|
-
function collectCallsRegex(content, symbols = []) {
|
|
6403
|
+
function collectCallsRegex(content, symbols = [], maxCalls = 512) {
|
|
6401
6404
|
const out2 = /* @__PURE__ */ new Map();
|
|
6402
6405
|
const ownDefLines = new Set(symbols.map((s) => `${s.name} ${s.line}`));
|
|
6403
6406
|
const lines = content.split("\n");
|
|
6404
6407
|
const CALL_RE = /(?:\bnew\s+)?(?:([A-Za-z_$][\w$]*)\s*\.\s*)?([A-Za-z_$][\w$]*)\s*\(/g;
|
|
6405
|
-
for (let i2 = 0; i2 < lines.length && out2.size <
|
|
6408
|
+
for (let i2 = 0; i2 < lines.length && out2.size < maxCalls; i2++) {
|
|
6406
6409
|
const line = lines[i2];
|
|
6407
6410
|
const trimmed = line.trimStart();
|
|
6408
6411
|
if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*")) continue;
|
|
@@ -6417,7 +6420,7 @@ function collectCallsRegex(content, symbols = []) {
|
|
|
6417
6420
|
CALL_RE.lastIndex = 0;
|
|
6418
6421
|
let m;
|
|
6419
6422
|
const fallbackExcluded = /* @__PURE__ */ new Set();
|
|
6420
|
-
while ((m = CALL_RE.exec(line)) !== null && out2.size <
|
|
6423
|
+
while ((m = CALL_RE.exec(line)) !== null && out2.size < maxCalls) {
|
|
6421
6424
|
const receiver = m[1];
|
|
6422
6425
|
const name2 = m[2];
|
|
6423
6426
|
if (name2.length < 2 || CALL_KEYWORDS.has(name2)) continue;
|
|
@@ -6434,8 +6437,8 @@ function collectCallsRegex(content, symbols = []) {
|
|
|
6434
6437
|
}
|
|
6435
6438
|
return [...out2.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : a.line - b.line);
|
|
6436
6439
|
}
|
|
6437
|
-
function extractCode(rel, ext, content) {
|
|
6438
|
-
const ast = extractAst(rel, ext, content);
|
|
6440
|
+
function extractCode(rel, ext, content, opts = {}) {
|
|
6441
|
+
const ast = extractAst(rel, ext, content, { maxCalls: opts.maxCallsPerFile });
|
|
6439
6442
|
const symbols = (ast ? ast.symbols : extractSymbols(rel, ext, content)).slice(0, 400);
|
|
6440
6443
|
const known = new Set(symbols.map((s) => s.name));
|
|
6441
6444
|
const reexports = extractReexports(rel, content, symbols).filter((s) => !known.has(s.name));
|
|
@@ -6451,7 +6454,7 @@ function extractCode(rel, ext, content) {
|
|
|
6451
6454
|
// collector otherwise, so caller indexes exist without the wasm sidecar.
|
|
6452
6455
|
// `symbols` (this file's own regex-extracted defs) lets the collector
|
|
6453
6456
|
// exclude a definition's own name+line from its call candidates.
|
|
6454
|
-
calls: ast ? ast.calls : collectCallsRegex(content, symbols),
|
|
6457
|
+
calls: ast ? ast.calls : collectCallsRegex(content, symbols, opts.maxCallsPerFile),
|
|
6455
6458
|
importedNames: ast?.importedNames
|
|
6456
6459
|
};
|
|
6457
6460
|
}
|
|
@@ -6523,7 +6526,8 @@ function scanRepo(root, opts = {}) {
|
|
|
6523
6526
|
const { files: walked, capped, excluded } = walk(root, {
|
|
6524
6527
|
maxFileBytes: opts.maxBytes,
|
|
6525
6528
|
maxFiles: opts.maxFiles,
|
|
6526
|
-
gitignore: opts.gitignore
|
|
6529
|
+
gitignore: opts.gitignore,
|
|
6530
|
+
ignoreDirs: opts.ignoreDirs
|
|
6527
6531
|
});
|
|
6528
6532
|
const outPrefix = opts.out ? opts.out.replace(/\/+$/, "") + "/" : null;
|
|
6529
6533
|
const files = [];
|
|
@@ -6572,7 +6576,7 @@ function scanRepo(root, opts = {}) {
|
|
|
6572
6576
|
} else if (kind === "doc") {
|
|
6573
6577
|
record.title = basename(f.rel);
|
|
6574
6578
|
} else if (kind === "code") {
|
|
6575
|
-
const code = extractCode(f.rel, f.ext, content);
|
|
6579
|
+
const code = extractCode(f.rel, f.ext, content, { maxCallsPerFile: opts.maxCallsPerFile });
|
|
6576
6580
|
record.title = basename(f.rel);
|
|
6577
6581
|
record.summary = code.summary;
|
|
6578
6582
|
record.symbols = code.symbols;
|
|
@@ -7731,8 +7735,11 @@ function buildCallerIndex(scan2, importPairs, opts = {}) {
|
|
|
7731
7735
|
function enclosingSymbol(scan2, file, line) {
|
|
7732
7736
|
const f = scan2.files.find((x) => x.rel === file);
|
|
7733
7737
|
if (!f?.symbols.length) return void 0;
|
|
7738
|
+
return enclosingAmong(f.symbols, line);
|
|
7739
|
+
}
|
|
7740
|
+
function enclosingAmong(symbols, line) {
|
|
7734
7741
|
let best;
|
|
7735
|
-
for (const s of
|
|
7742
|
+
for (const s of symbols) {
|
|
7736
7743
|
if (REFERENCE_KINDS3.has(s.kind)) continue;
|
|
7737
7744
|
if (s.line > line) continue;
|
|
7738
7745
|
if (s.endLine !== void 0 && line > s.endLine) continue;
|
|
@@ -7742,6 +7749,29 @@ function enclosingSymbol(scan2, file, line) {
|
|
|
7742
7749
|
}
|
|
7743
7750
|
return best;
|
|
7744
7751
|
}
|
|
7752
|
+
function buildRawCallerIndex(scan2) {
|
|
7753
|
+
const byName = /* @__PURE__ */ new Map();
|
|
7754
|
+
for (const f of scan2.files) {
|
|
7755
|
+
if (!f.calls?.length) continue;
|
|
7756
|
+
const symbols = f.symbols.filter((s) => !REFERENCE_KINDS3.has(s.kind));
|
|
7757
|
+
for (const c2 of f.calls) {
|
|
7758
|
+
const site = { file: f.rel, line: c2.line };
|
|
7759
|
+
if (c2.receiver !== void 0) site.receiver = c2.receiver;
|
|
7760
|
+
const enc = enclosingAmong(symbols, c2.line);
|
|
7761
|
+
if (enc) site.enclosingSymbol = enc;
|
|
7762
|
+
let arr = byName.get(c2.name);
|
|
7763
|
+
if (!arr) byName.set(c2.name, arr = []);
|
|
7764
|
+
arr.push(site);
|
|
7765
|
+
}
|
|
7766
|
+
}
|
|
7767
|
+
const index = /* @__PURE__ */ new Map();
|
|
7768
|
+
for (const name2 of [...byName.keys()].sort(byStr)) {
|
|
7769
|
+
const sites = byName.get(name2);
|
|
7770
|
+
sites.sort((a, b) => byStr(a.file, b.file) || a.line - b.line);
|
|
7771
|
+
index.set(name2, sites);
|
|
7772
|
+
}
|
|
7773
|
+
return index;
|
|
7774
|
+
}
|
|
7745
7775
|
var REFERENCE_KINDS3;
|
|
7746
7776
|
var init_callers = __esm({
|
|
7747
7777
|
"src/callers.ts"() {
|
|
@@ -9374,6 +9404,7 @@ var init_bm25 = __esm({
|
|
|
9374
9404
|
});
|
|
9375
9405
|
|
|
9376
9406
|
// src/embed/model.ts
|
|
9407
|
+
import { createHash as createHash2 } from "crypto";
|
|
9377
9408
|
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
9378
9409
|
import { join as join10 } from "path";
|
|
9379
9410
|
function resolveEmbedModelDir(repo) {
|
|
@@ -9390,16 +9421,12 @@ function resolveEmbedModelDir(repo) {
|
|
|
9390
9421
|
function hasEmbedModel(repo) {
|
|
9391
9422
|
return resolveEmbedModelDir(repo) !== void 0;
|
|
9392
9423
|
}
|
|
9393
|
-
function
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
if (!
|
|
9397
|
-
const raw = JSON.parse(readFileSync5(path, "utf8"));
|
|
9398
|
-
const { modelId, dim, vocab, weights } = raw;
|
|
9399
|
-
if (typeof modelId !== "string" || !modelId) throw new Error(`embed model: missing modelId in ${path}`);
|
|
9400
|
-
if (!Number.isInteger(dim) || dim <= 0) throw new Error(`embed model: bad dim ${dim} in ${path}`);
|
|
9424
|
+
function parseEmbedModel(raw, source) {
|
|
9425
|
+
const { modelId, dim, vocab, weights, unk: rawUnk } = raw ?? {};
|
|
9426
|
+
if (typeof modelId !== "string" || !modelId) throw new Error(`embed model: missing modelId in ${source}`);
|
|
9427
|
+
if (!Number.isInteger(dim) || dim <= 0) throw new Error(`embed model: bad dim ${dim} in ${source}`);
|
|
9401
9428
|
if (!Array.isArray(vocab) || !Array.isArray(weights) || vocab.length !== weights.length) {
|
|
9402
|
-
throw new Error(`embed model: vocab/weights length mismatch in ${
|
|
9429
|
+
throw new Error(`embed model: vocab/weights length mismatch in ${source}`);
|
|
9403
9430
|
}
|
|
9404
9431
|
const vocabSize = vocab.length;
|
|
9405
9432
|
const flat = new Float64Array(vocabSize * dim);
|
|
@@ -9414,20 +9441,42 @@ function loadEmbedModel(dir) {
|
|
|
9414
9441
|
}
|
|
9415
9442
|
for (let d = 0; d < dim; d++) flat[i2 * dim + d] = Number(row[d]);
|
|
9416
9443
|
}
|
|
9417
|
-
const unk = typeof
|
|
9444
|
+
const unk = typeof rawUnk === "string" ? rawUnk : "[UNK]";
|
|
9418
9445
|
const unkId = vmap.has(unk) ? vmap.get(unk) : -1;
|
|
9419
9446
|
return { modelId, dim, unk, unkId, vocabSize, vocab: vmap, weights: flat };
|
|
9420
9447
|
}
|
|
9448
|
+
function loadEmbedModel(dir) {
|
|
9449
|
+
if (!dir) return void 0;
|
|
9450
|
+
const path = join10(dir, "model.json");
|
|
9451
|
+
if (!existsSync3(path)) return void 0;
|
|
9452
|
+
const raw = JSON.parse(readFileSync5(path, "utf8"));
|
|
9453
|
+
return parseEmbedModel(raw, path);
|
|
9454
|
+
}
|
|
9421
9455
|
function resolveEmbedPullUrl() {
|
|
9422
|
-
const
|
|
9423
|
-
|
|
9456
|
+
const env = process.env.CODEINDEX_EMBED_URL;
|
|
9457
|
+
if (env && env.trim()) return { url: env.trim() };
|
|
9458
|
+
return { url: DEFAULT_EMBED_URL, sha256: EMBED_ASSET_SHA256 };
|
|
9424
9459
|
}
|
|
9425
|
-
|
|
9460
|
+
async function fetchEmbedModel(url, expectedSha256) {
|
|
9461
|
+
const res = await fetch(url);
|
|
9462
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
|
|
9463
|
+
const body2 = await res.text();
|
|
9464
|
+
if (expectedSha256) {
|
|
9465
|
+
const got = createHash2("sha256").update(body2).digest("hex");
|
|
9466
|
+
if (got !== expectedSha256) {
|
|
9467
|
+
throw new Error(`sha256 mismatch: expected ${expectedSha256}, got ${got}`);
|
|
9468
|
+
}
|
|
9469
|
+
}
|
|
9470
|
+
return body2;
|
|
9471
|
+
}
|
|
9472
|
+
var EMBED_VERSION, DEFAULT_EMBED_DIRNAME, DEFAULT_EMBED_URL, EMBED_ASSET_SHA256;
|
|
9426
9473
|
var init_model = __esm({
|
|
9427
9474
|
"src/embed/model.ts"() {
|
|
9428
9475
|
"use strict";
|
|
9429
9476
|
EMBED_VERSION = 1;
|
|
9430
9477
|
DEFAULT_EMBED_DIRNAME = "models";
|
|
9478
|
+
DEFAULT_EMBED_URL = "https://github.com/maxgfr/codeindex/releases/download/embed-model-v1/model.json";
|
|
9479
|
+
EMBED_ASSET_SHA256 = "163ad053eab4e9a80d421ed4164f32292c83290f02fbbe6fe4b9b1cd6ea18d34";
|
|
9431
9480
|
}
|
|
9432
9481
|
});
|
|
9433
9482
|
|
|
@@ -10129,8 +10178,13 @@ var init_viz = __esm({
|
|
|
10129
10178
|
// src/mcp.ts
|
|
10130
10179
|
var mcp_exports = {};
|
|
10131
10180
|
__export(mcp_exports, {
|
|
10132
|
-
|
|
10181
|
+
memoizedEmbedModel: () => memoizedEmbedModel,
|
|
10182
|
+
memoizedEmbeddingIndex: () => memoizedEmbeddingIndex,
|
|
10183
|
+
runMcpServer: () => runMcpServer,
|
|
10184
|
+
scanFingerprint: () => scanFingerprint
|
|
10133
10185
|
});
|
|
10186
|
+
import { statSync as statSync4 } from "fs";
|
|
10187
|
+
import { join as join12 } from "path";
|
|
10134
10188
|
import { createInterface } from "readline";
|
|
10135
10189
|
function str(v) {
|
|
10136
10190
|
return typeof v === "string" && v ? v : void 0;
|
|
@@ -10138,6 +10192,32 @@ function str(v) {
|
|
|
10138
10192
|
function strArray(v) {
|
|
10139
10193
|
return Array.isArray(v) && v.every((x) => typeof x === "string") && v.length ? v : void 0;
|
|
10140
10194
|
}
|
|
10195
|
+
function errMessage(e) {
|
|
10196
|
+
return e instanceof Error ? e.message : String(e);
|
|
10197
|
+
}
|
|
10198
|
+
function scanFingerprint(scan2) {
|
|
10199
|
+
return sha1(scan2.files.map((f) => `${f.rel}:${f.hash}`).join("\n"));
|
|
10200
|
+
}
|
|
10201
|
+
async function memoizedEmbeddingIndex(key, build) {
|
|
10202
|
+
const cacheKey = `${key.mode}:${key.identity}:${scanFingerprint(key.scan)}`;
|
|
10203
|
+
if (embeddingIndexCache && embeddingIndexCache.key === cacheKey) return embeddingIndexCache.index;
|
|
10204
|
+
const index = await build();
|
|
10205
|
+
embeddingIndexCache = { key: cacheKey, index };
|
|
10206
|
+
return index;
|
|
10207
|
+
}
|
|
10208
|
+
function memoizedEmbedModel(modelDir) {
|
|
10209
|
+
let stat;
|
|
10210
|
+
try {
|
|
10211
|
+
stat = statSync4(join12(modelDir, "model.json"));
|
|
10212
|
+
} catch {
|
|
10213
|
+
return void 0;
|
|
10214
|
+
}
|
|
10215
|
+
const key = `${modelDir}:${stat.mtimeMs}:${stat.size}`;
|
|
10216
|
+
if (embedModelCache && embedModelCache.key === key) return embedModelCache.model;
|
|
10217
|
+
const model = loadEmbedModel(modelDir);
|
|
10218
|
+
if (model) embedModelCache = { key, model };
|
|
10219
|
+
return model;
|
|
10220
|
+
}
|
|
10141
10221
|
async function callTool(name2, args2) {
|
|
10142
10222
|
const repo = str(args2.repo);
|
|
10143
10223
|
if (!repo) throw new Error("`repo` is required (absolute path to the repository root)");
|
|
@@ -10278,25 +10358,41 @@ async function callTool(name2, args2) {
|
|
|
10278
10358
|
const endpoint = resolveEmbedEndpoint();
|
|
10279
10359
|
if (endpoint) {
|
|
10280
10360
|
try {
|
|
10281
|
-
const index = await buildEndpointIndex(scan2);
|
|
10361
|
+
const index = await memoizedEmbeddingIndex({ mode: "endpoint", identity: endpoint, scan: scan2 }, () => buildEndpointIndex(scan2));
|
|
10282
10362
|
const queryVec = await encodeQueryViaEndpoint(query);
|
|
10283
|
-
|
|
10284
|
-
|
|
10285
|
-
|
|
10363
|
+
const results2 = searchSemantic(scan2, query, index, { queryVec, limit, fuzzy });
|
|
10364
|
+
return JSON.stringify({ results: results2, tier: "endpoint" }, null, 2);
|
|
10365
|
+
} catch (e) {
|
|
10366
|
+
const results2 = searchIndex(scan2, query, { limit, fuzzy });
|
|
10367
|
+
return JSON.stringify(
|
|
10368
|
+
{ results: results2, tier: "lexical", degradedReason: `embedding endpoint failed: ${errMessage(e)}` },
|
|
10369
|
+
null,
|
|
10370
|
+
2
|
|
10371
|
+
);
|
|
10286
10372
|
}
|
|
10287
10373
|
}
|
|
10288
10374
|
const modelDir = resolveEmbedModelDir(repo);
|
|
10289
|
-
const model = modelDir ?
|
|
10375
|
+
const model = modelDir ? memoizedEmbedModel(modelDir) : void 0;
|
|
10290
10376
|
if (model) {
|
|
10291
|
-
const index =
|
|
10292
|
-
|
|
10377
|
+
const index = await memoizedEmbeddingIndex(
|
|
10378
|
+
{ mode: "static", identity: `${modelDir}#${model.modelId}`, scan: scan2 },
|
|
10379
|
+
() => buildEmbeddingIndex(scan2, model)
|
|
10380
|
+
);
|
|
10381
|
+
const results2 = searchSemantic(scan2, query, index, { model, limit, fuzzy });
|
|
10382
|
+
return JSON.stringify({ results: results2, tier: "static" }, null, 2);
|
|
10293
10383
|
}
|
|
10384
|
+
const results = searchIndex(scan2, query, { limit, fuzzy });
|
|
10385
|
+
return JSON.stringify(
|
|
10386
|
+
{ results, tier: "lexical", degradedReason: "no embedding endpoint or static model configured \u2014 see embed_status" },
|
|
10387
|
+
null,
|
|
10388
|
+
2
|
|
10389
|
+
);
|
|
10294
10390
|
}
|
|
10295
10391
|
return JSON.stringify(searchIndex(scan2, query, { limit, fuzzy }), null, 2);
|
|
10296
10392
|
}
|
|
10297
10393
|
if (name2 === "embed_status") {
|
|
10298
10394
|
const modelDir = resolveEmbedModelDir(repo);
|
|
10299
|
-
const model = modelDir ?
|
|
10395
|
+
const model = modelDir ? memoizedEmbedModel(modelDir) : void 0;
|
|
10300
10396
|
const endpoint = resolveEmbedEndpoint();
|
|
10301
10397
|
const mode = endpoint ? "endpoint" : model ? "static" : "none";
|
|
10302
10398
|
const status = {
|
|
@@ -10371,7 +10467,7 @@ async function runMcpServer() {
|
|
|
10371
10467
|
}
|
|
10372
10468
|
}
|
|
10373
10469
|
}
|
|
10374
|
-
var repoProp, scopeProps, TOOLS;
|
|
10470
|
+
var repoProp, scopeProps, TOOLS, embeddingIndexCache, embedModelCache;
|
|
10375
10471
|
var init_mcp = __esm({
|
|
10376
10472
|
"src/mcp.ts"() {
|
|
10377
10473
|
"use strict";
|
|
@@ -10398,6 +10494,7 @@ var init_mcp = __esm({
|
|
|
10398
10494
|
init_embed();
|
|
10399
10495
|
init_search();
|
|
10400
10496
|
init_endpoint();
|
|
10497
|
+
init_hash();
|
|
10401
10498
|
repoProp = { repo: { type: "string", description: "Absolute path to the repository root" } };
|
|
10402
10499
|
scopeProps = {
|
|
10403
10500
|
scope: { type: "string", description: "Restrict to one directory (repo-relative)" },
|
|
@@ -10610,7 +10707,7 @@ var init_mcp = __esm({
|
|
|
10610
10707
|
},
|
|
10611
10708
|
{
|
|
10612
10709
|
name: "search",
|
|
10613
|
-
description: 'Natural-language-ish lexical search: BM25 ranking (k1=1.2, b=0.75) over symbol names (camelCase/snake_case subtokens), file path segments, markdown headings and summary lines. NOT embeddings by default \u2014 deterministic, diacritic-folded, zero API keys. Answers "where is auth handled?"-style queries with ranked files, matched terms and top symbols. Query terms with zero document frequency get a deterministic trigram-fuzzy fallback (typo-tolerant) unless `fuzzy: false`. Set `semantic: true` to RRF-fuse
|
|
10710
|
+
description: 'Natural-language-ish lexical search: BM25 ranking (k1=1.2, b=0.75) over symbol names (camelCase/snake_case subtokens), file path segments, markdown headings and summary lines. NOT embeddings by default \u2014 deterministic, diacritic-folded, zero API keys. Answers "where is auth handled?"-style queries with ranked files, matched terms and top symbols. Query terms with zero document frequency get a deterministic trigram-fuzzy fallback (typo-tolerant) unless `fuzzy: false`. Set `semantic: true` to RRF-fuse an embedding tier (HTTP endpoint, else a local static model) with lexical \u2014 the response then wraps the ranked list as `{ results, tier, degradedReason? }`, `tier` being "endpoint"/"static" when fusion happened or "lexical" (with `degradedReason`) when it did not (see embed_status). Without `semantic`, the response is the bare ranked array, unchanged.',
|
|
10614
10711
|
inputSchema: {
|
|
10615
10712
|
type: "object",
|
|
10616
10713
|
properties: {
|
|
@@ -10624,7 +10721,7 @@ var init_mcp = __esm({
|
|
|
10624
10721
|
},
|
|
10625
10722
|
semantic: {
|
|
10626
10723
|
type: "boolean",
|
|
10627
|
-
description:
|
|
10724
|
+
description: 'RRF-fuse an embedding tier with lexical (default false). Precedence: the HTTP endpoint (CODEINDEX_EMBED_ENDPOINT) if set, else a local static model. The response reports the effective tier as a top-level `tier` field ("endpoint"/"static" on success, "lexical" plus `degradedReason` when neither is available/reachable) instead of degrading silently \u2014 see embed_status.'
|
|
10628
10725
|
}
|
|
10629
10726
|
},
|
|
10630
10727
|
required: ["repo", "query"]
|
|
@@ -11073,7 +11170,7 @@ init_pipeline();
|
|
|
11073
11170
|
init_graph_json();
|
|
11074
11171
|
init_symbols_json();
|
|
11075
11172
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
11076
|
-
import { join as
|
|
11173
|
+
import { join as join13, resolve } from "path";
|
|
11077
11174
|
init_scan();
|
|
11078
11175
|
init_callers();
|
|
11079
11176
|
init_workspaces();
|
|
@@ -11116,8 +11213,9 @@ Commands:
|
|
|
11116
11213
|
embed status Effective mode (none/static/endpoint), model +
|
|
11117
11214
|
EMBED_VERSION, and endpoint reachability (JSON)
|
|
11118
11215
|
embed build Write embeddings.bin into --out <dir> (static tier)
|
|
11119
|
-
embed pull Fetch the model asset into CODEINDEX_EMBED_DIR
|
|
11120
|
-
<repo>/.codeindex/models/)
|
|
11216
|
+
embed pull Fetch the official model asset into CODEINDEX_EMBED_DIR
|
|
11217
|
+
(or <repo>/.codeindex/models/); sha256-verified. Override
|
|
11218
|
+
the source with CODEINDEX_EMBED_URL
|
|
11121
11219
|
embed serve Print (or --run) the docker command that starts the
|
|
11122
11220
|
containerized embedding server (rich tier)
|
|
11123
11221
|
rules Architecture rules (forbidden edges, cycles, orphans) validated
|
|
@@ -11140,8 +11238,11 @@ Flags:
|
|
|
11140
11238
|
--exclude <glob> Exclude matching paths (repeatable)
|
|
11141
11239
|
--scope <dir> Restrict to one directory (sugar for --include '<dir>/**')
|
|
11142
11240
|
--no-gitignore Do not honor .gitignore files (default: honored)
|
|
11241
|
+
--ignore-dir <name> Directory names to skip (repeatable) \u2014 REPLACES the
|
|
11242
|
+
default ignored-directory set, never merges with it
|
|
11143
11243
|
--max-files <n> Cap walked files (default 20000)
|
|
11144
11244
|
--max-bytes <n> Skip files above this size (default 1 MiB)
|
|
11245
|
+
--max-calls <n> Per-file call-site cap for extraction (default 512)
|
|
11145
11246
|
--no-ast Skip tree-sitter grammars even when present (regex tier)
|
|
11146
11247
|
--config <file> Rules config for \`rules\` (JSON: [{name, from, to, \u2026}])
|
|
11147
11248
|
--limit <n> Max results for \`search\` (default 20)
|
|
@@ -11156,7 +11257,7 @@ Flags:
|
|
|
11156
11257
|
each site corroborated|unique-name
|
|
11157
11258
|
`;
|
|
11158
11259
|
function parseFlags(args2) {
|
|
11159
|
-
const flags2 = { repo: process.cwd(), include: [], exclude: [], gitignore: true, noAst: false, fuzzy: true, semantic: false };
|
|
11260
|
+
const flags2 = { repo: process.cwd(), include: [], exclude: [], gitignore: true, ignoreDirs: [], noAst: false, fuzzy: true, semantic: false };
|
|
11160
11261
|
for (let i2 = 0; i2 < args2.length; i2++) {
|
|
11161
11262
|
const a = args2[i2];
|
|
11162
11263
|
const next = () => {
|
|
@@ -11179,8 +11280,10 @@ function parseFlags(args2) {
|
|
|
11179
11280
|
else if (a === "--exclude") flags2.exclude.push(next());
|
|
11180
11281
|
else if (a === "--scope") flags2.scope = next();
|
|
11181
11282
|
else if (a === "--no-gitignore") flags2.gitignore = false;
|
|
11283
|
+
else if (a === "--ignore-dir") flags2.ignoreDirs.push(next());
|
|
11182
11284
|
else if (a === "--max-files") flags2.maxFiles = num();
|
|
11183
11285
|
else if (a === "--max-bytes") flags2.maxBytes = num();
|
|
11286
|
+
else if (a === "--max-calls") flags2.maxCalls = num();
|
|
11184
11287
|
else if (a === "--ignore-case") flags2.ignoreCase = true;
|
|
11185
11288
|
else if (a === "--max-hits") flags2.maxHits = num();
|
|
11186
11289
|
else if (a === "--budget-tokens") flags2.budgetTokens = num();
|
|
@@ -11207,8 +11310,10 @@ function scanOptions(flags2) {
|
|
|
11207
11310
|
exclude: flags2.exclude.length ? flags2.exclude : void 0,
|
|
11208
11311
|
scope: flags2.scope,
|
|
11209
11312
|
gitignore: flags2.gitignore,
|
|
11313
|
+
ignoreDirs: flags2.ignoreDirs.length ? flags2.ignoreDirs : void 0,
|
|
11210
11314
|
maxFiles: flags2.maxFiles,
|
|
11211
|
-
maxBytes: flags2.maxBytes
|
|
11315
|
+
maxBytes: flags2.maxBytes,
|
|
11316
|
+
maxCallsPerFile: flags2.maxCalls
|
|
11212
11317
|
};
|
|
11213
11318
|
}
|
|
11214
11319
|
async function runCli(argv) {
|
|
@@ -11233,7 +11338,7 @@ async function runCli(argv) {
|
|
|
11233
11338
|
if (!flags2.out) throw new Error("index needs --out <dir>");
|
|
11234
11339
|
const outDir = flags2.out;
|
|
11235
11340
|
mkdirSync2(outDir, { recursive: true });
|
|
11236
|
-
const cachePath =
|
|
11341
|
+
const cachePath = join13(outDir, "cache.json");
|
|
11237
11342
|
let cache;
|
|
11238
11343
|
try {
|
|
11239
11344
|
const parsed = JSON.parse(readFileSync6(cachePath, "utf8"));
|
|
@@ -11243,8 +11348,8 @@ async function runCli(argv) {
|
|
|
11243
11348
|
} catch {
|
|
11244
11349
|
}
|
|
11245
11350
|
const { scan: scan2, graph, symbols } = buildIndexArtifacts(flags2.repo, { ...scanOptions(flags2), cache, out: outDir });
|
|
11246
|
-
writeFileSync3(
|
|
11247
|
-
writeFileSync3(
|
|
11351
|
+
writeFileSync3(join13(outDir, "graph.json"), renderGraphJson(graph));
|
|
11352
|
+
writeFileSync3(join13(outDir, "symbols.json"), renderSymbolsJson(symbols));
|
|
11248
11353
|
const files = {};
|
|
11249
11354
|
for (const f of scan2.files) {
|
|
11250
11355
|
const entry = { hash: f.hash, record: f, size: f.size };
|
|
@@ -11261,7 +11366,7 @@ async function runCli(argv) {
|
|
|
11261
11366
|
const model = modelDir ? loadEmbedModel(modelDir) : void 0;
|
|
11262
11367
|
if (model) {
|
|
11263
11368
|
const index = buildEmbeddingIndex(scan2, model);
|
|
11264
|
-
writeFileSync3(
|
|
11369
|
+
writeFileSync3(join13(outDir, "embeddings.bin"), serializeEmbeddings(index));
|
|
11265
11370
|
embedNote = ` + embeddings.bin (${index.records.length} records, model ${model.modelId})`;
|
|
11266
11371
|
}
|
|
11267
11372
|
process.stderr.write(`codeindex: ${scan2.files.length} files \u2192 ${outDir}/graph.json + symbols.json${embedNote}${scan2.capped ? " (capped)" : ""}
|
|
@@ -11393,39 +11498,36 @@ async function runCli(argv) {
|
|
|
11393
11498
|
mkdirSync2(flags2.out, { recursive: true });
|
|
11394
11499
|
const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
|
|
11395
11500
|
const index = buildEmbeddingIndex(scan2, model);
|
|
11396
|
-
writeFileSync3(
|
|
11501
|
+
writeFileSync3(join13(flags2.out, "embeddings.bin"), serializeEmbeddings(index));
|
|
11397
11502
|
process.stderr.write(`codeindex: ${index.records.length} embedding records \u2192 ${flags2.out}/embeddings.bin (model ${model.modelId})
|
|
11398
11503
|
`);
|
|
11399
11504
|
} else if (sub === "pull") {
|
|
11400
|
-
const url = resolveEmbedPullUrl();
|
|
11401
|
-
|
|
11402
|
-
process.stderr.write(
|
|
11403
|
-
"codeindex: no model URL configured. The official static-embedding asset is not published yet.\nSet CODEINDEX_EMBED_URL to a model.json URL (optionally CODEINDEX_EMBED_DIR as the destination), then re-run `codeindex embed pull`.\n"
|
|
11404
|
-
);
|
|
11405
|
-
process.exitCode = 1;
|
|
11406
|
-
return;
|
|
11407
|
-
}
|
|
11408
|
-
const destDir = process.env.CODEINDEX_EMBED_DIR ?? join12(flags2.repo, ".codeindex", "models");
|
|
11505
|
+
const { url, sha256 } = resolveEmbedPullUrl();
|
|
11506
|
+
const destDir = process.env.CODEINDEX_EMBED_DIR ?? join13(flags2.repo, ".codeindex", "models");
|
|
11409
11507
|
mkdirSync2(destDir, { recursive: true });
|
|
11410
|
-
process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${
|
|
11508
|
+
process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${join13(destDir, "model.json")}
|
|
11411
11509
|
`);
|
|
11412
|
-
|
|
11413
|
-
|
|
11414
|
-
|
|
11510
|
+
let body2;
|
|
11511
|
+
try {
|
|
11512
|
+
body2 = await fetchEmbedModel(url, sha256);
|
|
11513
|
+
} catch (e) {
|
|
11514
|
+
process.stderr.write(`codeindex: pull failed \u2014 ${e instanceof Error ? e.message : String(e)} (nothing written)
|
|
11415
11515
|
`);
|
|
11416
11516
|
process.exitCode = 1;
|
|
11417
11517
|
return;
|
|
11418
11518
|
}
|
|
11419
|
-
const body2 = await res.text();
|
|
11420
11519
|
try {
|
|
11421
|
-
JSON.parse(body2);
|
|
11422
|
-
} catch {
|
|
11423
|
-
process.stderr.write(
|
|
11520
|
+
parseEmbedModel(JSON.parse(body2), url);
|
|
11521
|
+
} catch (e) {
|
|
11522
|
+
process.stderr.write(
|
|
11523
|
+
`codeindex: pull failed \u2014 response is not a valid model.json (${e instanceof Error ? e.message : String(e)}) (nothing written)
|
|
11524
|
+
`
|
|
11525
|
+
);
|
|
11424
11526
|
process.exitCode = 1;
|
|
11425
11527
|
return;
|
|
11426
11528
|
}
|
|
11427
|
-
writeFileSync3(
|
|
11428
|
-
process.stderr.write(`codeindex: model written to ${
|
|
11529
|
+
writeFileSync3(join13(destDir, "model.json"), body2);
|
|
11530
|
+
process.stderr.write(`codeindex: model written to ${join13(destDir, "model.json")}
|
|
11429
11531
|
`);
|
|
11430
11532
|
} else {
|
|
11431
11533
|
throw new Error("embed needs a subcommand: status | build | pull | serve");
|
|
@@ -11508,6 +11610,7 @@ export {
|
|
|
11508
11610
|
buildGraph,
|
|
11509
11611
|
buildIndexArtifacts,
|
|
11510
11612
|
buildModules,
|
|
11613
|
+
buildRawCallerIndex,
|
|
11511
11614
|
buildResolveContext,
|
|
11512
11615
|
buildSymbolIndex,
|
|
11513
11616
|
byKey,
|
package/src/ast/extract.ts
CHANGED
|
@@ -399,8 +399,9 @@ function readReceiver(node: TSNode | null): string | undefined {
|
|
|
399
399
|
// dedicated member-call node's `name`; "constructor" reads the constructed type.
|
|
400
400
|
// A qualified call also carries the immediate `receiver` name (see readReceiver).
|
|
401
401
|
// Names are filtered to plausible identifiers (≥ 2 chars), deduped by name+line,
|
|
402
|
-
// sorted, and capped, so the set
|
|
403
|
-
|
|
402
|
+
// sorted, and capped (default MAX_CALLS; overridable via maxCalls), so the set
|
|
403
|
+
// stays small and deterministic.
|
|
404
|
+
function collectCalls(root: TSNode, spec: LangSpec, maxCalls: number = MAX_CALLS): { name: string; line: number; receiver?: string }[] {
|
|
404
405
|
if (!spec.calls) return [];
|
|
405
406
|
const out: { name: string; line: number; receiver?: string }[] = [];
|
|
406
407
|
const seen = new Set<string>();
|
|
@@ -439,7 +440,7 @@ function collectCalls(root: TSNode, spec: LangSpec): { name: string; line: numbe
|
|
|
439
440
|
};
|
|
440
441
|
visit(root);
|
|
441
442
|
out.sort((a, b) => byStr(a.name, b.name) || a.line - b.line);
|
|
442
|
-
return out.slice(0,
|
|
443
|
+
return out.slice(0, maxCalls);
|
|
443
444
|
}
|
|
444
445
|
|
|
445
446
|
// Collect JS/TS named-import bindings: `import { a, b as c } from "x"` →
|
|
@@ -476,7 +477,8 @@ function collectImportedNames(root: TSNode, spec: LangSpec): string[] {
|
|
|
476
477
|
// Extract declared symbols from one file via its committed grammar. Returns
|
|
477
478
|
// undefined when no grammar is loaded for the extension (caller falls back to the
|
|
478
479
|
// regex extractor). Walks top-level declarations plus one level of nested members.
|
|
479
|
-
|
|
480
|
+
// `opts.maxCalls` overrides the per-file call-site cap (default MAX_CALLS).
|
|
481
|
+
export function extractAst(rel: string, ext: string, content: string, opts: { maxCalls?: number } = {}): AstResult | undefined {
|
|
480
482
|
const key = grammarKeyForExt(ext);
|
|
481
483
|
if (!key || !grammarReady(key)) return undefined;
|
|
482
484
|
const spec = SPECS[key];
|
|
@@ -697,7 +699,7 @@ export function extractAst(rel: string, ext: string, content: string): AstResult
|
|
|
697
699
|
|
|
698
700
|
const refs = collectImports(root, spec);
|
|
699
701
|
const idents = collectRefIdents(root, new Set(symbols.map((s) => s.name)));
|
|
700
|
-
const calls = collectCalls(root, spec);
|
|
702
|
+
const calls = collectCalls(root, spec, opts.maxCalls);
|
|
701
703
|
const importedNames = collectImportedNames(root, spec);
|
|
702
704
|
let pkg: string | undefined;
|
|
703
705
|
if (spec.lang === "java") {
|
package/src/callers.ts
CHANGED
|
Binary file
|