@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.
@@ -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.11.1";
17
+ ENGINE_VERSION = "2.13.0";
18
18
  SCHEMA_VERSION = 4;
19
- EXTRACTOR_VERSION = 8;
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 (IGNORE_DIRS.has(name2)) continue;
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 localKindOf = /* @__PURE__ */ new Map();
741
- for (const s of localSymbols) if (!localKindOf.has(s.name)) localKindOf.set(s.name, s.kind);
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 mirroredKind = !from ? localKindOf.get(orig) : void 0;
755
+ const decl = !from ? localDeclOf.get(orig) : void 0;
754
756
  out2.push({
755
757
  name: name2,
756
- kind: mirroredKind ?? "reexport",
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, MAX_CALLS);
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 < 512; i2++) {
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 < 512) {
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 f.symbols) {
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 loadEmbedModel(dir) {
9394
- if (!dir) return void 0;
9395
- const path = join10(dir, "model.json");
9396
- if (!existsSync3(path)) return void 0;
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 ${path}`);
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 raw.unk === "string" ? raw.unk : "[UNK]";
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 url = process.env.CODEINDEX_EMBED_URL;
9423
- return url && url.trim() ? url.trim() : void 0;
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
- var EMBED_VERSION, DEFAULT_EMBED_DIRNAME;
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
- runMcpServer: () => runMcpServer
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
- return JSON.stringify(searchSemantic(scan2, query, index, { queryVec, limit, fuzzy }), null, 2);
10284
- } catch {
10285
- return JSON.stringify(searchIndex(scan2, query, { limit, fuzzy }), null, 2);
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 ? loadEmbedModel(modelDir) : void 0;
10375
+ const model = modelDir ? memoizedEmbedModel(modelDir) : void 0;
10290
10376
  if (model) {
10291
- const index = buildEmbeddingIndex(scan2, model);
10292
- return JSON.stringify(searchSemantic(scan2, query, index, { model, limit, fuzzy }), null, 2);
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 ? loadEmbedModel(modelDir) : void 0;
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 the deterministic static-embedding tier when a model asset is present (degrades to lexical otherwise \u2014 see embed_status).',
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: "RRF-fuse an embedding tier with lexical (default false). Precedence: the HTTP endpoint (CODEINDEX_EMBED_ENDPOINT) if set, else a local static model. Degrades silently to lexical-only when neither is available/reachable \u2014 see embed_status."
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 join12, resolve } from "path";
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 (or
11120
- <repo>/.codeindex/models/) \u2014 needs CODEINDEX_EMBED_URL
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 = join12(outDir, "cache.json");
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(join12(outDir, "graph.json"), renderGraphJson(graph));
11247
- writeFileSync3(join12(outDir, "symbols.json"), renderSymbolsJson(symbols));
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(join12(outDir, "embeddings.bin"), serializeEmbeddings(index));
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(join12(flags2.out, "embeddings.bin"), serializeEmbeddings(index));
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
- if (!url) {
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 ${join12(destDir, "model.json")}
11508
+ process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${join13(destDir, "model.json")}
11411
11509
  `);
11412
- const res = await fetch(url);
11413
- if (!res.ok) {
11414
- process.stderr.write(`codeindex: pull failed \u2014 HTTP ${res.status} from ${url}
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("codeindex: pull failed \u2014 response is not a valid model.json (expected JSON)\n");
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(join12(destDir, "model.json"), body2);
11428
- process.stderr.write(`codeindex: model written to ${join12(destDir, "model.json")}
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,
@@ -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 stays small and deterministic.
403
- function collectCalls(root: TSNode, spec: LangSpec): { name: string; line: number; receiver?: string }[] {
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, MAX_CALLS);
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
- export function extractAst(rel: string, ext: string, content: string): AstResult | undefined {
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