ultra11y 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,7 +14,7 @@ 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.27.1";
17
+ ENGINE_VERSION = "2.28.0";
18
18
  SCHEMA_VERSION = 5;
19
19
  EXTRACTOR_VERSION = 13;
20
20
  }
@@ -85,6 +85,14 @@ function keywords(question) {
85
85
  }
86
86
  return out2;
87
87
  }
88
+ function droppedKeywords(question) {
89
+ const out2 = [];
90
+ for (const raw of foldText(question).split(/[^A-Za-z0-9_]+/)) {
91
+ if (!raw) continue;
92
+ if (raw.length < 2 || STOPWORDS.has(raw.toLowerCase())) out2.push(raw);
93
+ }
94
+ return out2;
95
+ }
88
96
  function rankedKeywords(question) {
89
97
  const base = keywords(question);
90
98
  const score = (raw) => {
@@ -1759,7 +1767,7 @@ var init_literals = __esm({
1759
1767
  }
1760
1768
  });
1761
1769
 
1762
- // node_modules/.pnpm/web-tree-sitter@0.26.11/node_modules/web-tree-sitter/web-tree-sitter.js
1770
+ // node_modules/.pnpm/web-tree-sitter@0.26.12/node_modules/web-tree-sitter/web-tree-sitter.js
1763
1771
  function assertInternal(x) {
1764
1772
  if (x !== INTERNAL) throw new Error("Illegal constructor");
1765
1773
  }
@@ -1967,13 +1975,13 @@ async function Module2(moduleArg = {}) {
1967
1975
  }
1968
1976
  readAsync = /* @__PURE__ */ __name(async (url) => {
1969
1977
  if (isFileURI(url)) {
1970
- return new Promise((resolve3, reject) => {
1978
+ return new Promise((resolve5, reject) => {
1971
1979
  var xhr = new XMLHttpRequest();
1972
1980
  xhr.open("GET", url, true);
1973
1981
  xhr.responseType = "arraybuffer";
1974
1982
  xhr.onload = () => {
1975
1983
  if (xhr.status == 200 || xhr.status == 0 && xhr.response) {
1976
- resolve3(xhr.response);
1984
+ resolve5(xhr.response);
1977
1985
  return;
1978
1986
  }
1979
1987
  reject(xhr.status);
@@ -2169,9 +2177,9 @@ async function Module2(moduleArg = {}) {
2169
2177
  __name(receiveInstantiationResult, "receiveInstantiationResult");
2170
2178
  var info2 = getWasmImports();
2171
2179
  if (Module["instantiateWasm"]) {
2172
- return new Promise((resolve3, reject) => {
2180
+ return new Promise((resolve5, reject) => {
2173
2181
  Module["instantiateWasm"](info2, (mod, inst) => {
2174
- resolve3(receiveInstance(mod, inst));
2182
+ resolve5(receiveInstance(mod, inst));
2175
2183
  });
2176
2184
  });
2177
2185
  }
@@ -3502,8 +3510,8 @@ async function Module2(moduleArg = {}) {
3502
3510
  if (runtimeInitialized) {
3503
3511
  moduleRtn = Module;
3504
3512
  } else {
3505
- moduleRtn = new Promise((resolve3, reject) => {
3506
- readyPromiseResolve = resolve3;
3513
+ moduleRtn = new Promise((resolve5, reject) => {
3514
+ readyPromiseResolve = resolve5;
3507
3515
  readyPromiseReject = reject;
3508
3516
  });
3509
3517
  }
@@ -3687,7 +3695,7 @@ function parsePattern(index, stepType, stepValueId, captureNames, stringValues,
3687
3695
  }
3688
3696
  var __defProp2, __name, Edit, SIZE_OF_SHORT, SIZE_OF_INT, SIZE_OF_CURSOR, SIZE_OF_NODE, SIZE_OF_POINT, SIZE_OF_RANGE, ZERO_POINT, INTERNAL, C, LookaheadIterator, Tree, TreeCursor, Node, LANGUAGE_FUNCTION_REGEX, Language, web_tree_sitter_default, Module3, TRANSFER_BUFFER, LANGUAGE_VERSION, MIN_COMPATIBLE_VERSION, Parser, PREDICATE_STEP_TYPE_CAPTURE, PREDICATE_STEP_TYPE_STRING, QUERY_WORD_REGEX, CaptureQuantifier, isCaptureStep, isStringStep, QueryErrorKind, QueryError, Query;
3689
3697
  var init_web_tree_sitter = __esm({
3690
- "node_modules/.pnpm/web-tree-sitter@0.26.11/node_modules/web-tree-sitter/web-tree-sitter.js"() {
3698
+ "node_modules/.pnpm/web-tree-sitter@0.26.12/node_modules/web-tree-sitter/web-tree-sitter.js"() {
3691
3699
  "use strict";
3692
3700
  __defProp2 = Object.defineProperty;
3693
3701
  __name = (target, value) => __defProp2(target, "name", { value, configurable: true });
@@ -9849,6 +9857,27 @@ function buildTrigramIndex(docs) {
9849
9857
  return index;
9850
9858
  }
9851
9859
  function searchIndex(scan2, query, opts = {}) {
9860
+ return runSearch(scan2, query, opts).results;
9861
+ }
9862
+ function explainQuery(scan2, query, opts = {}) {
9863
+ return runSearch(scan2, query, opts);
9864
+ }
9865
+ function emptyExplanation(query, verdict, note) {
9866
+ return {
9867
+ results: [],
9868
+ explain: {
9869
+ query,
9870
+ terms: [],
9871
+ droppedStopwords: droppedKeywords(query),
9872
+ unresolvedTerms: [],
9873
+ verdict,
9874
+ note,
9875
+ bridgedOnlyResults: 0,
9876
+ resultCount: 0
9877
+ }
9878
+ };
9879
+ }
9880
+ function runSearch(scan2, query, opts = {}) {
9852
9881
  const terms = [];
9853
9882
  const seen = /* @__PURE__ */ new Set();
9854
9883
  for (const kw of keywords(query)) {
@@ -9858,11 +9887,18 @@ function searchIndex(scan2, query, opts = {}) {
9858
9887
  terms.push(t);
9859
9888
  }
9860
9889
  }
9861
- if (!terms.length) return [];
9890
+ if (!terms.length) {
9891
+ const dropped = droppedKeywords(query);
9892
+ return emptyExplanation(
9893
+ query,
9894
+ "none",
9895
+ dropped.length ? `Nothing was searched for: every token in this query (${dropped.join(", ")}) is a stopword or too short to index. Search with the identifiers or domain words you are actually looking for.` : "Nothing was searched for: the query carried no indexable token."
9896
+ );
9897
+ }
9862
9898
  const queryWantsTests = terms.some((t) => TEST_INTENT.test(t));
9863
9899
  const docs = bm25DocsFor(scan2);
9864
9900
  const n = docs.length;
9865
- if (!n) return [];
9901
+ if (!n) return emptyExplanation(query, "none", "This index contains no files.");
9866
9902
  const avgLen = {};
9867
9903
  for (const f of FIELDS) {
9868
9904
  let total = 0;
@@ -9924,7 +9960,7 @@ function searchIndex(scan2, query, opts = {}) {
9924
9960
  const matchedFields = /* @__PURE__ */ new Set();
9925
9961
  const symbolTerms = /* @__PURE__ */ new Set();
9926
9962
  const fuzzyHit = /* @__PURE__ */ new Set();
9927
- let exact = false;
9963
+ let exactNameHit = false;
9928
9964
  const weightedTf = (term) => {
9929
9965
  let wtf = 0;
9930
9966
  for (const f of FIELDS) {
@@ -9942,7 +9978,7 @@ function searchIndex(scan2, query, opts = {}) {
9942
9978
  if (wtf > 0) {
9943
9979
  matched.push(t);
9944
9980
  symbolTerms.add(t);
9945
- if (d.exactNames.has(t)) exact = true;
9981
+ if (d.exactNames.has(t)) exactNameHit = true;
9946
9982
  score += idfOf(df.get(t)) * (wtf / (K1 + wtf));
9947
9983
  continue;
9948
9984
  }
@@ -9955,7 +9991,7 @@ function searchIndex(scan2, query, opts = {}) {
9955
9991
  }
9956
9992
  }
9957
9993
  if (!matched.length && !fuzzyHit.size) continue;
9958
- if (exact) score *= EXACT_NAME_BOOST;
9994
+ if (exactNameHit) score *= EXACT_NAME_BOOST;
9959
9995
  if (d.isTest && !queryWantsTests) score *= TEST_DEMOTION;
9960
9996
  if (prior) {
9961
9997
  score *= 1 + 0.35 * Math.log1p(prior.get(d.file) ?? 0);
@@ -9979,10 +10015,60 @@ function searchIndex(scan2, query, opts = {}) {
9979
10015
  result.line = Math.min(...hits.map((h) => h.line));
9980
10016
  }
9981
10017
  if (fuzzyHit.size) result.fuzzyTerms = [...fuzzyHit].sort(byStr);
10018
+ if (!matched.length) result.bridgedOnly = true;
9982
10019
  results.push(result);
9983
10020
  }
9984
10021
  results.sort((a, b) => b.score - a.score || byStr(a.file, b.file));
9985
- return results.slice(0, opts.limit ?? DEFAULT_LIMIT);
10022
+ const kept = (opts.exact ? results.filter((r) => !r.bridgedOnly) : results).slice(0, opts.limit ?? DEFAULT_LIMIT);
10023
+ return { results: kept, explain: explainOf(query, terms, df, fuzzyCandidates, kept) };
10024
+ }
10025
+ function explainOf(query, terms, df, fuzzyCandidates, kept) {
10026
+ const diagnostics = terms.map((term) => {
10027
+ const frequency = df.get(term) ?? 0;
10028
+ const candidates = frequency === 0 ? fuzzyCandidates.get(term) ?? [] : [];
10029
+ if (!candidates.length) return { term, df: frequency };
10030
+ const via = candidates[0].dice === STEM_WEIGHT ? "stem" : "trigram";
10031
+ return { term, df: frequency, bridge: { via, to: candidates.map((c2) => c2.term), dice: candidates[0].dice } };
10032
+ });
10033
+ const unresolvedTerms = diagnostics.filter((d) => d.df === 0 && !d.bridge).map((d) => d.term).sort(byStr);
10034
+ const wholeIdentifier = !/\s/.test(query.trim()) && terms.length ? { term: terms[0], df: df.get(terms[0]) ?? 0 } : void 0;
10035
+ const bridgedOnlyResults = kept.filter((r) => r.bridgedOnly).length;
10036
+ const allBridged = kept.length > 0 && bridgedOnlyResults === kept.length;
10037
+ const missingIdentifier = wholeIdentifier?.df === 0;
10038
+ const verdict = !kept.length ? "none" : missingIdentifier || allBridged ? "weak" : "match";
10039
+ const explain = {
10040
+ query,
10041
+ terms: diagnostics,
10042
+ droppedStopwords: droppedKeywords(query),
10043
+ unresolvedTerms,
10044
+ ...wholeIdentifier ? { wholeIdentifier } : {},
10045
+ verdict,
10046
+ bridgedOnlyResults,
10047
+ resultCount: kept.length
10048
+ };
10049
+ const note = noteFor(explain, missingIdentifier, allBridged);
10050
+ if (note) explain.note = note;
10051
+ return explain;
10052
+ }
10053
+ function noteFor(explain, missingIdentifier, allBridged) {
10054
+ const { verdict, wholeIdentifier, resultCount, terms } = explain;
10055
+ if (verdict === "match") return void 0;
10056
+ if (!resultCount) {
10057
+ return explain.unresolvedTerms.length ? `No file matches. ${explain.unresolvedTerms.length === 1 ? "The term" : "The terms"} ${explain.unresolvedTerms.join(", ")} appear nowhere in this index.` : "No file matches this query.";
10058
+ }
10059
+ if (missingIdentifier && wholeIdentifier) {
10060
+ const parts2 = terms.filter((t) => t.term !== wholeIdentifier.term && t.df > 0).map((t) => t.term).join(", ");
10061
+ const near = terms.find((t) => t.term === wholeIdentifier.term)?.bridge?.to ?? [];
10062
+ const because = parts2 ? ` The ${resultCount} result${resultCount === 1 ? "" : "s"} below match only its parts (${parts2}).` : "";
10063
+ const suggestion = near.length ? ` Closest indexed name${near.length === 1 ? "" : "s"}: ${near.join(", ")}.` : "";
10064
+ return `No file in this index defines or mentions "${wholeIdentifier.term}".${because}${suggestion} If you expected it here, check you are indexing the right branch or commit.`;
10065
+ }
10066
+ if (allBridged) {
10067
+ const bridged = terms.filter((t) => t.bridge);
10068
+ const spelled = bridged.map((t) => `"${t.term}" \u2192 ${t.bridge.to.join(", ")}`).join("; ");
10069
+ return `Nothing matched verbatim. Every result below came from a near match: ${spelled}. Re-run with --exact to see only literal matches.`;
10070
+ }
10071
+ return void 0;
9986
10072
  }
9987
10073
  var K1, DEFAULT_LIMIT, TOP_SYMBOLS, FUZZY_DICE_THRESHOLD, FUZZY_CAP, STEM_WEIGHT, FIELDS, FIELD_WEIGHT, FIELD_B, EXACT_NAME_BOOST, TEST_DEMOTION, TEST_INTENT;
9988
10074
  var init_bm25 = __esm({
@@ -10518,6 +10604,15 @@ function findSymbol(scan2, namePath, opts = {}) {
10518
10604
  m.body = content.split("\n").slice(m.line - 1, end).join("\n");
10519
10605
  }
10520
10606
  }
10607
+ if (opts.concise) {
10608
+ return capped.map((m) => ({
10609
+ name: m.name,
10610
+ kind: m.kind,
10611
+ file: m.file,
10612
+ line: m.line,
10613
+ ...m.body !== void 0 ? { body: m.body } : {}
10614
+ }));
10615
+ }
10521
10616
  return capped;
10522
10617
  }
10523
10618
  function findReferences(scan2, name2) {
@@ -12193,6 +12288,722 @@ var init_endpoint = __esm({
12193
12288
  }
12194
12289
  });
12195
12290
 
12291
+ // src/repomap.ts
12292
+ function renderRepoMap(scan2, graph, opts = {}) {
12293
+ const budgetChars = (opts.budgetTokens ?? 1024) * CHARS_PER_TOKEN;
12294
+ const maxSymbols = opts.maxSymbolsPerFile ?? 8;
12295
+ const ranked = [...graph.files].filter((f) => f.fileKind === "code").sort((a, b) => (b.pagerank ?? 0) - (a.pagerank ?? 0) || b.symbols - a.symbols || byStr(a.rel, b.rel));
12296
+ const records = new Map(scan2.files.map((f) => [f.rel, f]));
12297
+ const header = `# repo map \u2014 ${graph.fileCount} files
12298
+ `;
12299
+ let out2 = header;
12300
+ let files = 0;
12301
+ for (const node of ranked) {
12302
+ const rec = records.get(node.rel);
12303
+ if (!rec) continue;
12304
+ const symbols = [...rec.symbols].filter((s) => s.kind !== "reexport" && s.kind !== "reexport-all").sort((a, b) => Number(b.exported) - Number(a.exported) || a.line - b.line).slice(0, maxSymbols);
12305
+ let block = `
12306
+ ${node.rel}:
12307
+ `;
12308
+ for (const s of symbols) {
12309
+ const sig = (s.signature ?? `${s.kind} ${s.name}`).replace(/\s+/g, " ").trim().slice(0, 120);
12310
+ block += ` ${s.line}: ${sig}
12311
+ `;
12312
+ }
12313
+ if (out2.length + block.length > budgetChars) break;
12314
+ out2 += block;
12315
+ files++;
12316
+ }
12317
+ return `${out2}
12318
+ (${files} of ${ranked.length} code files shown, ~${Math.ceil(out2.length / CHARS_PER_TOKEN)} tokens)
12319
+ `;
12320
+ }
12321
+ var CHARS_PER_TOKEN;
12322
+ var init_repomap = __esm({
12323
+ "src/repomap.ts"() {
12324
+ "use strict";
12325
+ init_sort();
12326
+ CHARS_PER_TOKEN = 4;
12327
+ }
12328
+ });
12329
+
12330
+ // src/coupling.ts
12331
+ function changeCoupling(dir, opts = {}) {
12332
+ const maxCommitFiles = opts.maxCommitFiles ?? 30;
12333
+ const minTogether = opts.minTogether ?? 3;
12334
+ const maxPairs = opts.maxPairs ?? 100;
12335
+ const range = opts.since ? [`${opts.since}..HEAD`] : [];
12336
+ const res = sh("git", ["-C", dir, "-c", "core.quotePath=false", "log", ...range, "--pretty=format:%x1e", "--name-only"]);
12337
+ if (!res.ok) return { ok: false, couplings: [] };
12338
+ const totals = /* @__PURE__ */ new Map();
12339
+ const pairs = /* @__PURE__ */ new Map();
12340
+ for (const block of res.stdout.split("")) {
12341
+ const files = block.split("\n").map((l) => l.trim()).filter(Boolean);
12342
+ if (!files.length || files.length > maxCommitFiles) continue;
12343
+ const unique = [...new Set(files)].sort(byStr);
12344
+ for (const f of unique) totals.set(f, (totals.get(f) ?? 0) + 1);
12345
+ for (let i2 = 0; i2 < unique.length; i2++) {
12346
+ for (let j = i2 + 1; j < unique.length; j++) {
12347
+ const key = `${unique[i2]}${SEP4}${unique[j]}`;
12348
+ pairs.set(key, (pairs.get(key) ?? 0) + 1);
12349
+ }
12350
+ }
12351
+ }
12352
+ const out2 = [];
12353
+ for (const [key, together] of pairs) {
12354
+ if (together < minTogether) continue;
12355
+ const [a, b] = key.split(SEP4);
12356
+ const totalA = totals.get(a) ?? together;
12357
+ const totalB = totals.get(b) ?? together;
12358
+ out2.push({ a, b, together, totalA, totalB, strength: Number((together / Math.min(totalA, totalB)).toFixed(3)) });
12359
+ }
12360
+ out2.sort((x, y) => y.strength - x.strength || y.together - x.together || byStr(x.a, y.a) || byStr(x.b, y.b));
12361
+ return { ok: true, couplings: out2.slice(0, maxPairs) };
12362
+ }
12363
+ function rankHotspots(scan2, churn, top = 20) {
12364
+ const out2 = scan2.files.filter((f) => f.kind === "code").map((f) => {
12365
+ const commits = churn.get(f.rel) ?? 0;
12366
+ return { rel: f.rel, lines: f.lines, commits, score: Number((commits * Math.log2(f.lines + 1)).toFixed(2)) };
12367
+ });
12368
+ out2.sort((a, b) => b.score - a.score || b.lines - a.lines || byStr(a.rel, b.rel));
12369
+ return out2.slice(0, top);
12370
+ }
12371
+ var SEP4;
12372
+ var init_coupling = __esm({
12373
+ "src/coupling.ts"() {
12374
+ "use strict";
12375
+ init_util();
12376
+ init_sort();
12377
+ SEP4 = "\0";
12378
+ }
12379
+ });
12380
+
12381
+ // src/onboard.ts
12382
+ import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
12383
+ import { join as join17, resolve as resolve2 } from "path";
12384
+ function tagline(root) {
12385
+ for (const name2 of README_NAMES) {
12386
+ const path = join17(root, name2);
12387
+ if (!existsSync7(path)) continue;
12388
+ let text;
12389
+ try {
12390
+ text = readFileSync9(path, "utf8");
12391
+ } catch {
12392
+ continue;
12393
+ }
12394
+ for (const raw of text.split(/\n\s*\n/)) {
12395
+ const line = raw.trim();
12396
+ if (!line || line.startsWith("#") || line.startsWith("<")) continue;
12397
+ if (/^(\[!\[|!\[|\[)/.test(line) && !/[.:]\s/.test(line)) continue;
12398
+ const cleaned = line.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*_`]/g, "").replace(/\s+/g, " ").trim();
12399
+ if (cleaned.length > 20) return cleaned.slice(0, 400);
12400
+ }
12401
+ break;
12402
+ }
12403
+ return void 0;
12404
+ }
12405
+ function onboardBrief(scan2, graph, opts = {}) {
12406
+ const lines = [];
12407
+ const name2 = resolve2(scan2.root).replace(/\/+$/, "").split("/").pop() || "repository";
12408
+ lines.push(`# ${name2}`, "");
12409
+ const summary = tagline(scan2.root);
12410
+ if (summary) lines.push(summary, "");
12411
+ const languages = Object.entries(graph.languages).sort((a, b) => b[1] - a[1] || byStr(a[0], b[0])).slice(0, 6).map(([lang, count]) => `${lang} ${count}`).join(", ");
12412
+ lines.push(`**${graph.fileCount} indexed files** \xB7 ${languages}`, "");
12413
+ const workspaces = detectWorkspaces(scan2.root);
12414
+ if (workspaces.packages.length > 1) {
12415
+ lines.push(`## Layout \u2014 monorepo, ${workspaces.packages.length} packages`, "");
12416
+ for (const pkg of workspaces.packages.slice(0, 20)) lines.push(`- \`${pkg.dir}\` \u2014 ${pkg.name}`);
12417
+ if (workspaces.packages.length > 20) lines.push(`- \u2026and ${workspaces.packages.length - 20} more`);
12418
+ if (workspaces.cycle?.length) lines.push("", `\u26A0 dependency cycle: ${workspaces.cycle.join(" \u2192 ")}`);
12419
+ lines.push("");
12420
+ }
12421
+ lines.push("## Key files", "", renderRepoMap(scan2, graph, { budgetTokens: opts.budgetTokens ?? 900 }).trim(), "");
12422
+ const { churn, ok: churnOk } = gitChurn(scan2.root);
12423
+ if (churnOk && churn.size) {
12424
+ const hotspots = rankHotspots(scan2, churn, 8);
12425
+ if (hotspots.length) {
12426
+ lines.push("## Where work concentrates", "", "Files ranked by commits \xD7 size \u2014 where changes and defects cluster.", "");
12427
+ for (const spot of hotspots) lines.push(`- \`${spot.rel}\` \u2014 ${spot.commits} commits, ${spot.lines} lines`);
12428
+ lines.push("");
12429
+ }
12430
+ }
12431
+ lines.push(
12432
+ "## Next",
12433
+ "",
12434
+ "- `search <query>` \u2014 BM25 over names, paths, doc comments and prose; `explain_search` says whether it really matched",
12435
+ "- `find_symbol <Name>` / `symbols_overview <file>` \u2014 read structure without reading files",
12436
+ "- `find_references <Name>` \u2014 three labelled tiers; add `lsp: true` when a language server is configured",
12437
+ ""
12438
+ );
12439
+ const brief = lines.join("\n");
12440
+ if (opts.remember === false) return { brief };
12441
+ const memoryName = opts.memoryName ?? "onboarding";
12442
+ writeMemory(scan2.root, memoryName, brief);
12443
+ return { brief, memory: memoryName };
12444
+ }
12445
+ var README_NAMES;
12446
+ var init_onboard = __esm({
12447
+ "src/onboard.ts"() {
12448
+ "use strict";
12449
+ init_repomap();
12450
+ init_workspaces();
12451
+ init_coupling();
12452
+ init_git();
12453
+ init_memory();
12454
+ init_sort();
12455
+ README_NAMES = ["README.md", "README.markdown", "README.rst", "README.txt", "README"];
12456
+ }
12457
+ });
12458
+
12459
+ // src/lsp/protocol.ts
12460
+ function encodeMessage(msg) {
12461
+ const body2 = JSON.stringify(msg);
12462
+ return `Content-Length: ${byteLength(body2)}${HEADER_END}${body2}`;
12463
+ }
12464
+ function byteLength(s) {
12465
+ return new TextEncoder().encode(s).byteLength;
12466
+ }
12467
+ function createFramer() {
12468
+ const encoder = new TextEncoder();
12469
+ const decoder = new TextDecoder();
12470
+ let buffer = new Uint8Array(0);
12471
+ const append = (chunk) => {
12472
+ const next = new Uint8Array(buffer.length + chunk.length);
12473
+ next.set(buffer);
12474
+ next.set(chunk, buffer.length);
12475
+ buffer = next;
12476
+ };
12477
+ const headerEnd = () => {
12478
+ for (let i2 = 0; i2 + 3 < buffer.length; i2++) {
12479
+ if (buffer[i2] === 13 && buffer[i2 + 1] === 10 && buffer[i2 + 2] === 13 && buffer[i2 + 3] === 10) return i2;
12480
+ }
12481
+ return -1;
12482
+ };
12483
+ return {
12484
+ push(chunk) {
12485
+ append(typeof chunk === "string" ? encoder.encode(chunk) : chunk);
12486
+ const out2 = [];
12487
+ for (; ; ) {
12488
+ const end = headerEnd();
12489
+ if (end < 0) return out2;
12490
+ const headers = decoder.decode(buffer.subarray(0, end));
12491
+ const match = /content-length:\s*(\d+)/i.exec(headers);
12492
+ if (!match) {
12493
+ buffer = buffer.subarray(end + 4);
12494
+ continue;
12495
+ }
12496
+ const length = Number(match[1]);
12497
+ if (!Number.isSafeInteger(length) || length < 0 || length > MAX_FRAME_BYTES) {
12498
+ buffer = buffer.subarray(end + 4);
12499
+ continue;
12500
+ }
12501
+ const start2 = end + 4;
12502
+ if (buffer.length < start2 + length) return out2;
12503
+ const body2 = decoder.decode(buffer.subarray(start2, start2 + length));
12504
+ buffer = buffer.subarray(start2 + length);
12505
+ try {
12506
+ out2.push(JSON.parse(body2));
12507
+ } catch {
12508
+ }
12509
+ }
12510
+ }
12511
+ };
12512
+ }
12513
+ function fileUri(root, rel2) {
12514
+ const abs = `${root.replace(/\/+$/, "")}/${rel2.replace(/^\/+/, "")}`;
12515
+ const drive = /^([A-Za-z]):/.exec(abs);
12516
+ const path = drive ? `/${abs}` : abs;
12517
+ return "file://" + path.split("/").map((segment, i2) => i2 === 0 ? segment : encodeURIComponent(segment)).join("/");
12518
+ }
12519
+ function relFromUri(root, uri) {
12520
+ if (!uri.startsWith("file://")) return void 0;
12521
+ let path = decodeURIComponent(uri.slice("file://".length));
12522
+ if (/^\/[A-Za-z]:/.test(path)) path = path.slice(1);
12523
+ const base = root.replace(/\/+$/, "");
12524
+ if (path === base) return "";
12525
+ if (!path.startsWith(`${base}/`)) return void 0;
12526
+ return path.slice(base.length + 1);
12527
+ }
12528
+ function locationsToRefs(root, raw) {
12529
+ const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" ? [raw] : [];
12530
+ const seen = /* @__PURE__ */ new Set();
12531
+ const out2 = [];
12532
+ for (const item of list) {
12533
+ if (!item || typeof item !== "object") continue;
12534
+ const uri = item.uri ?? item.targetUri;
12535
+ if (!uri) continue;
12536
+ const file = relFromUri(root, uri);
12537
+ if (file === void 0 || file === "") continue;
12538
+ const start2 = (item.range ?? item.targetSelectionRange ?? item.targetRange)?.start;
12539
+ const line = typeof start2?.line === "number" ? start2.line + 1 : 1;
12540
+ const character = typeof start2?.character === "number" ? start2.character : void 0;
12541
+ const key = `${file}:${line}:${character ?? ""}`;
12542
+ if (seen.has(key)) continue;
12543
+ seen.add(key);
12544
+ out2.push(character === void 0 ? { file, line } : { file, line, character });
12545
+ }
12546
+ return out2.sort((a, b) => byStr(a.file, b.file) || a.line - b.line || (a.character ?? 0) - (b.character ?? 0));
12547
+ }
12548
+ var MAX_FRAME_BYTES, HEADER_END;
12549
+ var init_protocol = __esm({
12550
+ "src/lsp/protocol.ts"() {
12551
+ "use strict";
12552
+ init_sort();
12553
+ MAX_FRAME_BYTES = 32 * 1024 * 1024;
12554
+ HEADER_END = "\r\n\r\n";
12555
+ }
12556
+ });
12557
+
12558
+ // src/lsp/client.ts
12559
+ async function openLspSession(transport, options) {
12560
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT;
12561
+ const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT;
12562
+ const framer = createFramer();
12563
+ const pending = /* @__PURE__ */ new Map();
12564
+ const open = /* @__PURE__ */ new Set();
12565
+ let nextId = 1;
12566
+ let dead;
12567
+ const failAll = (error) => {
12568
+ dead = error;
12569
+ for (const [, p] of pending) {
12570
+ clearTimeout(p.timer);
12571
+ p.reject(error);
12572
+ }
12573
+ pending.clear();
12574
+ };
12575
+ transport.onData((chunk) => {
12576
+ for (const message of framer.push(chunk)) {
12577
+ if (typeof message.id !== "number") continue;
12578
+ const waiter = pending.get(message.id);
12579
+ if (!waiter) continue;
12580
+ pending.delete(message.id);
12581
+ clearTimeout(waiter.timer);
12582
+ if (message.error) waiter.reject(new Error(`${message.error.message} (code ${message.error.code})`));
12583
+ else waiter.resolve(message.result);
12584
+ }
12585
+ });
12586
+ transport.onExit((code) => failAll(new Error(`language server exited (code ${code ?? "unknown"})`)));
12587
+ const notify = (method, params) => {
12588
+ if (dead) return;
12589
+ transport.write(encodeMessage({ jsonrpc: "2.0", method, params }));
12590
+ };
12591
+ const request = (method, params, budget = timeoutMs) => {
12592
+ if (dead) return Promise.reject(dead);
12593
+ const id = nextId++;
12594
+ return new Promise((resolve5, reject) => {
12595
+ const timer = setTimeout(() => {
12596
+ pending.delete(id);
12597
+ reject(new LspTimeout(method, budget));
12598
+ }, budget);
12599
+ timer.unref?.();
12600
+ pending.set(id, { resolve: resolve5, reject, timer });
12601
+ transport.write(encodeMessage({ jsonrpc: "2.0", id, method, params }));
12602
+ });
12603
+ };
12604
+ const initResult = await request(
12605
+ "initialize",
12606
+ {
12607
+ processId: null,
12608
+ rootUri: fileUri(options.root, ""),
12609
+ workspaceFolders: [{ uri: fileUri(options.root, ""), name: "repo" }],
12610
+ capabilities: {
12611
+ textDocument: {
12612
+ references: { dynamicRegistration: false },
12613
+ definition: { dynamicRegistration: false, linkSupport: true },
12614
+ implementation: { dynamicRegistration: false, linkSupport: true }
12615
+ }
12616
+ },
12617
+ ...options.initializationOptions !== void 0 ? { initializationOptions: options.initializationOptions } : {}
12618
+ },
12619
+ startupTimeoutMs
12620
+ );
12621
+ notify("initialized", {});
12622
+ const provides = (key) => {
12623
+ const value = initResult?.capabilities?.[key];
12624
+ return value === true || typeof value === "object" && value !== null;
12625
+ };
12626
+ const capabilities = {
12627
+ references: provides("referencesProvider"),
12628
+ definition: provides("definitionProvider"),
12629
+ implementation: provides("implementationProvider"),
12630
+ typeHierarchy: provides("typeHierarchyProvider")
12631
+ };
12632
+ const positionOf = (rel2, line, character) => ({
12633
+ textDocument: { uri: fileUri(options.root, rel2) },
12634
+ // The engine counts lines from 1; LSP counts from 0.
12635
+ position: { line: Math.max(0, line - 1), character }
12636
+ });
12637
+ return {
12638
+ capabilities,
12639
+ didOpen(rel2, text, languageId) {
12640
+ if (open.has(rel2)) return;
12641
+ open.add(rel2);
12642
+ notify("textDocument/didOpen", {
12643
+ textDocument: { uri: fileUri(options.root, rel2), languageId, version: 1, text }
12644
+ });
12645
+ },
12646
+ async references(rel2, line, character) {
12647
+ if (!capabilities.references) return [];
12648
+ const raw = await request("textDocument/references", {
12649
+ ...positionOf(rel2, line, character),
12650
+ context: { includeDeclaration: true }
12651
+ });
12652
+ return locationsToRefs(options.root, raw);
12653
+ },
12654
+ async definition(rel2, line, character) {
12655
+ if (!capabilities.definition) return [];
12656
+ return locationsToRefs(options.root, await request("textDocument/definition", positionOf(rel2, line, character)));
12657
+ },
12658
+ async shutdown() {
12659
+ try {
12660
+ for (const rel2 of open) notify("textDocument/didClose", { textDocument: { uri: fileUri(options.root, rel2) } });
12661
+ open.clear();
12662
+ await request("shutdown", null, Math.min(timeoutMs, 2e3));
12663
+ notify("exit", void 0);
12664
+ } catch {
12665
+ } finally {
12666
+ failAll(new Error("session closed"));
12667
+ transport.close();
12668
+ }
12669
+ }
12670
+ };
12671
+ }
12672
+ var LspTimeout, DEFAULT_TIMEOUT, DEFAULT_STARTUP_TIMEOUT;
12673
+ var init_client = __esm({
12674
+ "src/lsp/client.ts"() {
12675
+ "use strict";
12676
+ init_protocol();
12677
+ LspTimeout = class extends Error {
12678
+ constructor(method, ms) {
12679
+ super(`${method} exceeded ${ms}ms`);
12680
+ this.name = "LspTimeout";
12681
+ }
12682
+ };
12683
+ DEFAULT_TIMEOUT = 5e3;
12684
+ DEFAULT_STARTUP_TIMEOUT = 15e3;
12685
+ }
12686
+ });
12687
+
12688
+ // src/lsp/config.ts
12689
+ import { existsSync as existsSync8, readFileSync as readFileSync10 } from "fs";
12690
+ import { join as join18, resolve as resolve3 } from "path";
12691
+ function resolveLspConfigPath(repo) {
12692
+ const env = process.env.CODEINDEX_LSP_CONFIG;
12693
+ if (env !== void 0) {
12694
+ const trimmed = env.trim();
12695
+ if (!trimmed || trimmed === "0" || trimmed.toLowerCase() === "off") return { path: void 0, source: "none" };
12696
+ return { path: resolve3(trimmed), source: "env" };
12697
+ }
12698
+ const inRepo = join18(repo, LSP_CONFIG_DIR, LSP_CONFIG_NAME);
12699
+ if (existsSync8(inRepo)) return { path: inRepo, source: "repo" };
12700
+ const inCwd = join18(process.cwd(), LSP_CONFIG_DIR, LSP_CONFIG_NAME);
12701
+ if (inCwd !== inRepo && existsSync8(inCwd)) return { path: inCwd, source: "cwd" };
12702
+ return { path: void 0, source: "none" };
12703
+ }
12704
+ function parseLspConfig(payload) {
12705
+ if (!payload || typeof payload !== "object") throw new Error("lsp.json must be a JSON object");
12706
+ const raw = payload;
12707
+ if (raw.version !== 1) throw new Error(`lsp.json: unsupported version ${JSON.stringify(raw.version)} (expected 1)`);
12708
+ if (!Array.isArray(raw.servers)) throw new Error("lsp.json: `servers` must be an array");
12709
+ const ids = /* @__PURE__ */ new Set();
12710
+ const servers = raw.servers.map((entry, i2) => {
12711
+ if (!entry || typeof entry !== "object") throw new Error(`lsp.json: servers[${i2}] must be an object`);
12712
+ const s = entry;
12713
+ const id = typeof s.id === "string" && s.id.trim() ? s.id.trim() : void 0;
12714
+ if (!id) throw new Error(`lsp.json: servers[${i2}].id must be a non-empty string`);
12715
+ if (ids.has(id)) throw new Error(`lsp.json: duplicate server id ${JSON.stringify(id)}`);
12716
+ ids.add(id);
12717
+ if (typeof s.command !== "string" || !s.command.trim()) throw new Error(`lsp.json: servers[${i2}].command must be a non-empty string`);
12718
+ if (!Array.isArray(s.languages) || !s.languages.length || s.languages.some((l) => typeof l !== "string")) {
12719
+ throw new Error(`lsp.json: servers[${i2}].languages must be a non-empty array of strings`);
12720
+ }
12721
+ if (s.args !== void 0 && (!Array.isArray(s.args) || s.args.some((a) => typeof a !== "string"))) {
12722
+ throw new Error(`lsp.json: servers[${i2}].args must be an array of strings`);
12723
+ }
12724
+ return {
12725
+ id,
12726
+ languages: s.languages,
12727
+ ...typeof s.languageId === "string" ? { languageId: s.languageId } : {},
12728
+ command: s.command,
12729
+ ...Array.isArray(s.args) ? { args: s.args } : {},
12730
+ ...s.env && typeof s.env === "object" ? { env: s.env } : {},
12731
+ ...s.initializationOptions !== void 0 ? { initializationOptions: s.initializationOptions } : {},
12732
+ ...typeof s.timeoutMs === "number" ? { timeoutMs: s.timeoutMs } : {},
12733
+ ...typeof s.startupTimeoutMs === "number" ? { startupTimeoutMs: s.startupTimeoutMs } : {}
12734
+ };
12735
+ });
12736
+ return { version: 1, servers };
12737
+ }
12738
+ function loadLspConfig(repo) {
12739
+ const { path } = resolveLspConfigPath(repo);
12740
+ if (!path || !existsSync8(path)) return void 0;
12741
+ let payload;
12742
+ try {
12743
+ payload = JSON.parse(readFileSync10(path, "utf8"));
12744
+ } catch (e) {
12745
+ throw new Error(`${path}: ${e instanceof Error ? e.message : String(e)}`);
12746
+ }
12747
+ return parseLspConfig(payload);
12748
+ }
12749
+ function serverForLang(config, lang) {
12750
+ return config.servers.find((s) => s.languages.includes(lang));
12751
+ }
12752
+ function timeoutFor(server) {
12753
+ return positiveEnv("CODEINDEX_LSP_TIMEOUT_MS") ?? server.timeoutMs ?? DEFAULT_TIMEOUT_MS;
12754
+ }
12755
+ function startupTimeoutFor(server) {
12756
+ return positiveEnv("CODEINDEX_LSP_STARTUP_TIMEOUT_MS") ?? server.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
12757
+ }
12758
+ function positiveEnv(name2) {
12759
+ const raw = process.env[name2];
12760
+ if (raw === void 0) return void 0;
12761
+ const value = Number(raw);
12762
+ return Number.isFinite(value) && value > 0 ? value : void 0;
12763
+ }
12764
+ var LSP_CONFIG_NAME, LSP_CONFIG_DIR, DEFAULT_TIMEOUT_MS, DEFAULT_STARTUP_TIMEOUT_MS;
12765
+ var init_config2 = __esm({
12766
+ "src/lsp/config.ts"() {
12767
+ "use strict";
12768
+ LSP_CONFIG_NAME = "lsp.json";
12769
+ LSP_CONFIG_DIR = ".codeindex";
12770
+ DEFAULT_TIMEOUT_MS = 5e3;
12771
+ DEFAULT_STARTUP_TIMEOUT_MS = 15e3;
12772
+ }
12773
+ });
12774
+
12775
+ // src/lsp/refs.ts
12776
+ import { readFileSync as readFileSync11 } from "fs";
12777
+ import { join as join19 } from "path";
12778
+ function lspUnavailable(server, reason) {
12779
+ return { server, ok: false, reason, refs: [], agreement: { both: [], lspOnly: [], staticOnly: [] } };
12780
+ }
12781
+ function columnOfSymbol(root, rel2, line, name2) {
12782
+ try {
12783
+ const lines = readFileSync11(join19(root, rel2), "utf8").split(/\r?\n/);
12784
+ const index = lines[line - 1]?.indexOf(name2) ?? -1;
12785
+ return index < 0 ? 0 : index;
12786
+ } catch {
12787
+ return 0;
12788
+ }
12789
+ }
12790
+ function agreementOf(refs, statik) {
12791
+ const lspFiles = new Set(refs.map((r) => r.file));
12792
+ const staticFiles = /* @__PURE__ */ new Set([
12793
+ ...statik.callSites.map((c2) => c2.file),
12794
+ ...statik.referencingFiles,
12795
+ // Declaration sites are references in the LSP sense (includeDeclaration),
12796
+ // so counting them keeps the two sides comparing the same population.
12797
+ ...statik.defs.map((d) => d.file)
12798
+ ]);
12799
+ const both = [];
12800
+ const lspOnly = [];
12801
+ const staticOnly = [];
12802
+ for (const file of lspFiles) (staticFiles.has(file) ? both : lspOnly).push(file);
12803
+ for (const file of staticFiles) if (!lspFiles.has(file)) staticOnly.push(file);
12804
+ return { both: both.sort(byStr), lspOnly: lspOnly.sort(byStr), staticOnly: staticOnly.sort(byStr) };
12805
+ }
12806
+ async function annotateWithLsp(scan2, name2, statik, session, serverId, languageId) {
12807
+ if (!session.capabilities.references) {
12808
+ return { ...statik, lsp: lspUnavailable(serverId, "server does not provide textDocument/references") };
12809
+ }
12810
+ if (!statik.defs.length) {
12811
+ return { ...statik, lsp: lspUnavailable(serverId, `no declaration of ${name2} to anchor a request on`) };
12812
+ }
12813
+ const seen = /* @__PURE__ */ new Set();
12814
+ const refs = [];
12815
+ try {
12816
+ for (const def of statik.defs) {
12817
+ const text = readTextOrEmpty(scan2.root, def.file);
12818
+ if (!text) continue;
12819
+ session.didOpen(def.file, text, languageId);
12820
+ const character = columnOfSymbol(scan2.root, def.file, def.line, name2);
12821
+ for (const ref of await session.references(def.file, def.line, character)) {
12822
+ const key = `${ref.file}:${ref.line}:${ref.character ?? ""}`;
12823
+ if (seen.has(key)) continue;
12824
+ seen.add(key);
12825
+ refs.push(ref);
12826
+ }
12827
+ }
12828
+ } catch (e) {
12829
+ return {
12830
+ ...statik,
12831
+ lsp: {
12832
+ server: serverId,
12833
+ ok: false,
12834
+ reason: e instanceof Error ? e.message : String(e),
12835
+ refs: refs.sort(refOrder),
12836
+ agreement: agreementOf(refs, statik)
12837
+ }
12838
+ };
12839
+ }
12840
+ refs.sort(refOrder);
12841
+ return { ...statik, lsp: { server: serverId, ok: true, refs, agreement: agreementOf(refs, statik) } };
12842
+ }
12843
+ function refOrder(a, b) {
12844
+ return byStr(a.file, b.file) || a.line - b.line || (a.character ?? 0) - (b.character ?? 0);
12845
+ }
12846
+ function readTextOrEmpty(root, rel2) {
12847
+ try {
12848
+ return readFileSync11(join19(root, rel2), "utf8");
12849
+ } catch {
12850
+ return "";
12851
+ }
12852
+ }
12853
+ var init_refs = __esm({
12854
+ "src/lsp/refs.ts"() {
12855
+ "use strict";
12856
+ init_sort();
12857
+ }
12858
+ });
12859
+
12860
+ // src/lsp/spawn.ts
12861
+ import { spawn } from "child_process";
12862
+ function spawnLspTransport(server, cwd) {
12863
+ let child;
12864
+ try {
12865
+ child = spawn(server.command, server.args ?? [], {
12866
+ cwd,
12867
+ stdio: ["pipe", "pipe", "pipe"],
12868
+ ...server.env ? { env: { ...process.env, ...server.env } } : {}
12869
+ });
12870
+ } catch {
12871
+ return void 0;
12872
+ }
12873
+ let exited = false;
12874
+ const dataListeners = [];
12875
+ const exitListeners = [];
12876
+ const fireExit = (code) => {
12877
+ if (exited) return;
12878
+ exited = true;
12879
+ for (const listener of exitListeners) listener(code);
12880
+ };
12881
+ child.on("error", () => fireExit(null));
12882
+ child.on("close", (code) => fireExit(code));
12883
+ child.stdout?.on("data", (chunk) => {
12884
+ for (const listener of dataListeners) listener(chunk);
12885
+ });
12886
+ child.stderr?.on("data", () => {
12887
+ });
12888
+ return {
12889
+ write(chunk) {
12890
+ if (exited) return;
12891
+ try {
12892
+ child.stdin?.write(chunk);
12893
+ } catch {
12894
+ fireExit(null);
12895
+ }
12896
+ },
12897
+ onData(cb) {
12898
+ dataListeners.push(cb);
12899
+ },
12900
+ onExit(cb) {
12901
+ if (exited) cb(null);
12902
+ else exitListeners.push(cb);
12903
+ },
12904
+ close() {
12905
+ try {
12906
+ child.stdin?.end();
12907
+ } catch {
12908
+ }
12909
+ const timer = setTimeout(() => {
12910
+ try {
12911
+ child.kill("SIGKILL");
12912
+ } catch {
12913
+ }
12914
+ }, 2e3);
12915
+ timer.unref?.();
12916
+ child.on("close", () => clearTimeout(timer));
12917
+ }
12918
+ };
12919
+ }
12920
+ var init_spawn = __esm({
12921
+ "src/lsp/spawn.ts"() {
12922
+ "use strict";
12923
+ }
12924
+ });
12925
+
12926
+ // src/lsp/index.ts
12927
+ async function lspStatus(scan2, repo, probe = false) {
12928
+ const { path, source } = resolveLspConfigPath(repo);
12929
+ const config = loadLspConfig(repo);
12930
+ if (!config) return { lspVersion: 1, mode: "none", configPath: path ?? null, source, servers: [], unmappedLanguages: [] };
12931
+ const counts = /* @__PURE__ */ new Map();
12932
+ for (const file of scan2.files) counts.set(file.lang, (counts.get(file.lang) ?? 0) + 1);
12933
+ const servers = [];
12934
+ for (const server of config.servers) {
12935
+ const status = {
12936
+ id: server.id,
12937
+ languages: server.languages,
12938
+ command: server.command,
12939
+ onPath: have(server.command),
12940
+ filesInRepo: server.languages.reduce((sum, lang) => sum + (counts.get(lang) ?? 0), 0)
12941
+ };
12942
+ if (probe) {
12943
+ const session = await tryOpen(server, scan2.root);
12944
+ if (session.ok) {
12945
+ status.reachable = true;
12946
+ status.capabilities = session.session.capabilities;
12947
+ await session.session.shutdown();
12948
+ } else {
12949
+ status.reachable = false;
12950
+ status.error = session.reason;
12951
+ }
12952
+ }
12953
+ servers.push(status);
12954
+ }
12955
+ const claimed = new Set(config.servers.flatMap((s) => s.languages));
12956
+ const unmappedLanguages = [...counts.keys()].filter((lang) => !claimed.has(lang) && lang !== "other").sort();
12957
+ return { lspVersion: 1, mode: "configured", configPath: path ?? null, source, servers, unmappedLanguages };
12958
+ }
12959
+ async function tryOpen(server, root) {
12960
+ if (!have(server.command)) return { ok: false, reason: `${server.command} is not on PATH` };
12961
+ const transport = spawnLspTransport(server, root);
12962
+ if (!transport) return { ok: false, reason: `could not start ${server.command}` };
12963
+ try {
12964
+ const session = await openLspSession(transport, {
12965
+ root,
12966
+ timeoutMs: timeoutFor(server),
12967
+ startupTimeoutMs: startupTimeoutFor(server),
12968
+ ...server.initializationOptions !== void 0 ? { initializationOptions: server.initializationOptions } : {}
12969
+ });
12970
+ return { ok: true, session };
12971
+ } catch (e) {
12972
+ transport.close();
12973
+ return { ok: false, reason: e instanceof Error ? e.message : String(e) };
12974
+ }
12975
+ }
12976
+ async function referencesWithLsp(scan2, repo, name2, statik) {
12977
+ let config;
12978
+ try {
12979
+ config = loadLspConfig(repo);
12980
+ } catch (e) {
12981
+ return { ...statik, lsp: lspUnavailable("(config)", e instanceof Error ? e.message : String(e)) };
12982
+ }
12983
+ if (!config) return statik;
12984
+ const lang = statik.defs[0]?.lang;
12985
+ if (!lang) return { ...statik, lsp: lspUnavailable("(none)", `no declaration of ${name2} to anchor a request on`) };
12986
+ const server = serverForLang(config, lang);
12987
+ if (!server) return { ...statik, lsp: lspUnavailable("(none)", `no server configured for ${lang}`) };
12988
+ const opened = await tryOpen(server, scan2.root);
12989
+ if (!opened.ok) return { ...statik, lsp: lspUnavailable(server.id, opened.reason) };
12990
+ try {
12991
+ return await annotateWithLsp(scan2, name2, statik, opened.session, server.id, server.languageId ?? server.languages[0]);
12992
+ } finally {
12993
+ await opened.session.shutdown();
12994
+ }
12995
+ }
12996
+ var init_lsp = __esm({
12997
+ "src/lsp/index.ts"() {
12998
+ "use strict";
12999
+ init_util();
13000
+ init_client();
13001
+ init_config2();
13002
+ init_refs();
13003
+ init_spawn();
13004
+ }
13005
+ });
13006
+
12196
13007
  // src/rules.ts
12197
13008
  function isEntrypointLike(rel2) {
12198
13009
  const base = rel2.split("/").pop();
@@ -12395,96 +13206,6 @@ var init_rules = __esm({
12395
13206
  }
12396
13207
  });
12397
13208
 
12398
- // src/coupling.ts
12399
- function changeCoupling(dir, opts = {}) {
12400
- const maxCommitFiles = opts.maxCommitFiles ?? 30;
12401
- const minTogether = opts.minTogether ?? 3;
12402
- const maxPairs = opts.maxPairs ?? 100;
12403
- const range = opts.since ? [`${opts.since}..HEAD`] : [];
12404
- const res = sh("git", ["-C", dir, "-c", "core.quotePath=false", "log", ...range, "--pretty=format:%x1e", "--name-only"]);
12405
- if (!res.ok) return { ok: false, couplings: [] };
12406
- const totals = /* @__PURE__ */ new Map();
12407
- const pairs = /* @__PURE__ */ new Map();
12408
- for (const block of res.stdout.split("")) {
12409
- const files = block.split("\n").map((l) => l.trim()).filter(Boolean);
12410
- if (!files.length || files.length > maxCommitFiles) continue;
12411
- const unique = [...new Set(files)].sort(byStr);
12412
- for (const f of unique) totals.set(f, (totals.get(f) ?? 0) + 1);
12413
- for (let i2 = 0; i2 < unique.length; i2++) {
12414
- for (let j = i2 + 1; j < unique.length; j++) {
12415
- const key = `${unique[i2]}${SEP4}${unique[j]}`;
12416
- pairs.set(key, (pairs.get(key) ?? 0) + 1);
12417
- }
12418
- }
12419
- }
12420
- const out2 = [];
12421
- for (const [key, together] of pairs) {
12422
- if (together < minTogether) continue;
12423
- const [a, b] = key.split(SEP4);
12424
- const totalA = totals.get(a) ?? together;
12425
- const totalB = totals.get(b) ?? together;
12426
- out2.push({ a, b, together, totalA, totalB, strength: Number((together / Math.min(totalA, totalB)).toFixed(3)) });
12427
- }
12428
- out2.sort((x, y) => y.strength - x.strength || y.together - x.together || byStr(x.a, y.a) || byStr(x.b, y.b));
12429
- return { ok: true, couplings: out2.slice(0, maxPairs) };
12430
- }
12431
- function rankHotspots(scan2, churn, top = 20) {
12432
- const out2 = scan2.files.filter((f) => f.kind === "code").map((f) => {
12433
- const commits = churn.get(f.rel) ?? 0;
12434
- return { rel: f.rel, lines: f.lines, commits, score: Number((commits * Math.log2(f.lines + 1)).toFixed(2)) };
12435
- });
12436
- out2.sort((a, b) => b.score - a.score || b.lines - a.lines || byStr(a.rel, b.rel));
12437
- return out2.slice(0, top);
12438
- }
12439
- var SEP4;
12440
- var init_coupling = __esm({
12441
- "src/coupling.ts"() {
12442
- "use strict";
12443
- init_util();
12444
- init_sort();
12445
- SEP4 = "\0";
12446
- }
12447
- });
12448
-
12449
- // src/repomap.ts
12450
- function renderRepoMap(scan2, graph, opts = {}) {
12451
- const budgetChars = (opts.budgetTokens ?? 1024) * CHARS_PER_TOKEN;
12452
- const maxSymbols = opts.maxSymbolsPerFile ?? 8;
12453
- const ranked = [...graph.files].filter((f) => f.fileKind === "code").sort((a, b) => (b.pagerank ?? 0) - (a.pagerank ?? 0) || b.symbols - a.symbols || byStr(a.rel, b.rel));
12454
- const records = new Map(scan2.files.map((f) => [f.rel, f]));
12455
- const header = `# repo map \u2014 ${graph.fileCount} files
12456
- `;
12457
- let out2 = header;
12458
- let files = 0;
12459
- for (const node of ranked) {
12460
- const rec = records.get(node.rel);
12461
- if (!rec) continue;
12462
- const symbols = [...rec.symbols].filter((s) => s.kind !== "reexport" && s.kind !== "reexport-all").sort((a, b) => Number(b.exported) - Number(a.exported) || a.line - b.line).slice(0, maxSymbols);
12463
- let block = `
12464
- ${node.rel}:
12465
- `;
12466
- for (const s of symbols) {
12467
- const sig = (s.signature ?? `${s.kind} ${s.name}`).replace(/\s+/g, " ").trim().slice(0, 120);
12468
- block += ` ${s.line}: ${sig}
12469
- `;
12470
- }
12471
- if (out2.length + block.length > budgetChars) break;
12472
- out2 += block;
12473
- files++;
12474
- }
12475
- return `${out2}
12476
- (${files} of ${ranked.length} code files shown, ~${Math.ceil(out2.length / CHARS_PER_TOKEN)} tokens)
12477
- `;
12478
- }
12479
- var CHARS_PER_TOKEN;
12480
- var init_repomap = __esm({
12481
- "src/repomap.ts"() {
12482
- "use strict";
12483
- init_sort();
12484
- CHARS_PER_TOKEN = 4;
12485
- }
12486
- });
12487
-
12488
13209
  // src/deadcode.ts
12489
13210
  function findDeadCode(scan2) {
12490
13211
  const callers = callerIndexFor(scan2);
@@ -12594,8 +13315,8 @@ var init_viz = __esm({
12594
13315
  });
12595
13316
 
12596
13317
  // src/mcp/protocol.ts
12597
- import { existsSync as existsSync7 } from "fs";
12598
- import { join as join17 } from "path";
13318
+ import { existsSync as existsSync9 } from "fs";
13319
+ import { join as join20 } from "path";
12599
13320
  import { pathToFileURL as pathToFileURL2 } from "url";
12600
13321
  function validateArgs(schema, args2) {
12601
13322
  const props = schema.properties ?? {};
@@ -12637,7 +13358,7 @@ function negotiateProtocol(requested) {
12637
13358
  function capResponse(text, tool, repo, maxBytes) {
12638
13359
  const bytes = Buffer.byteLength(text, "utf8");
12639
13360
  if (bytes <= maxBytes) return text;
12640
- const artifact = ARTIFACT_FOR[tool] ? join17(repo, INDEX_DIR, ARTIFACT_FOR[tool]) : void 0;
13361
+ const artifact = ARTIFACT_FOR[tool] ? join20(repo, INDEX_DIR, ARTIFACT_FOR[tool]) : void 0;
12641
13362
  return JSON.stringify(
12642
13363
  {
12643
13364
  truncated: true,
@@ -12646,7 +13367,7 @@ function capResponse(text, tool, repo, maxBytes) {
12646
13367
  maxBytes,
12647
13368
  reason: "This response exceeds the configured limit and was withheld rather than sent as an unusable partial payload.",
12648
13369
  narrower: NARROWER[tool] ?? "narrow the request with `scope`, `include`/`exclude`, or a `limit`",
12649
- ...artifact && existsSync7(artifact) ? { artifact, artifactNote: "The full result is already on disk here \u2014 read it directly if you need all of it." } : artifact ? { artifactNote: `Run \`codeindex index --repo ${repo} --out ${join17(repo, INDEX_DIR)}\` to get this as a file.` } : {}
13370
+ ...artifact && existsSync9(artifact) ? { artifact, artifactNote: "The full result is already on disk here \u2014 read it directly if you need all of it." } : artifact ? { artifactNote: `Run \`codeindex index --repo ${repo} --out ${join20(repo, INDEX_DIR)}\` to get this as a file.` } : {}
12650
13371
  },
12651
13372
  null,
12652
13373
  2
@@ -12671,7 +13392,7 @@ function resourceLinkFor(text, tool) {
12671
13392
  };
12672
13393
  }
12673
13394
  var PROTOCOL_VERSIONS, LATEST_PROTOCOL, ANNOTATIONS_SINCE, RICH_TOOLS_SINCE, DEFAULT_MAX_RESPONSE_BYTES, NARROWER, ARTIFACT_FOR;
12674
- var init_protocol = __esm({
13395
+ var init_protocol2 = __esm({
12675
13396
  "src/mcp/protocol.ts"() {
12676
13397
  "use strict";
12677
13398
  init_preload();
@@ -12702,11 +13423,27 @@ function annotationsFor(name2) {
12702
13423
  openWorldHint: meta.openWorld === true
12703
13424
  };
12704
13425
  }
12705
- function toolsFor(defaultRepo, protocolVersion = PROTOCOL_VERSIONS[0]) {
13426
+ function profileNames() {
13427
+ return ["all", ...Object.keys(TOOL_PROFILES).sort()];
13428
+ }
13429
+ function toolsInProfiles(spec) {
13430
+ const names = spec.split(",").map((s) => s.trim()).filter(Boolean);
13431
+ const out2 = /* @__PURE__ */ new Set();
13432
+ for (const name2 of names) {
13433
+ if (name2 === "all") return new Set(TOOLS.map((t) => t.name));
13434
+ const profile = TOOL_PROFILES[name2];
13435
+ if (!profile) throw new Error(`unknown tool profile "${name2}" \u2014 one of: ${profileNames().join(", ")}`);
13436
+ for (const tool of profile) out2.add(tool);
13437
+ }
13438
+ return out2;
13439
+ }
13440
+ function toolsFor(defaultRepo, protocolVersion = PROTOCOL_VERSIONS[0], profile) {
12706
13441
  const withAnnotations = protocolVersion >= ANNOTATIONS_SINCE;
12707
13442
  const withRich = protocolVersion >= RICH_TOOLS_SINCE;
12708
- if (!defaultRepo && !withAnnotations && !withRich) return TOOLS;
12709
- return TOOLS.map((t) => ({
13443
+ const allowed = profile ? toolsInProfiles(profile) : void 0;
13444
+ const TOOLS_ = allowed ? TOOLS.filter((t) => allowed.has(t.name)) : TOOLS;
13445
+ if (!defaultRepo && !withAnnotations && !withRich) return TOOLS_;
13446
+ return TOOLS_.map((t) => ({
12710
13447
  ...t,
12711
13448
  ...withRich && TOOL_META[t.name] ? { title: TOOL_META[t.name].title } : {},
12712
13449
  ...withRich && OUTPUT_SCHEMAS[t.name] ? { outputSchema: OUTPUT_SCHEMAS[t.name] } : {},
@@ -12724,11 +13461,11 @@ function toolsFor(defaultRepo, protocolVersion = PROTOCOL_VERSIONS[0]) {
12724
13461
  }
12725
13462
  }));
12726
13463
  }
12727
- var repoProp, scopeProps, TOOLS, strArr, anyObj, OUTPUT_SCHEMAS, TOOL_META;
13464
+ var repoProp, scopeProps, TOOLS, strArr, anyObj, OUTPUT_SCHEMAS, TOOL_META, TOOL_PROFILES;
12728
13465
  var init_tools = __esm({
12729
13466
  "src/mcp/tools.ts"() {
12730
13467
  "use strict";
12731
- init_protocol();
13468
+ init_protocol2();
12732
13469
  repoProp = { repo: { type: "string", description: "Absolute path to the repository root" } };
12733
13470
  scopeProps = {
12734
13471
  scope: { type: "string", description: "Restrict to one directory (repo-relative)" },
@@ -12796,7 +13533,7 @@ var init_tools = __esm({
12796
13533
  },
12797
13534
  {
12798
13535
  name: "find_symbol",
12799
- description: "Find symbol declarations by name or name path ('Class/method' matches a method inside Class). Options: substring matching, includeBody to return the declaration's source. Exact-name matches rank first.",
13536
+ description: `Find symbol declarations by name or name path ('Class/method' matches a method inside Class). Each match carries its COMPLETE SIGNATURE (parameters and return type) by default, because "what shape is it" is the question that follows "where is it" almost every time and one round trip beats two. Options: substring matching, includeBody for the declaration's source, concise to drop everything but name/kind/file/line when you genuinely only want a location. Exact-name matches rank first.`,
12800
13537
  inputSchema: {
12801
13538
  type: "object",
12802
13539
  properties: {
@@ -12804,6 +13541,10 @@ var init_tools = __esm({
12804
13541
  namePath: { type: "string", description: "Symbol name or Parent/child path" },
12805
13542
  substring: { type: "boolean" },
12806
13543
  includeBody: { type: "boolean" },
13544
+ concise: {
13545
+ type: "boolean",
13546
+ description: "Return only name/kind/file/line \u2014 drop the signature, line span, visibility and language. Roughly 2.5x smaller; use it when you are resolving a path and nothing more (default false)."
13547
+ },
12807
13548
  maxResults: { type: "number", description: "Cap matches (default 50)" }
12808
13549
  },
12809
13550
  required: ["repo", "namePath"]
@@ -12814,10 +13555,42 @@ var init_tools = __esm({
12814
13555
  description: "Who references a symbol? Three labeled tiers: defs (declarations), callSites (line-precise, import-corroborated call bindings), referencingFiles (file-level identifier/doc mentions \u2014 may include homonyms). Confidence decreases across tiers; the labels let you decide what to trust.",
12815
13556
  inputSchema: {
12816
13557
  type: "object",
12817
- properties: { ...repoProp, name: { type: "string", description: "Symbol name" } },
13558
+ properties: {
13559
+ ...repoProp,
13560
+ name: { type: "string", description: "Symbol name" },
13561
+ lsp: {
13562
+ type: "boolean",
13563
+ description: "Also ask a configured language server (see lsp_status) and append an `lsp` block: its references plus an `agreement` matrix (both / lspOnly / staticOnly). The three static tiers are unchanged either way. `staticOnly` is where the homonyms are. No config, no binary, a crash or a timeout all degrade to the static answer with a stated reason (default false)."
13564
+ }
13565
+ },
12818
13566
  required: ["repo", "name"]
12819
13567
  }
12820
13568
  },
13569
+ {
13570
+ name: "lsp_status",
13571
+ description: "Is the optional LSP tier configured, and would it answer? Reports the config path and its source, each server with whether its command is on PATH and how many files in this repo it claims, and the languages no server covers. Opt-in by asset: the tier is active only when <repo>/.codeindex/lsp.json exists (or CODEINDEX_LSP_CONFIG points at one). `probe: true` additionally starts each server to read the capabilities it really advertises. The tier never touches graph.json/symbols.json \u2014 it annotates query answers only.",
13572
+ inputSchema: {
13573
+ type: "object",
13574
+ properties: {
13575
+ ...repoProp,
13576
+ probe: { type: "boolean", description: "Start each server and read its real capabilities (default false: no spawn)" }
13577
+ },
13578
+ required: ["repo"]
13579
+ }
13580
+ },
13581
+ {
13582
+ name: "onboard",
13583
+ description: "One call that says what this repository IS: its own tagline, size and language mix, monorepo layout when there is one, a token-budgeted map of the highest-PageRank files with their key signatures, and where git says work concentrates. Composes scan_summary + workspaces + repo_map + hotspots so the first four round trips of a session become one, and persists the result as the `onboarding` memory (read_memory) so the next session does not repeat them. Set remember:false to skip the write.",
13584
+ inputSchema: {
13585
+ type: "object",
13586
+ properties: {
13587
+ ...repoProp,
13588
+ budgetTokens: { type: "number", description: "Token budget for the key-files section (default 900)" },
13589
+ remember: { type: "boolean", description: "Persist the brief as the `onboarding` memory (default true)" }
13590
+ },
13591
+ required: ["repo"]
13592
+ }
13593
+ },
12821
13594
  {
12822
13595
  name: "repo_map",
12823
13596
  description: "Token-budgeted map of the repository: the highest-PageRank files with their key exported signatures, deterministically rendered to fit `budgetTokens` (default 1024). The densest single read to understand an unfamiliar codebase.",
@@ -12990,6 +13763,14 @@ var init_tools = __esm({
12990
13763
  type: "string",
12991
13764
  description: `Structural prior: "graph" multiplies the lexical score by the file's PageRank over the resolved import graph; "lexical" (default) scores on text alone. Unproven on the judged corpus \u2014 see SearchOptions.rank.`
12992
13765
  },
13766
+ exact: {
13767
+ type: "boolean",
13768
+ description: "Drop results that carry no verbatim query-term match \u2014 the ones the stem/trigram bridge produced (default false)."
13769
+ },
13770
+ explain: {
13771
+ type: "boolean",
13772
+ description: "Wrap the response as `{ results, explain }` with the query verdict (default false = bare array). See explain_search."
13773
+ },
12993
13774
  semantic: {
12994
13775
  type: "boolean",
12995
13776
  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.'
@@ -12998,6 +13779,22 @@ var init_tools = __esm({
12998
13779
  required: ["repo", "query"]
12999
13780
  }
13000
13781
  },
13782
+ {
13783
+ name: "explain_search",
13784
+ description: 'Search, and say whether the query actually found anything. Returns `{ results, explain }` where explain.verdict is "match" (a verbatim term matched), "weak" (results exist but rest on a near match, or the identifier you asked for has document frequency 0) or "none". Use this instead of `search` whenever an empty-feeling or surprising result matters: a query for an identifier that is NOT in the indexed tree still returns confident-looking rows built from its subtokens \u2014 searching "nullGipStep7" in a repo that only has "nullGipStep2" ranks files matching "null" and "gip" \u2014 and only the verdict distinguishes that from a real hit. Also names the terms dropped as stopwords, the terms that exist nowhere, and what each near match bridged to.',
13785
+ inputSchema: {
13786
+ type: "object",
13787
+ properties: {
13788
+ ...repoProp,
13789
+ ...scopeProps,
13790
+ query: { type: "string", description: "Natural-language or identifier query" },
13791
+ limit: { type: "number", description: "Max results (default 20)" },
13792
+ fuzzy: { type: "boolean", description: "Stem/trigram fallback for zero-document-frequency terms (default true)" },
13793
+ exact: { type: "boolean", description: "Drop results carrying no verbatim term match (default false)" }
13794
+ },
13795
+ required: ["repo", "query"]
13796
+ }
13797
+ },
13001
13798
  {
13002
13799
  name: "embed_status",
13003
13800
  description: "Report the embedding tier: the effective mode (none/static/endpoint; endpoint > static model), the resolved model (opt-in, never shipped in the package) with its modelId/dim, EMBED_VERSION, and the configured HTTP endpoint with its reachability. Use to check whether `search` with semantic:true will fuse embeddings or degrade to lexical.",
@@ -13137,6 +13934,45 @@ var init_tools = __esm({
13137
13934
  },
13138
13935
  required: ["defs", "callSites", "referencingFiles"]
13139
13936
  },
13937
+ lsp_status: {
13938
+ type: "object",
13939
+ properties: {
13940
+ lspVersion: { type: "number" },
13941
+ mode: { type: "string", enum: ["none", "configured"] },
13942
+ configPath: { type: ["string", "null"] },
13943
+ source: { type: "string", enum: ["env", "repo", "cwd", "none"] },
13944
+ servers: { type: "array", items: anyObj },
13945
+ unmappedLanguages: strArr
13946
+ },
13947
+ required: ["lspVersion", "mode", "source", "servers", "unmappedLanguages"]
13948
+ },
13949
+ onboard: {
13950
+ type: "object",
13951
+ properties: { brief: { type: "string" }, memory: { type: "string" } },
13952
+ required: ["brief"]
13953
+ },
13954
+ explain_search: {
13955
+ type: "object",
13956
+ properties: {
13957
+ results: { type: "array", items: anyObj },
13958
+ explain: {
13959
+ type: "object",
13960
+ properties: {
13961
+ query: { type: "string" },
13962
+ terms: { type: "array", items: anyObj },
13963
+ droppedStopwords: strArr,
13964
+ unresolvedTerms: strArr,
13965
+ wholeIdentifier: anyObj,
13966
+ verdict: { type: "string", enum: ["match", "weak", "none"] },
13967
+ note: { type: "string" },
13968
+ bridgedOnlyResults: { type: "number" },
13969
+ resultCount: { type: "number" }
13970
+ },
13971
+ required: ["query", "terms", "droppedStopwords", "unresolvedTerms", "verdict", "bridgedOnlyResults", "resultCount"]
13972
+ }
13973
+ },
13974
+ required: ["results", "explain"]
13975
+ },
13140
13976
  hotspots: {
13141
13977
  type: "object",
13142
13978
  properties: { churnOk: { type: "boolean" }, hotspots: { type: "array", items: anyObj } },
@@ -13200,6 +14036,7 @@ var init_tools = __esm({
13200
14036
  find_symbol: { title: "Find symbol" },
13201
14037
  find_references: { title: "Find references" },
13202
14038
  repo_map: { title: "Repository map" },
14039
+ onboard: { title: "Project brief", write: true, destructive: false, idempotent: true },
13203
14040
  hotspots: { title: "Hotspots" },
13204
14041
  coupling: { title: "Change coupling" },
13205
14042
  replace_symbol_body: { title: "Replace symbol body", write: true, destructive: true, idempotent: true },
@@ -13215,18 +14052,35 @@ var init_tools = __esm({
13215
14052
  mermaid: { title: "Mermaid module diagram" },
13216
14053
  grep: { title: "Grep file contents" },
13217
14054
  search: { title: "Lexical search", openWorld: true },
14055
+ explain_search: { title: "Search with a verdict", openWorld: true },
14056
+ // openWorld: it spawns a process the user configured, whose answer this
14057
+ // engine does not determine — the same honesty `search` and `embed_status`
14058
+ // already carry for reaching outside their own artifacts.
14059
+ lsp_status: { title: "LSP tier status", openWorld: true },
13218
14060
  embed_status: { title: "Embedding tier status", openWorld: true },
13219
14061
  type_hierarchy: { title: "Type hierarchy" },
13220
14062
  implementations: { title: "Implementations" },
13221
14063
  call_graph: { title: "Call graph neighborhood" },
13222
14064
  check_rules: { title: "Check architecture rules" }
13223
14065
  };
14066
+ TOOL_PROFILES = {
14067
+ // Land in an unfamiliar repository and get your bearings.
14068
+ orient: ["scan_summary", "repo_map", "onboard", "workspaces", "mermaid", "read_memory", "list_memories"],
14069
+ // Locate a thing.
14070
+ find: ["search", "explain_search", "grep", "find_symbol", "symbols", "symbols_overview"],
14071
+ // Decide whether changing it is safe.
14072
+ impact: ["find_references", "callers", "call_graph", "dead_code", "type_hierarchy", "implementations", "lsp_status"],
14073
+ // Change it.
14074
+ edit: ["find_symbol", "symbols_overview", "replace_symbol_body", "insert_after_symbol", "insert_before_symbol"],
14075
+ // Where the work and the risk concentrate.
14076
+ risk: ["hotspots", "churn", "coupling", "complexity", "check_rules", "duplicated_literals", "dead_code"]
14077
+ };
13224
14078
  }
13225
14079
  });
13226
14080
 
13227
14081
  // src/mcp/session.ts
13228
14082
  import { statSync as statSync5 } from "fs";
13229
- import { join as join18 } from "path";
14083
+ import { join as join21 } from "path";
13230
14084
  function scanFingerprint(scan2) {
13231
14085
  return sha1(scan2.files.map((f) => `${f.rel}:${f.hash}`).join("\n"));
13232
14086
  }
@@ -13240,7 +14094,7 @@ async function memoizedEmbeddingIndex(key, build) {
13240
14094
  function memoizedEmbedModel(modelDir) {
13241
14095
  let stat;
13242
14096
  try {
13243
- stat = statSync5(join18(modelDir, "model.json"));
14097
+ stat = statSync5(join21(modelDir, "model.json"));
13244
14098
  } catch {
13245
14099
  return void 0;
13246
14100
  }
@@ -13354,6 +14208,7 @@ __export(mcp_exports, {
13354
14208
  PROTOCOL_VERSIONS: () => PROTOCOL_VERSIONS,
13355
14209
  TOOLS: () => TOOLS,
13356
14210
  TOOL_META: () => TOOL_META,
14211
+ TOOL_PROFILES: () => TOOL_PROFILES,
13357
14212
  annotationsFor: () => annotationsFor,
13358
14213
  capResponse: () => capResponse,
13359
14214
  getArtifacts: () => getArtifacts,
@@ -13362,18 +14217,20 @@ __export(mcp_exports, {
13362
14217
  memoizedEmbedModel: () => memoizedEmbedModel,
13363
14218
  memoizedEmbeddingIndex: () => memoizedEmbeddingIndex,
13364
14219
  negotiateProtocol: () => negotiateProtocol,
14220
+ profileNames: () => profileNames,
13365
14221
  resourceLinkFor: () => resourceLinkFor,
13366
14222
  runMcpServer: () => runMcpServer,
13367
14223
  scanFingerprint: () => scanFingerprint,
13368
14224
  structuredContentFor: () => structuredContentFor,
13369
14225
  toCacheMap: () => toCacheMap,
13370
14226
  toolsFor: () => toolsFor,
14227
+ toolsInProfiles: () => toolsInProfiles,
13371
14228
  validateArgs: () => validateArgs,
13372
14229
  warmGrammarsForRepo: () => warmGrammarsForRepo,
13373
14230
  warmGrammarsForWalk: () => warmGrammarsForWalk
13374
14231
  });
13375
- import { readFileSync as readFileSync9 } from "fs";
13376
- import { isAbsolute, join as join19 } from "path";
14232
+ import { readFileSync as readFileSync12 } from "fs";
14233
+ import { isAbsolute, join as join22 } from "path";
13377
14234
  import { createInterface } from "readline";
13378
14235
  function str(v) {
13379
14236
  return typeof v === "string" && v ? v : void 0;
@@ -13451,6 +14308,7 @@ async function callTool(name2, args2, defaultRepo) {
13451
14308
  const matches = findSymbol(getScan(repo, scanOpts, walked), namePath, {
13452
14309
  substring: args2.substring === true,
13453
14310
  includeBody: args2.includeBody === true,
14311
+ concise: args2.concise === true,
13454
14312
  maxResults: num(args2.maxResults)
13455
14313
  });
13456
14314
  return JSON.stringify(matches, null, 2);
@@ -13458,7 +14316,13 @@ async function callTool(name2, args2, defaultRepo) {
13458
14316
  if (name2 === "find_references") {
13459
14317
  const symName = str(args2.name);
13460
14318
  if (!symName) throw new Error("`name` is required");
13461
- return JSON.stringify(findReferences(getScan(repo, scanOpts, walked), symName), null, 2);
14319
+ const scan2 = getScan(repo, scanOpts, walked);
14320
+ const statik = findReferences(scan2, symName);
14321
+ if (args2.lsp === true) return JSON.stringify(await referencesWithLsp(scan2, repo, symName, statik), null, 2);
14322
+ return JSON.stringify(statik, null, 2);
14323
+ }
14324
+ if (name2 === "lsp_status") {
14325
+ return JSON.stringify(await lspStatus(getScan(repo, scanOpts, walked), repo, args2.probe === true), null, 2);
13462
14326
  }
13463
14327
  if (name2 === "replace_symbol_body" || name2 === "insert_after_symbol" || name2 === "insert_before_symbol") {
13464
14328
  const namePath = str(args2.namePath);
@@ -13529,6 +14393,18 @@ async function callTool(name2, args2, defaultRepo) {
13529
14393
  const { graph } = getArtifacts(repo, scanOpts, walked);
13530
14394
  return renderMermaid(graph, { module: str(args2.module), maxEdges: num(args2.maxEdges) });
13531
14395
  }
14396
+ if (name2 === "onboard") {
14397
+ const scan2 = getScan(repo, scanOpts, walked);
14398
+ const { graph } = getArtifacts(repo, scanOpts, walked);
14399
+ return JSON.stringify(
14400
+ onboardBrief(scan2, graph, {
14401
+ ...typeof args2.budgetTokens === "number" ? { budgetTokens: args2.budgetTokens } : {},
14402
+ ...args2.remember === false ? { remember: false } : {}
14403
+ }),
14404
+ null,
14405
+ 2
14406
+ );
14407
+ }
13532
14408
  if (name2 === "repo_map") {
13533
14409
  const { scan: scan2, graph } = getArtifacts(repo, scanOpts, walked);
13534
14410
  return renderRepoMap(scan2, graph, { budgetTokens: typeof args2.budgetTokens === "number" ? args2.budgetTokens : void 0 });
@@ -13560,18 +14436,19 @@ async function callTool(name2, args2, defaultRepo) {
13560
14436
  const scan2 = getScan(repo, scanOpts, walked);
13561
14437
  const limit = typeof args2.limit === "number" ? args2.limit : void 0;
13562
14438
  const fuzzy = typeof args2.fuzzy === "boolean" ? args2.fuzzy : void 0;
14439
+ const exactOpt = args2.exact === true ? { exact: true } : {};
13563
14440
  if (args2.semantic === true) {
13564
14441
  const endpoint = resolveEmbedEndpoint();
13565
14442
  if (endpoint) {
13566
14443
  try {
13567
14444
  const index = await memoizedEmbeddingIndex({ mode: "endpoint", identity: endpoint, scan: scan2 }, () => buildEndpointIndex(scan2));
13568
14445
  const queryVec = await encodeQueryViaEndpoint(query);
13569
- const results2 = searchSemantic(scan2, query, index, { queryVec, limit, fuzzy });
13570
- return JSON.stringify({ results: results2, tier: "endpoint" }, null, 2);
14446
+ const results3 = searchSemantic(scan2, query, index, { queryVec, limit, fuzzy });
14447
+ return JSON.stringify({ results: results3, tier: "endpoint" }, null, 2);
13571
14448
  } catch (e) {
13572
- const results2 = searchIndex(scan2, query, { limit, fuzzy, ...rankOpt });
14449
+ const results3 = searchIndex(scan2, query, { limit, fuzzy, ...rankOpt });
13573
14450
  return JSON.stringify(
13574
- { results: results2, tier: "lexical", degradedReason: `embedding endpoint failed: ${errMessage(e)}` },
14451
+ { results: results3, tier: "lexical", degradedReason: `embedding endpoint failed: ${errMessage(e)}` },
13575
14452
  null,
13576
14453
  2
13577
14454
  );
@@ -13584,17 +14461,32 @@ async function callTool(name2, args2, defaultRepo) {
13584
14461
  { mode: "static", identity: `${modelDir}#${model.modelId}`, scan: scan2 },
13585
14462
  () => buildEmbeddingIndex(scan2, model)
13586
14463
  );
13587
- const results2 = searchSemantic(scan2, query, index, { model, limit, fuzzy });
13588
- return JSON.stringify({ results: results2, tier: "static" }, null, 2);
14464
+ const results3 = searchSemantic(scan2, query, index, { model, limit, fuzzy });
14465
+ return JSON.stringify({ results: results3, tier: "static" }, null, 2);
13589
14466
  }
13590
- const results = searchIndex(scan2, query, { limit, fuzzy, ...rankOpt });
14467
+ const results2 = searchIndex(scan2, query, { limit, fuzzy, ...rankOpt });
13591
14468
  return JSON.stringify(
13592
- { results, tier: "lexical", degradedReason: "no embedding endpoint or static model configured \u2014 see embed_status" },
14469
+ { results: results2, tier: "lexical", degradedReason: "no embedding endpoint or static model configured \u2014 see embed_status" },
13593
14470
  null,
13594
14471
  2
13595
14472
  );
13596
14473
  }
13597
- return JSON.stringify(searchIndex(scan2, query, { limit, fuzzy, ...rankOpt }), null, 2);
14474
+ const { results, explain } = explainQuery(scan2, query, { limit, fuzzy, ...exactOpt, ...rankOpt });
14475
+ return JSON.stringify(args2.explain === true ? { results, explain } : results, null, 2);
14476
+ }
14477
+ if (name2 === "explain_search") {
14478
+ const query = str(args2.query);
14479
+ if (!query) throw new Error("`query` is required");
14480
+ const scan2 = getScan(repo, scanOpts, walked);
14481
+ const limit = typeof args2.limit === "number" ? args2.limit : void 0;
14482
+ const fuzzy = typeof args2.fuzzy === "boolean" ? args2.fuzzy : void 0;
14483
+ const { results, explain } = explainQuery(scan2, query, {
14484
+ limit,
14485
+ fuzzy,
14486
+ ...args2.exact === true ? { exact: true } : {},
14487
+ ...rankOpt
14488
+ });
14489
+ return JSON.stringify({ results, explain }, null, 2);
13598
14490
  }
13599
14491
  if (name2 === "embed_status") {
13600
14492
  const modelDir = resolveEmbedModelDir(repo);
@@ -13645,9 +14537,9 @@ async function callTool(name2, args2, defaultRepo) {
13645
14537
  const configPath = str(args2.configPath);
13646
14538
  let payload = args2.rules;
13647
14539
  if (payload === void 0 && configPath) {
13648
- const abs = isAbsolute(configPath) ? configPath : join19(repo, configPath);
14540
+ const abs = isAbsolute(configPath) ? configPath : join22(repo, configPath);
13649
14541
  try {
13650
- payload = JSON.parse(readFileSync9(abs, "utf8"));
14542
+ payload = JSON.parse(readFileSync12(abs, "utf8"));
13651
14543
  } catch (e) {
13652
14544
  throw new Error(`cannot read rules from ${abs}: ${errMessage(e)}`);
13653
14545
  }
@@ -13665,7 +14557,7 @@ async function runMcpServer(opts = {}) {
13665
14557
  version: opts.serverInfo?.version ?? ENGINE_VERSION
13666
14558
  };
13667
14559
  let protocolVersion = PROTOCOL_VERSIONS[0];
13668
- let tools = toolsFor(opts.defaultRepo, protocolVersion);
14560
+ let tools = toolsFor(opts.defaultRepo, protocolVersion, opts.profile);
13669
14561
  const send = (msg) => {
13670
14562
  process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...msg }) + "\n");
13671
14563
  };
@@ -13688,7 +14580,7 @@ async function runMcpServer(opts = {}) {
13688
14580
  try {
13689
14581
  if (req.method === "initialize") {
13690
14582
  protocolVersion = negotiateProtocol(req.params?.protocolVersion);
13691
- tools = toolsFor(opts.defaultRepo, protocolVersion);
14583
+ tools = toolsFor(opts.defaultRepo, protocolVersion, opts.profile);
13692
14584
  send({
13693
14585
  id: req.id,
13694
14586
  result: {
@@ -13758,6 +14650,8 @@ var init_mcp = __esm({
13758
14650
  init_complexity();
13759
14651
  init_viz();
13760
14652
  init_query();
14653
+ init_lsp();
14654
+ init_onboard();
13761
14655
  init_edit();
13762
14656
  init_memory();
13763
14657
  init_bm25();
@@ -13768,10 +14662,10 @@ var init_mcp = __esm({
13768
14662
  init_endpoint();
13769
14663
  init_walk();
13770
14664
  init_tools();
13771
- init_protocol();
14665
+ init_protocol2();
13772
14666
  init_session();
13773
14667
  init_tools();
13774
- init_protocol();
14668
+ init_protocol2();
13775
14669
  init_session();
13776
14670
  SCANLESS_TOOLS = /* @__PURE__ */ new Set([
13777
14671
  "workspaces",
@@ -13984,7 +14878,7 @@ runExtractWorker(workerData.input, (o) => parentPort.postMessage(o)).catch((e) =
13984
14878
  try {
13985
14879
  const outputs = await Promise.all(
13986
14880
  shards.map(
13987
- (jobsForShard) => new Promise((resolve3, reject) => {
14881
+ (jobsForShard) => new Promise((resolve5, reject) => {
13988
14882
  const w = new Worker(bootstrap, {
13989
14883
  eval: true,
13990
14884
  workerData: { input: { jobs: jobsForShard, grammarKeys: wanted, maxCallsPerFile: opts.maxCallsPerFile } }
@@ -13998,7 +14892,7 @@ runExtractWorker(workerData.input, (o) => parentPort.postMessage(o)).catch((e) =
13998
14892
  fn();
13999
14893
  };
14000
14894
  w.once("message", (m) => {
14001
- settle(() => resolve3(m));
14895
+ settle(() => resolve5(m));
14002
14896
  void w.terminate();
14003
14897
  });
14004
14898
  w.once("error", (e) => settle(() => reject(e)));
@@ -14778,6 +15672,13 @@ init_encode();
14778
15672
  init_embed();
14779
15673
  init_search();
14780
15674
  init_endpoint();
15675
+ init_onboard();
15676
+ init_lsp();
15677
+ init_config2();
15678
+ init_client();
15679
+ init_spawn();
15680
+ init_protocol();
15681
+ init_refs();
14781
15682
  init_rules();
14782
15683
  init_coupling();
14783
15684
  init_repomap();
@@ -15177,8 +16078,8 @@ init_util();
15177
16078
  init_types();
15178
16079
  init_types();
15179
16080
  init_loader();
15180
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "fs";
15181
- import { join as join20, resolve as resolve2 } from "path";
16081
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync4 } from "fs";
16082
+ import { join as join23, resolve as resolve4 } from "path";
15182
16083
  init_pipeline();
15183
16084
  init_hash();
15184
16085
  init_graph_json();
@@ -15206,6 +16107,8 @@ init_embed();
15206
16107
  init_search();
15207
16108
  init_endpoint();
15208
16109
  init_util();
16110
+ init_lsp();
16111
+ init_tools();
15209
16112
  var HELP = `codeindex engine v${ENGINE_VERSION} \u2014 deterministic repo indexing
15210
16113
 
15211
16114
  Usage: engine.mjs <command> [flags]
@@ -15239,6 +16142,15 @@ Commands:
15239
16142
  the source with CODEINDEX_EMBED_URL
15240
16143
  embed serve Print (or --run) the docker command that starts the
15241
16144
  containerized embedding server (rich tier)
16145
+ lsp Optional LSP tier (opt-in by asset \u2014 the tier is active only when
16146
+ <repo>/.codeindex/lsp.json exists, or CODEINDEX_LSP_CONFIG points
16147
+ at one). It annotates QUERY answers only and never touches
16148
+ graph.json/symbols.json:
16149
+ lsp status Config path and source, each server with whether
16150
+ its command is on PATH and how many files it
16151
+ claims, and the languages nothing covers (JSON).
16152
+ --probe also starts each server to read the
16153
+ capabilities it really advertises
15242
16154
  grammars Tree-sitter wasm grammars (optional AST tier; regex without them).
15243
16155
  Two tiers: CORE ships with the bundle; EXTENDED (kotlin, elixir,
15244
16156
  zig, solidity, hcl/terraform) arrives only via \`grammars pull\`.
@@ -15284,17 +16196,22 @@ Commands:
15284
16196
  and exits 0, or exits 1 when it has no opinion (run the original).
15285
16197
  Deliberately conservative \u2014 any shell metacharacter or unknown
15286
16198
  flag refuses the rewrite
15287
- mcp Run as an MCP server over stdio (30 tools: scan_summary, graph,
16199
+ mcp Run as an MCP server over stdio (33 tools: scan_summary, graph,
15288
16200
  symbols, callers, workspaces, churn, symbols_overview,
15289
- find_symbol, find_references, repo_map, hotspots, coupling,
15290
- dead_code, complexity, mermaid, grep, search, embed_status,
15291
- check_rules, the memory quartet and the three symbolic-edit
15292
- writes). Flags: --repo <dir> pins ONE repository so the per-tool
15293
- repo argument becomes optional (an explicit per-call repo still
15294
- wins); --server-name <name> overrides the announced serverInfo;
15295
- --max-response-bytes <n> caps a single tool response (default 1e6;
15296
- a response under the cap is byte-identical, one over it is
15297
- replaced by an actionable notice instead of an unusable blob)
16201
+ find_symbol, find_references, lsp_status, onboard, repo_map,
16202
+ hotspots, coupling, dead_code, complexity, mermaid, grep, search,
16203
+ explain_search, embed_status, check_rules, the memory quartet and
16204
+ the three symbolic-edit writes). Flags: --repo <dir> pins ONE
16205
+ repository so the per-tool repo argument becomes optional (an
16206
+ explicit per-call repo still wins); --server-name <name> overrides
16207
+ the announced serverInfo; --max-response-bytes <n> caps a single
16208
+ tool response (default 1e6; a response under the cap is
16209
+ byte-identical, one over it is replaced by an actionable notice
16210
+ instead of an unusable blob); --tools <profile[,profile]>
16211
+ advertises a named subset (all | orient | find | impact | edit |
16212
+ risk, default all) \u2014 every advertised tool's schema costs an agent
16213
+ context on EVERY turn, and a tool left out is still answerable
16214
+ when called by name
15298
16215
  version Print the engine version
15299
16216
 
15300
16217
  Flags (accepted before OR after the subcommand: '--repo X scan' and
@@ -15329,10 +16246,16 @@ Flags (accepted before OR after the subcommand: '--repo X scan' and
15329
16246
  --limit <n> Max results for \`search\` (default 20)
15330
16247
  --no-fuzzy \`search\`: disable trigram fuzzy fallback for query terms
15331
16248
  with zero document frequency (default: enabled)
16249
+ --exact \`search\`: drop results that carry no verbatim term match
16250
+ (the ones the stem/trigram bridge produced)
16251
+ --explain \`search\`: emit { results, explain } \u2014 which terms matched,
16252
+ which bridged, and whether the query really found anything
15332
16253
  --semantic \`search\`: RRF-fuse an embedding tier with lexical \u2014 the
15333
16254
  HTTP endpoint if CODEINDEX_EMBED_ENDPOINT is set, else a
15334
16255
  local static model (lexical-only when neither is available)
15335
16256
  --run \`embed serve\`: run the docker command instead of printing it
16257
+ --probe \`lsp status\`: start each server and read the capabilities
16258
+ it really advertises (default: no spawn)
15336
16259
  --recall \`callers\`: recall-oriented binding (issue #7) \u2014 relaxes
15337
16260
  the JS/TS import gate to unique repo-wide names and labels
15338
16261
  each site corroborated|unique-name
@@ -15358,10 +16281,10 @@ function parseFlags(args2) {
15358
16281
  if (!Number.isFinite(n) || n <= 0) throw new Error(`${a} expects a positive number, got "${raw}"`);
15359
16282
  return n;
15360
16283
  };
15361
- if (a === "--repo") flags2.repo = resolve2(next());
16284
+ if (a === "--repo") flags2.repo = resolve4(next());
15362
16285
  else if (a === "--out") {
15363
16286
  const v = next();
15364
- flags2.out = v === "-" ? "-" : resolve2(v);
16287
+ flags2.out = v === "-" ? "-" : resolve4(v);
15365
16288
  } else if (a === "--project-root") flags2.projectRoot = next();
15366
16289
  else if (a === "--include") flags2.include.push(next());
15367
16290
  else if (a === "--exclude") flags2.exclude.push(next());
@@ -15386,12 +16309,15 @@ function parseFlags(args2) {
15386
16309
  if (!Number.isInteger(n) || n < 0) throw new Error(`--workers expects a non-negative integer, got "${raw}"`);
15387
16310
  flags2.workers = n;
15388
16311
  } else if (a === "--since") flags2.since = next();
15389
- else if (a === "--config") flags2.config = resolve2(next());
16312
+ else if (a === "--config") flags2.config = resolve4(next());
15390
16313
  else if (a === "--limit") flags2.limit = num2();
15391
16314
  else if (a === "--no-fuzzy") flags2.fuzzy = false;
16315
+ else if (a === "--exact") flags2.exact = true;
16316
+ else if (a === "--explain") flags2.explain = true;
15392
16317
  else if (a === "--semantic") flags2.semantic = true;
15393
16318
  else if (a === "--recall") flags2.recall = true;
15394
16319
  else if (a === "--run") flags2.run = true;
16320
+ else if (a === "--probe") flags2.probe = true;
15395
16321
  else if (a === "--base") flags2.base = next();
15396
16322
  else if (a === "--staged") flags2.staged = true;
15397
16323
  else if (a === "--depth") flags2.depth = num2();
@@ -15435,12 +16361,13 @@ function parseMcpFlags(argv) {
15435
16361
  let defaultRepo;
15436
16362
  let name2;
15437
16363
  let maxResponseBytes;
16364
+ let profile;
15438
16365
  for (let i2 = 0; i2 < argv.length; i2++) {
15439
16366
  const a = argv[i2];
15440
16367
  if (a === "--repo") {
15441
16368
  const v = argv[++i2];
15442
16369
  if (!v) throw new Error("--repo requires a directory");
15443
- defaultRepo = resolve2(v);
16370
+ defaultRepo = resolve4(v);
15444
16371
  } else if (a === "--server-name") {
15445
16372
  const v = argv[++i2];
15446
16373
  if (!v) throw new Error("--server-name requires a value");
@@ -15450,12 +16377,17 @@ function parseMcpFlags(argv) {
15450
16377
  const n = Number(v);
15451
16378
  if (!v || !Number.isFinite(n) || n <= 0) throw new Error("--max-response-bytes requires a positive number");
15452
16379
  maxResponseBytes = n;
16380
+ } else if (a === "--tools") {
16381
+ const v = argv[++i2];
16382
+ if (!v) throw new Error(`--tools requires a profile: ${profileNames().join(", ")}`);
16383
+ toolsInProfiles(v);
16384
+ profile = v === "all" ? void 0 : v;
15453
16385
  } else {
15454
16386
  throw new Error(`unknown flag for \`mcp\`: ${a}`);
15455
16387
  }
15456
16388
  }
15457
- if (defaultRepo && !existsSync8(defaultRepo)) throw new Error(`--repo path does not exist: ${defaultRepo}`);
15458
- return { defaultRepo, serverInfo: name2 ? { name: name2 } : void 0, maxResponseBytes };
16389
+ if (defaultRepo && !existsSync10(defaultRepo)) throw new Error(`--repo path does not exist: ${defaultRepo}`);
16390
+ return { defaultRepo, serverInfo: name2 ? { name: name2 } : void 0, maxResponseBytes, profile };
15459
16391
  }
15460
16392
  var VALUE_FLAGS = /* @__PURE__ */ new Set([
15461
16393
  "--repo",
@@ -15476,6 +16408,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
15476
16408
  "--config",
15477
16409
  "--limit",
15478
16410
  "--server-name",
16411
+ "--tools",
15479
16412
  "--workers",
15480
16413
  "--index",
15481
16414
  "--max-response-bytes"
@@ -15523,7 +16456,7 @@ async function runCli(rawArgv) {
15523
16456
  return;
15524
16457
  }
15525
16458
  const flags2 = parseFlags(rest);
15526
- if (!existsSync8(flags2.repo)) throw new Error(`--repo path does not exist: ${flags2.repo}`);
16459
+ if (!existsSync10(flags2.repo)) throw new Error(`--repo path does not exist: ${flags2.repo}`);
15527
16460
  const scans = !SCANLESS_COMMANDS.has(cmd) && !(cmd === "embed" && flags2.positional !== "build");
15528
16461
  let precomputedWalk;
15529
16462
  if (scans && !flags2.noAst) {
@@ -15557,11 +16490,11 @@ async function runCli(rawArgv) {
15557
16490
  if (!flags2.out) throw new Error("index needs --out <dir>");
15558
16491
  const outDir = flags2.out;
15559
16492
  mkdirSync3(outDir, { recursive: true });
15560
- const cachePath = join20(outDir, "cache.json");
16493
+ const cachePath = join23(outDir, "cache.json");
15561
16494
  let cache;
15562
16495
  let meta = {};
15563
16496
  try {
15564
- const parsed = JSON.parse(readFileSync10(cachePath, "utf8"));
16497
+ const parsed = JSON.parse(readFileSync13(cachePath, "utf8"));
15565
16498
  if (parsed.schemaVersion === SCHEMA_VERSION && parsed.extractorVersion === EXTRACTOR_VERSION) {
15566
16499
  cache = new Map(Object.entries(parsed.files));
15567
16500
  meta = {
@@ -15582,12 +16515,12 @@ async function runCli(rawArgv) {
15582
16515
  });
15583
16516
  const modelDir = resolveEmbedModelDir(flags2.repo);
15584
16517
  const model = modelDir ? loadEmbedModel(modelDir) : void 0;
15585
- const graphPath = join20(outDir, "graph.json");
15586
- const symbolsPath = join20(outDir, "symbols.json");
15587
- const embedPath = join20(outDir, "embeddings.bin");
16518
+ const graphPath = join23(outDir, "graph.json");
16519
+ const symbolsPath = join23(outDir, "symbols.json");
16520
+ const embedPath = join23(outDir, "embeddings.bin");
15588
16521
  const artifactSha = (path) => {
15589
16522
  try {
15590
- return sha1(readFileSync10(path));
16523
+ return sha1(readFileSync13(path));
15591
16524
  } catch {
15592
16525
  return void 0;
15593
16526
  }
@@ -15660,7 +16593,7 @@ async function runCli(rawArgv) {
15660
16593
  } else if (cmd === "scip") {
15661
16594
  const scan2 = readScan();
15662
16595
  const bytes = renderScip(scan2, { projectRoot: flags2.projectRoot });
15663
- const out2 = flags2.out ?? resolve2("index.scip");
16596
+ const out2 = flags2.out ?? resolve4("index.scip");
15664
16597
  if (out2 === "-") process.stdout.write(Buffer.from(bytes));
15665
16598
  else {
15666
16599
  writeFileSync4(out2, bytes);
@@ -15707,10 +16640,21 @@ async function runCli(rawArgv) {
15707
16640
  } else if (cmd === "search") {
15708
16641
  if (!flags2.positional) throw new Error('search needs a query: cli.mjs search "<query>" --repo <dir>');
15709
16642
  const scan2 = readScan();
16643
+ const searchOpts = {
16644
+ limit: flags2.limit,
16645
+ fuzzy: flags2.fuzzy,
16646
+ ...flags2.exact ? { exact: true } : {},
16647
+ ...flags2.rank ? { rank: flags2.rank } : {}
16648
+ };
16649
+ const warnIfWeak = () => {
16650
+ const { explain } = explainQuery(scan2, flags2.positional, searchOpts);
16651
+ if (explain.note) process.stderr.write(`codeindex: ${explain.note}
16652
+ `);
16653
+ };
15710
16654
  if (flags2.semantic) {
15711
16655
  const endpoint = resolveEmbedEndpoint();
15712
16656
  const lexical = () => {
15713
- const results = searchIndex(scan2, flags2.positional, { limit: flags2.limit, fuzzy: flags2.fuzzy, ...flags2.rank ? { rank: flags2.rank } : {} });
16657
+ const results = searchIndex(scan2, flags2.positional, searchOpts);
15714
16658
  emit(JSON.stringify(results, null, 2) + "\n", flags2.out);
15715
16659
  };
15716
16660
  if (endpoint) {
@@ -15740,9 +16684,12 @@ async function runCli(rawArgv) {
15740
16684
  emit(JSON.stringify(results, null, 2) + "\n", flags2.out);
15741
16685
  }
15742
16686
  }
16687
+ warnIfWeak();
15743
16688
  } else {
15744
- const results = searchIndex(scan2, flags2.positional, { limit: flags2.limit, fuzzy: flags2.fuzzy, ...flags2.rank ? { rank: flags2.rank } : {} });
15745
- emit(JSON.stringify(results, null, 2) + "\n", flags2.out);
16689
+ const { results, explain } = explainQuery(scan2, flags2.positional, searchOpts);
16690
+ emit(JSON.stringify(flags2.explain ? { results, explain } : results, null, 2) + "\n", flags2.out);
16691
+ if (explain.note) process.stderr.write(`codeindex: ${explain.note}
16692
+ `);
15746
16693
  }
15747
16694
  } else if (cmd === "embed") {
15748
16695
  const sub = flags2.positional;
@@ -15799,14 +16746,14 @@ async function runCli(rawArgv) {
15799
16746
  mkdirSync3(flags2.out, { recursive: true });
15800
16747
  const scan2 = readScan();
15801
16748
  const index = buildEmbeddingIndex(scan2, model);
15802
- writeFileSync4(join20(flags2.out, "embeddings.bin"), serializeEmbeddings(index));
16749
+ writeFileSync4(join23(flags2.out, "embeddings.bin"), serializeEmbeddings(index));
15803
16750
  process.stderr.write(`codeindex: ${index.records.length} embedding records \u2192 ${flags2.out}/embeddings.bin (model ${model.modelId})
15804
16751
  `);
15805
16752
  } else if (sub === "pull") {
15806
16753
  const { url, sha256 } = resolveEmbedPullUrl();
15807
- const destDir = process.env.CODEINDEX_EMBED_DIR ?? join20(flags2.repo, ".codeindex", "models");
16754
+ const destDir = process.env.CODEINDEX_EMBED_DIR ?? join23(flags2.repo, ".codeindex", "models");
15808
16755
  mkdirSync3(destDir, { recursive: true });
15809
- process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${join20(destDir, "model.json")}
16756
+ process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${join23(destDir, "model.json")}
15810
16757
  `);
15811
16758
  let body2;
15812
16759
  try {
@@ -15827,18 +16774,22 @@ async function runCli(rawArgv) {
15827
16774
  process.exitCode = 1;
15828
16775
  return;
15829
16776
  }
15830
- writeFileSync4(join20(destDir, "model.json"), body2);
15831
- process.stderr.write(`codeindex: model written to ${join20(destDir, "model.json")}
16777
+ writeFileSync4(join23(destDir, "model.json"), body2);
16778
+ process.stderr.write(`codeindex: model written to ${join23(destDir, "model.json")}
15832
16779
  `);
15833
16780
  } else {
15834
16781
  throw new Error("embed needs a subcommand: status | build | pull | serve");
15835
16782
  }
16783
+ } else if (cmd === "lsp") {
16784
+ const sub = flags2.positional;
16785
+ if (sub !== "status") throw new Error("lsp needs a subcommand: status");
16786
+ emit(JSON.stringify(await lspStatus(readScan(), flags2.repo, flags2.probe === true), null, 2) + "\n", flags2.out);
15836
16787
  } else if (cmd === "grammars") {
15837
16788
  const sub = flags2.positional;
15838
16789
  const cacheDir = sharedGrammarsCacheDir();
15839
16790
  if (sub === "status") {
15840
16791
  const info2 = resolveGrammarsTier();
15841
- const present = (name2) => info2.dirs.some((d) => existsSync8(join20(d, name2)));
16792
+ const present = (name2) => info2.dirs.some((d) => existsSync10(join23(d, name2)));
15842
16793
  const runtimePresent = present("web-tree-sitter.wasm");
15843
16794
  const target = resolveGrammarsPullTarget();
15844
16795
  const resolvedIn = (keys) => [...keys].filter((k) => present(`${k}.wasm`)).sort();
@@ -15870,7 +16821,7 @@ async function runCli(rawArgv) {
15870
16821
  }
15871
16822
  } else if (cmd === "rules") {
15872
16823
  if (!flags2.config) throw new Error("rules needs --config <codeindex.rules.json>");
15873
- const rules = parseRules(JSON.parse(readFileSync10(flags2.config, "utf8")));
16824
+ const rules = parseRules(JSON.parse(readFileSync13(flags2.config, "utf8")));
15874
16825
  const { graph } = readArtifacts();
15875
16826
  const violations = checkRules(graph, rules);
15876
16827
  const errors = violations.filter((v) => v.severity === "error").length;
@@ -15970,9 +16921,12 @@ export {
15970
16921
  EXTRACTOR_VERSION,
15971
16922
  EXT_GRAMMAR,
15972
16923
  INDEX_DIR,
16924
+ LspTimeout,
15973
16925
  MARKDOWN_EXT,
16926
+ MAX_FRAME_BYTES,
15974
16927
  RISK_WEIGHTS,
15975
16928
  SCHEMA_VERSION,
16929
+ agreementOf,
15976
16930
  allGrammarKeys,
15977
16931
  applyCentrality,
15978
16932
  basicTokenize,
@@ -15999,6 +16953,7 @@ export {
15999
16953
  classify,
16000
16954
  clip,
16001
16955
  clipInline,
16956
+ columnOfSymbol,
16002
16957
  communityOf,
16003
16958
  compileGlobs,
16004
16959
  complexityOfSource,
@@ -16007,6 +16962,7 @@ export {
16007
16962
  computeSurprises,
16008
16963
  computeSymbolRefs,
16009
16964
  computeTestMap,
16965
+ createFramer,
16010
16966
  deleteMemory,
16011
16967
  deltaFor,
16012
16968
  deserializeEmbeddings,
@@ -16019,9 +16975,11 @@ export {
16019
16975
  embeddingUnits,
16020
16976
  enclosingSymbol,
16021
16977
  encode,
16978
+ encodeMessage,
16022
16979
  encodeQueryViaEndpoint,
16023
16980
  ensureGrammars,
16024
16981
  escapeRegExp,
16982
+ explainQuery,
16025
16983
  extToLang,
16026
16984
  extractAst,
16027
16985
  extractCode,
@@ -16033,6 +16991,7 @@ export {
16033
16991
  extractTarInto,
16034
16992
  fetchExpectedSha256,
16035
16993
  fetchGrammarsTarball,
16994
+ fileUri,
16036
16995
  findDeadCode,
16037
16996
  findLiteralDuplications,
16038
16997
  findReferences,
@@ -16066,10 +17025,17 @@ export {
16066
17025
  languageOf,
16067
17026
  listMemories,
16068
17027
  loadEmbedModel,
17028
+ loadLspConfig,
17029
+ locationsToRefs,
17030
+ lspStatus,
17031
+ lspUnavailable,
16069
17032
  neighborhood,
16070
17033
  neighborsOf,
17034
+ onboardBrief,
17035
+ openLspSession,
16071
17036
  pagerankOf,
16072
17037
  parseGitignore,
17038
+ parseLspConfig,
16073
17039
  parseRules,
16074
17040
  preloadArtifacts,
16075
17041
  preloadSession,
@@ -16081,6 +17047,8 @@ export {
16081
17047
  readMemory,
16082
17048
  readPersistedIndex,
16083
17049
  readText,
17050
+ referencesWithLsp,
17051
+ relFromUri,
16084
17052
  renderGraphJson,
16085
17053
  renderMermaid,
16086
17054
  renderMermaidClustered,
@@ -16098,6 +17066,7 @@ export {
16098
17066
  resolveGrammarsPullTarget,
16099
17067
  resolveGrammarsTier,
16100
17068
  resolveImport,
17069
+ resolveLspConfigPath,
16101
17070
  resolveRelationEdges,
16102
17071
  resolveRelations,
16103
17072
  resolveUniqueSymbol,
@@ -16115,11 +17084,13 @@ export {
16115
17084
  searchIndex,
16116
17085
  searchSemantic,
16117
17086
  serializeEmbeddings,
17087
+ serverForLang,
16118
17088
  sh,
16119
17089
  sha1,
16120
17090
  sharedGrammarsCacheDir,
16121
17091
  shortHash,
16122
17092
  slugify,
17093
+ spawnLspTransport,
16123
17094
  subtokens,
16124
17095
  symbolComplexity,
16125
17096
  symbolId,