@maxgfr/codeindex 2.10.0 → 2.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,9 +14,9 @@ var ENGINE_VERSION, SCHEMA_VERSION, EXTRACTOR_VERSION;
14
14
  var init_types = __esm({
15
15
  "src/types.ts"() {
16
16
  "use strict";
17
- ENGINE_VERSION = "2.10.0";
17
+ ENGINE_VERSION = "2.11.1";
18
18
  SCHEMA_VERSION = 4;
19
- EXTRACTOR_VERSION = 6;
19
+ EXTRACTOR_VERSION = 8;
20
20
  }
21
21
  });
22
22
 
@@ -731,7 +731,57 @@ function scan(rel, content, lang, rules) {
731
731
  function extToLang(ext) {
732
732
  return EXT_LANG[ext] ?? "other";
733
733
  }
734
- var EXT_LANG;
734
+ function extractReexports(rel, content, localSymbols) {
735
+ if (!REEXPORT_EXTS.has(rel.slice(rel.lastIndexOf(".")))) return [];
736
+ const lang = /\.(ts|tsx|mts|cts)$/.test(rel) ? "typescript" : "javascript";
737
+ const out2 = [];
738
+ const seen = /* @__PURE__ */ new Set();
739
+ const lineAt = (idx) => content.slice(0, idx).split(/\r?\n/).length;
740
+ const localKindOf = /* @__PURE__ */ new Map();
741
+ for (const s of localSymbols) if (!localKindOf.has(s.name)) localKindOf.set(s.name, s.kind);
742
+ const named = /export\s*\{([\s\S]*?)\}\s*(?:from\s*['"]([^'"]+)['"])?\s*;?/g;
743
+ let m;
744
+ while ((m = named.exec(content)) && out2.length < 60) {
745
+ const from = m[2];
746
+ for (const part of m[1].split(",")) {
747
+ const p = part.trim().replace(/^type\s+/, "");
748
+ const as = /^(\S+)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(p);
749
+ const orig = as ? as[1] : p;
750
+ const name2 = as ? as[2] : p;
751
+ if (!/^[A-Za-z_$][\w$]*$/.test(name2) || name2 === "default" || seen.has(name2)) continue;
752
+ seen.add(name2);
753
+ const mirroredKind = !from ? localKindOf.get(orig) : void 0;
754
+ out2.push({
755
+ name: name2,
756
+ kind: mirroredKind ?? "reexport",
757
+ file: rel,
758
+ line: lineAt(m.index),
759
+ signature: from ? `export { ${name2} } from "${from}"` : `export { ${name2} }`,
760
+ exported: true,
761
+ lang
762
+ });
763
+ }
764
+ }
765
+ const star = /export\s*\*\s*(?:as\s+([A-Za-z_$][\w$]*)\s+)?from\s*['"]([^'"]+)['"]/g;
766
+ while ((m = star.exec(content)) && out2.length < 60) {
767
+ const ns = m[1];
768
+ const from = m[2];
769
+ const key = "*" + (ns ?? from);
770
+ if (seen.has(key)) continue;
771
+ seen.add(key);
772
+ out2.push({
773
+ name: ns ?? `* (${from})`,
774
+ kind: ns ? "reexport" : "reexport-all",
775
+ file: rel,
776
+ line: lineAt(m.index),
777
+ signature: `export * ${ns ? `as ${ns} ` : ""}from "${from}"`,
778
+ exported: true,
779
+ lang
780
+ });
781
+ }
782
+ return out2;
783
+ }
784
+ var EXT_LANG, REEXPORT_EXTS;
735
785
  var init_common = __esm({
736
786
  "src/lang/common.ts"() {
737
787
  "use strict";
@@ -799,6 +849,7 @@ var init_common = __esm({
799
849
  ".svelte": "svelte",
800
850
  ".astro": "astro"
801
851
  };
852
+ REEXPORT_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
802
853
  }
803
854
  });
804
855
 
@@ -1231,12 +1282,18 @@ var init_scala = __esm({
1231
1282
  // src/lang/registry.ts
1232
1283
  function extractSymbols(rel, ext, content) {
1233
1284
  const extractor = BY_EXT.get(ext);
1234
- if (!extractor) return [];
1235
- try {
1236
- return extractor.extract(rel, content);
1237
- } catch {
1238
- return [];
1285
+ let symbols;
1286
+ if (!extractor) symbols = [];
1287
+ else {
1288
+ try {
1289
+ symbols = extractor.extract(rel, content);
1290
+ } catch {
1291
+ symbols = [];
1292
+ }
1239
1293
  }
1294
+ const known = new Set(symbols.map((s) => s.name));
1295
+ const reexports = extractReexports(rel, content, symbols).filter((s) => !known.has(s.name));
1296
+ return reexports.length ? [...symbols, ...reexports] : symbols;
1240
1297
  }
1241
1298
  function languageOf(ext) {
1242
1299
  return BY_EXT.get(ext)?.lang ?? extToLang(ext);
@@ -6340,54 +6397,9 @@ function extractImports(ext, content) {
6340
6397
  }
6341
6398
  return [...specs].map((spec) => ({ kind: "import", spec }));
6342
6399
  }
6343
- function extractReexports(rel, content) {
6344
- if (!JS_TS.has(rel.slice(rel.lastIndexOf(".")))) return [];
6345
- const lang = /\.(ts|tsx|mts|cts)$/.test(rel) ? "typescript" : "javascript";
6346
- const out2 = [];
6347
- const seen = /* @__PURE__ */ new Set();
6348
- const lineAt = (idx) => content.slice(0, idx).split(/\r?\n/).length;
6349
- const named = /export\s*\{([\s\S]*?)\}\s*(?:from\s*['"]([^'"]+)['"])?\s*;?/g;
6350
- let m;
6351
- while ((m = named.exec(content)) && out2.length < 60) {
6352
- const from = m[2];
6353
- for (const part of m[1].split(",")) {
6354
- const p = part.trim().replace(/^type\s+/, "");
6355
- const as = /^(\S+)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(p);
6356
- const name2 = as ? as[2] : p;
6357
- if (!/^[A-Za-z_$][\w$]*$/.test(name2) || name2 === "default" || seen.has(name2)) continue;
6358
- seen.add(name2);
6359
- out2.push({
6360
- name: name2,
6361
- kind: "reexport",
6362
- file: rel,
6363
- line: lineAt(m.index),
6364
- signature: from ? `export { ${name2} } from "${from}"` : `export { ${name2} }`,
6365
- exported: true,
6366
- lang
6367
- });
6368
- }
6369
- }
6370
- const star = /export\s*\*\s*(?:as\s+([A-Za-z_$][\w$]*)\s+)?from\s*['"]([^'"]+)['"]/g;
6371
- while ((m = star.exec(content)) && out2.length < 60) {
6372
- const ns = m[1];
6373
- const from = m[2];
6374
- const key = "*" + (ns ?? from);
6375
- if (seen.has(key)) continue;
6376
- seen.add(key);
6377
- out2.push({
6378
- name: ns ?? `* (${from})`,
6379
- kind: ns ? "reexport" : "reexport-all",
6380
- file: rel,
6381
- line: lineAt(m.index),
6382
- signature: `export * ${ns ? `as ${ns} ` : ""}from "${from}"`,
6383
- exported: true,
6384
- lang
6385
- });
6386
- }
6387
- return out2;
6388
- }
6389
- function collectCallsRegex(content) {
6400
+ function collectCallsRegex(content, symbols = []) {
6390
6401
  const out2 = /* @__PURE__ */ new Map();
6402
+ const ownDefLines = new Set(symbols.map((s) => `${s.name} ${s.line}`));
6391
6403
  const lines = content.split("\n");
6392
6404
  const CALL_RE = /(?:\bnew\s+)?(?:([A-Za-z_$][\w$]*)\s*\.\s*)?([A-Za-z_$][\w$]*)\s*\(/g;
6393
6405
  for (let i2 = 0; i2 < lines.length && out2.size < 512; i2++) {
@@ -6395,13 +6407,28 @@ function collectCallsRegex(content) {
6395
6407
  const trimmed = line.trimStart();
6396
6408
  if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*")) continue;
6397
6409
  CALL_RE.lastIndex = 0;
6410
+ let probe;
6411
+ const introducerCaught = /* @__PURE__ */ new Set();
6412
+ while ((probe = CALL_RE.exec(line)) !== null) {
6413
+ const name2 = probe[2];
6414
+ const key = `${name2} ${i2 + 1}`;
6415
+ if (ownDefLines.has(key) && DEF_INTRODUCERS.test(line.slice(0, probe.index))) introducerCaught.add(key);
6416
+ }
6417
+ CALL_RE.lastIndex = 0;
6398
6418
  let m;
6419
+ const fallbackExcluded = /* @__PURE__ */ new Set();
6399
6420
  while ((m = CALL_RE.exec(line)) !== null && out2.size < 512) {
6400
6421
  const receiver = m[1];
6401
6422
  const name2 = m[2];
6402
6423
  if (name2.length < 2 || CALL_KEYWORDS.has(name2)) continue;
6403
6424
  if (DEF_INTRODUCERS.test(line.slice(0, m.index))) continue;
6404
6425
  const key = `${name2} ${i2 + 1}`;
6426
+ if (ownDefLines.has(key) && !introducerCaught.has(key)) {
6427
+ if (!fallbackExcluded.has(key)) {
6428
+ fallbackExcluded.add(key);
6429
+ continue;
6430
+ }
6431
+ }
6405
6432
  if (!out2.has(key)) out2.set(key, receiver ? { name: name2, line: i2 + 1, receiver } : { name: name2, line: i2 + 1 });
6406
6433
  }
6407
6434
  }
@@ -6411,7 +6438,7 @@ function extractCode(rel, ext, content) {
6411
6438
  const ast = extractAst(rel, ext, content);
6412
6439
  const symbols = (ast ? ast.symbols : extractSymbols(rel, ext, content)).slice(0, 400);
6413
6440
  const known = new Set(symbols.map((s) => s.name));
6414
- const reexports = extractReexports(rel, content).filter((s) => !known.has(s.name));
6441
+ const reexports = extractReexports(rel, content, symbols).filter((s) => !known.has(s.name));
6415
6442
  return {
6416
6443
  symbols: [...symbols, ...reexports],
6417
6444
  summary: topDocComment(content),
@@ -6422,7 +6449,9 @@ function extractCode(rel, ext, content) {
6422
6449
  idents: ast?.idents,
6423
6450
  // AST call sites when a grammar parsed the file; the conservative regex
6424
6451
  // collector otherwise, so caller indexes exist without the wasm sidecar.
6425
- calls: ast ? ast.calls : collectCallsRegex(content),
6452
+ // `symbols` (this file's own regex-extracted defs) lets the collector
6453
+ // exclude a definition's own name+line from its call candidates.
6454
+ calls: ast ? ast.calls : collectCallsRegex(content, symbols),
6426
6455
  importedNames: ast?.importedNames
6427
6456
  };
6428
6457
  }
@@ -6432,6 +6461,7 @@ var init_code = __esm({
6432
6461
  "use strict";
6433
6462
  init_registry();
6434
6463
  init_extract();
6464
+ init_common();
6435
6465
  JS_TS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
6436
6466
  PY = /* @__PURE__ */ new Set([".py", ".pyi"]);
6437
6467
  C_CPP = /* @__PURE__ */ new Set([".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hh"]);
@@ -9447,30 +9477,34 @@ function roundHalfToEven(x) {
9447
9477
  if (diff > 0.5) return f + 1;
9448
9478
  return f % 2 === 0 ? f : f + 1;
9449
9479
  }
9450
- function encode(model, text) {
9451
- const { dim, weights } = model;
9480
+ function quantize(vec) {
9481
+ const dim = vec.length;
9452
9482
  const out2 = new Int8Array(dim);
9453
- const ids = tokenize(text, model);
9454
- if (ids.length === 0) return out2;
9455
- const pooled = new Float64Array(dim);
9456
- for (const id of ids) {
9457
- const base = id * dim;
9458
- for (let d = 0; d < dim; d++) pooled[d] += weights[base + d];
9459
- }
9460
- const inv = 1 / ids.length;
9461
- for (let d = 0; d < dim; d++) pooled[d] *= inv;
9462
9483
  let sumsq = 0;
9463
- for (let d = 0; d < dim; d++) sumsq += pooled[d] * pooled[d];
9484
+ for (let d = 0; d < dim; d++) sumsq += vec[d] * vec[d];
9464
9485
  const norm2 = Math.sqrt(sumsq);
9465
9486
  if (norm2 === 0) return out2;
9466
9487
  for (let d = 0; d < dim; d++) {
9467
- let q = roundHalfToEven(pooled[d] / norm2 * QUANT);
9488
+ let q = roundHalfToEven(vec[d] / norm2 * QUANT);
9468
9489
  if (q > QUANT) q = QUANT;
9469
9490
  else if (q < -QUANT) q = -QUANT;
9470
9491
  out2[d] = q;
9471
9492
  }
9472
9493
  return out2;
9473
9494
  }
9495
+ function encode(model, text) {
9496
+ const { dim, weights } = model;
9497
+ const ids = tokenize(text, model);
9498
+ if (ids.length === 0) return new Int8Array(dim);
9499
+ const pooled = new Float64Array(dim);
9500
+ for (const id of ids) {
9501
+ const base = id * dim;
9502
+ for (let d = 0; d < dim; d++) pooled[d] += weights[base + d];
9503
+ }
9504
+ const inv = 1 / ids.length;
9505
+ for (let d = 0; d < dim; d++) pooled[d] *= inv;
9506
+ return quantize(pooled);
9507
+ }
9474
9508
  function intDot(a, b) {
9475
9509
  const n = Math.min(a.length, b.length);
9476
9510
  let dot = 0;
@@ -9493,8 +9527,8 @@ function symbolText(rel, name2, signature, summary) {
9493
9527
  function fileText(rel, title, summary, headings) {
9494
9528
  return [title ?? "", summary ?? "", ...headings, rel.replace(/\//g, " ")].join("\n");
9495
9529
  }
9496
- function buildEmbeddingIndex(scan2, model) {
9497
- const records = [];
9530
+ function embeddingUnits(scan2) {
9531
+ const units = [];
9498
9532
  for (const f of scan2.files) {
9499
9533
  const seen = /* @__PURE__ */ new Set();
9500
9534
  let hadSymbol = false;
@@ -9502,20 +9536,22 @@ function buildEmbeddingIndex(scan2, model) {
9502
9536
  if (seen.has(s.name)) continue;
9503
9537
  seen.add(s.name);
9504
9538
  hadSymbol = true;
9505
- records.push({
9506
- file: f.rel,
9507
- symbol: s.name,
9508
- line: s.line,
9509
- vec: encode(model, symbolText(f.rel, s.name, s.signature, f.summary))
9510
- });
9539
+ units.push({ file: f.rel, symbol: s.name, line: s.line, text: symbolText(f.rel, s.name, s.signature, f.summary) });
9511
9540
  }
9512
9541
  if (!hadSymbol) {
9513
9542
  const text = fileText(f.rel, f.title, f.summary, f.headings);
9514
- if (text.replace(/\s+/g, "")) {
9515
- records.push({ file: f.rel, vec: encode(model, text) });
9516
- }
9543
+ if (text.replace(/\s+/g, "")) units.push({ file: f.rel, text });
9517
9544
  }
9518
9545
  }
9546
+ return units;
9547
+ }
9548
+ function buildEmbeddingIndex(scan2, model) {
9549
+ const records = embeddingUnits(scan2).map((u) => {
9550
+ const rec = { file: u.file, vec: encode(model, u.text) };
9551
+ if (u.symbol !== void 0) rec.symbol = u.symbol;
9552
+ if (u.line !== void 0) rec.line = u.line;
9553
+ return rec;
9554
+ });
9519
9555
  return { embedVersion: EMBED_VERSION, modelId: model.modelId, dim: model.dim, records };
9520
9556
  }
9521
9557
  function serializeEmbeddings(index) {
@@ -9572,10 +9608,10 @@ var init_embed = __esm({
9572
9608
  function searchSemantic(scan2, query, index, opts = {}) {
9573
9609
  const limit = opts.limit ?? DEFAULT_LIMIT2;
9574
9610
  const lexical = searchIndex(scan2, query, { limit: Math.max(limit, 50), fuzzy: opts.fuzzy });
9575
- if (!opts.model || !index || index.records.length === 0) {
9611
+ const q = opts.queryVec ?? (opts.model ? encode(opts.model, query) : void 0);
9612
+ if (!q || !index || index.records.length === 0) {
9576
9613
  return lexical.slice(0, limit);
9577
9614
  }
9578
- const q = encode(opts.model, query);
9579
9615
  const bestByFile = /* @__PURE__ */ new Map();
9580
9616
  for (const r of index.records) {
9581
9617
  const dot = intDot(q, r.vec);
@@ -9614,6 +9650,99 @@ var init_search = __esm({
9614
9650
  }
9615
9651
  });
9616
9652
 
9653
+ // src/embed/endpoint.ts
9654
+ function resolveEmbedEndpoint(opts = {}) {
9655
+ const url = opts.url ?? process.env.CODEINDEX_EMBED_ENDPOINT;
9656
+ return url && url.trim() ? url.trim() : void 0;
9657
+ }
9658
+ function stripTrailingSlash(url) {
9659
+ return url.replace(/\/+$/, "");
9660
+ }
9661
+ function embedEndpointUrl(base) {
9662
+ const b = stripTrailingSlash(base);
9663
+ return b.endsWith("/embed") ? b : b + "/embed";
9664
+ }
9665
+ function healthzUrl(base) {
9666
+ return stripTrailingSlash(base).replace(/\/embed$/, "") + "/healthz";
9667
+ }
9668
+ function resolveTimeout(opts) {
9669
+ if (typeof opts.timeoutMs === "number") return opts.timeoutMs;
9670
+ const env = Number(process.env.CODEINDEX_EMBED_TIMEOUT_MS);
9671
+ return Number.isFinite(env) && env > 0 ? env : 3e4;
9672
+ }
9673
+ async function embedViaEndpoint(texts, opts = {}) {
9674
+ const base = resolveEmbedEndpoint(opts);
9675
+ if (!base) throw new Error("no embedding endpoint configured (set CODEINDEX_EMBED_ENDPOINT or pass opts.url)");
9676
+ const url = embedEndpointUrl(base);
9677
+ const controller = new AbortController();
9678
+ const timer = setTimeout(() => controller.abort(), resolveTimeout(opts));
9679
+ try {
9680
+ const res = await fetch(url, {
9681
+ method: "POST",
9682
+ headers: { "content-type": "application/json", ...opts.headers ?? {} },
9683
+ body: JSON.stringify({ texts }),
9684
+ signal: controller.signal
9685
+ });
9686
+ if (!res.ok) throw new Error(`embedding endpoint ${url} returned HTTP ${res.status}`);
9687
+ const data = await res.json();
9688
+ const vectors = data.vectors;
9689
+ if (!Array.isArray(vectors) || !vectors.every((v) => Array.isArray(v) && v.every((x) => typeof x === "number"))) {
9690
+ throw new Error(`embedding endpoint ${url} returned a malformed { vectors } payload`);
9691
+ }
9692
+ return vectors;
9693
+ } finally {
9694
+ clearTimeout(timer);
9695
+ }
9696
+ }
9697
+ async function probeEndpoint(base, opts = {}) {
9698
+ const controller = new AbortController();
9699
+ const timer = setTimeout(() => controller.abort(), resolveTimeout(opts));
9700
+ try {
9701
+ const res = await fetch(healthzUrl(base), { signal: controller.signal, headers: opts.headers });
9702
+ return res.ok;
9703
+ } catch {
9704
+ return false;
9705
+ } finally {
9706
+ clearTimeout(timer);
9707
+ }
9708
+ }
9709
+ async function encodeQueryViaEndpoint(query, opts = {}) {
9710
+ const [vec] = await embedViaEndpoint([query], opts);
9711
+ if (!vec) throw new Error("embedding endpoint returned no vector for the query");
9712
+ return quantize(vec);
9713
+ }
9714
+ async function buildEndpointIndex(scan2, opts = {}) {
9715
+ const units = embeddingUnits(scan2);
9716
+ const batchSize = opts.batchSize && opts.batchSize > 0 ? opts.batchSize : 64;
9717
+ const records = [];
9718
+ let dim = 0;
9719
+ for (let i2 = 0; i2 < units.length; i2 += batchSize) {
9720
+ const batch = units.slice(i2, i2 + batchSize);
9721
+ const vectors = await embedViaEndpoint(batch.map((u) => u.text), opts);
9722
+ if (vectors.length !== batch.length) {
9723
+ throw new Error(`embedding endpoint returned ${vectors.length} vectors for ${batch.length} texts`);
9724
+ }
9725
+ for (let j = 0; j < batch.length; j++) {
9726
+ const u = batch[j];
9727
+ const vec = quantize(vectors[j]);
9728
+ if (vec.length > dim) dim = vec.length;
9729
+ const rec = { file: u.file, vec };
9730
+ if (u.symbol !== void 0) rec.symbol = u.symbol;
9731
+ if (u.line !== void 0) rec.line = u.line;
9732
+ records.push(rec);
9733
+ }
9734
+ }
9735
+ return { embedVersion: EMBED_VERSION, modelId: "endpoint", dim, records };
9736
+ }
9737
+ var init_endpoint = __esm({
9738
+ "src/embed/endpoint.ts"() {
9739
+ "use strict";
9740
+ init_encode();
9741
+ init_model();
9742
+ init_embed();
9743
+ }
9744
+ });
9745
+
9617
9746
  // src/rules.ts
9618
9747
  function isEntrypointLike(rel) {
9619
9748
  const base = rel.split("/").pop();
@@ -10009,7 +10138,7 @@ function str(v) {
10009
10138
  function strArray(v) {
10010
10139
  return Array.isArray(v) && v.every((x) => typeof x === "string") && v.length ? v : void 0;
10011
10140
  }
10012
- function callTool(name2, args2) {
10141
+ async function callTool(name2, args2) {
10013
10142
  const repo = str(args2.repo);
10014
10143
  if (!repo) throw new Error("`repo` is required (absolute path to the repository root)");
10015
10144
  const scanOpts = { scope: str(args2.scope), include: strArray(args2.include), exclude: strArray(args2.exclude) };
@@ -10146,6 +10275,16 @@ function callTool(name2, args2) {
10146
10275
  const limit = typeof args2.limit === "number" ? args2.limit : void 0;
10147
10276
  const fuzzy = typeof args2.fuzzy === "boolean" ? args2.fuzzy : void 0;
10148
10277
  if (args2.semantic === true) {
10278
+ const endpoint = resolveEmbedEndpoint();
10279
+ if (endpoint) {
10280
+ try {
10281
+ const index = await buildEndpointIndex(scan2);
10282
+ const queryVec = await encodeQueryViaEndpoint(query);
10283
+ return JSON.stringify(searchSemantic(scan2, query, index, { queryVec, limit, fuzzy }), null, 2);
10284
+ } catch {
10285
+ return JSON.stringify(searchIndex(scan2, query, { limit, fuzzy }), null, 2);
10286
+ }
10287
+ }
10149
10288
  const modelDir = resolveEmbedModelDir(repo);
10150
10289
  const model = modelDir ? loadEmbedModel(modelDir) : void 0;
10151
10290
  if (model) {
@@ -10158,15 +10297,16 @@ function callTool(name2, args2) {
10158
10297
  if (name2 === "embed_status") {
10159
10298
  const modelDir = resolveEmbedModelDir(repo);
10160
10299
  const model = modelDir ? loadEmbedModel(modelDir) : void 0;
10161
- return JSON.stringify(
10162
- {
10163
- embedVersion: EMBED_VERSION,
10164
- model: model ? { present: true, dir: modelDir, modelId: model.modelId, dim: model.dim, vocabSize: model.vocabSize } : { present: false },
10165
- endpoint: process.env.CODEINDEX_EMBED_ENDPOINT ?? null
10166
- },
10167
- null,
10168
- 2
10169
- );
10300
+ const endpoint = resolveEmbedEndpoint();
10301
+ const mode = endpoint ? "endpoint" : model ? "static" : "none";
10302
+ const status = {
10303
+ embedVersion: EMBED_VERSION,
10304
+ mode,
10305
+ model: model ? { present: true, dir: modelDir, modelId: model.modelId, dim: model.dim, vocabSize: model.vocabSize } : { present: false },
10306
+ endpoint: endpoint ?? null
10307
+ };
10308
+ if (endpoint) status.endpointReachable = await probeEndpoint(endpoint);
10309
+ return JSON.stringify(status, null, 2);
10170
10310
  }
10171
10311
  if (name2 === "check_rules") {
10172
10312
  const rules = parseRules(args2.rules);
@@ -10192,9 +10332,9 @@ async function runMcpServer() {
10192
10332
  continue;
10193
10333
  }
10194
10334
  const requests = Array.isArray(parsed) ? parsed : [parsed];
10195
- for (const req of requests) handle2(req);
10335
+ for (const req of requests) await handle2(req);
10196
10336
  }
10197
- function handle2(req) {
10337
+ async function handle2(req) {
10198
10338
  if (req.id === void 0 || req.id === null) return;
10199
10339
  try {
10200
10340
  if (req.method === "initialize") {
@@ -10215,7 +10355,7 @@ async function runMcpServer() {
10215
10355
  const name2 = str(params.name) ?? "";
10216
10356
  const args2 = params.arguments ?? {};
10217
10357
  try {
10218
- const text = callTool(name2, args2);
10358
+ const text = await callTool(name2, args2);
10219
10359
  send({ id: req.id, result: { content: [{ type: "text", text }] } });
10220
10360
  } catch (e) {
10221
10361
  send({
@@ -10257,6 +10397,7 @@ var init_mcp = __esm({
10257
10397
  init_model();
10258
10398
  init_embed();
10259
10399
  init_search();
10400
+ init_endpoint();
10260
10401
  repoProp = { repo: { type: "string", description: "Absolute path to the repository root" } };
10261
10402
  scopeProps = {
10262
10403
  scope: { type: "string", description: "Restrict to one directory (repo-relative)" },
@@ -10483,7 +10624,7 @@ var init_mcp = __esm({
10483
10624
  },
10484
10625
  semantic: {
10485
10626
  type: "boolean",
10486
- description: "RRF-fuse the deterministic static-embedding tier with lexical when a model asset is present (default false; silently lexical-only when no model)"
10627
+ description: "RRF-fuse an embedding tier with lexical (default false). Precedence: the HTTP endpoint (CODEINDEX_EMBED_ENDPOINT) if set, else a local static model. Degrades silently to lexical-only when neither is available/reachable \u2014 see embed_status."
10487
10628
  }
10488
10629
  },
10489
10630
  required: ["repo", "query"]
@@ -10491,7 +10632,7 @@ var init_mcp = __esm({
10491
10632
  },
10492
10633
  {
10493
10634
  name: "embed_status",
10494
- description: "Report the deterministic static-embedding tier: whether a model asset is resolved (opt-in, never shipped in the package), its modelId/dim, EMBED_VERSION, and any configured HTTP endpoint. Use to check whether `search` with semantic:true will actually fuse embeddings or degrade to lexical.",
10635
+ 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.",
10495
10636
  inputSchema: { type: "object", properties: { ...repoProp }, required: ["repo"] }
10496
10637
  },
10497
10638
  {
@@ -10912,37 +11053,7 @@ init_model();
10912
11053
  init_encode();
10913
11054
  init_embed();
10914
11055
  init_search();
10915
-
10916
- // src/embed/endpoint.ts
10917
- function resolveEmbedEndpoint(opts = {}) {
10918
- const url = opts.url ?? process.env.CODEINDEX_EMBED_ENDPOINT;
10919
- return url && url.trim() ? url.trim() : void 0;
10920
- }
10921
- async function embedViaEndpoint(texts, opts = {}) {
10922
- const url = resolveEmbedEndpoint(opts);
10923
- if (!url) throw new Error("no embedding endpoint configured (set CODEINDEX_EMBED_ENDPOINT or pass opts.url)");
10924
- const controller = new AbortController();
10925
- const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 3e4);
10926
- try {
10927
- const res = await fetch(url, {
10928
- method: "POST",
10929
- headers: { "content-type": "application/json", ...opts.headers ?? {} },
10930
- body: JSON.stringify({ texts }),
10931
- signal: controller.signal
10932
- });
10933
- if (!res.ok) throw new Error(`embedding endpoint ${url} returned HTTP ${res.status}`);
10934
- const data = await res.json();
10935
- const vectors = data.vectors;
10936
- if (!Array.isArray(vectors) || !vectors.every((v) => Array.isArray(v) && v.every((x) => typeof x === "number"))) {
10937
- throw new Error(`embedding endpoint ${url} returned a malformed { vectors } payload`);
10938
- }
10939
- return vectors;
10940
- } finally {
10941
- clearTimeout(timer);
10942
- }
10943
- }
10944
-
10945
- // src/engine.ts
11056
+ init_endpoint();
10946
11057
  init_rules();
10947
11058
  init_coupling();
10948
11059
  init_repomap();
@@ -10978,6 +11089,8 @@ init_rules();
10978
11089
  init_model();
10979
11090
  init_embed();
10980
11091
  init_search();
11092
+ init_endpoint();
11093
+ init_util();
10981
11094
  var HELP = `codeindex engine v${ENGINE_VERSION} \u2014 deterministic repo indexing
10982
11095
 
10983
11096
  Usage: engine.mjs <command> [flags]
@@ -10996,13 +11109,17 @@ Commands:
10996
11109
  grep Search: cli.mjs grep <pattern> --repo <dir> (JSON hits)
10997
11110
  search Keyless BM25 lexical search over symbol names, path segments,
10998
11111
  markdown headings and summaries: cli.mjs search "<query>" --repo <dir>.
10999
- --semantic fuses in the deterministic static-embedding tier (RRF)
11000
- when a model is present; degrades to lexical (exit 0) when absent
11001
- embed Deterministic static-embedding tier (opt-in by model asset):
11002
- embed status Report the resolved model + EMBED_VERSION (JSON)
11003
- embed build Write embeddings.bin into --out <dir> from the repo
11112
+ --semantic fuses in an embedding tier (RRF) \u2014 the HTTP endpoint
11113
+ (CODEINDEX_EMBED_ENDPOINT) if set, else a local static model;
11114
+ degrades to lexical (exit 0) when neither is available/reachable
11115
+ embed Embedding tiers (opt-in). Precedence: endpoint > static model:
11116
+ embed status Effective mode (none/static/endpoint), model +
11117
+ EMBED_VERSION, and endpoint reachability (JSON)
11118
+ embed build Write embeddings.bin into --out <dir> (static tier)
11004
11119
  embed pull Fetch the model asset into CODEINDEX_EMBED_DIR (or
11005
11120
  <repo>/.codeindex/models/) \u2014 needs CODEINDEX_EMBED_URL
11121
+ embed serve Print (or --run) the docker command that starts the
11122
+ containerized embedding server (rich tier)
11006
11123
  rules Architecture rules (forbidden edges, cycles, orphans) validated
11007
11124
  against the link-graph: --config <codeindex.rules.json>; exits 1
11008
11125
  on any error-severity violation (a CI gate)
@@ -11030,8 +11147,10 @@ Flags:
11030
11147
  --limit <n> Max results for \`search\` (default 20)
11031
11148
  --no-fuzzy \`search\`: disable trigram fuzzy fallback for query terms
11032
11149
  with zero document frequency (default: enabled)
11033
- --semantic \`search\`: RRF-fuse the deterministic static-embedding tier
11034
- with lexical (needs a model asset; lexical-only otherwise)
11150
+ --semantic \`search\`: RRF-fuse an embedding tier with lexical \u2014 the
11151
+ HTTP endpoint if CODEINDEX_EMBED_ENDPOINT is set, else a
11152
+ local static model (lexical-only when neither is available)
11153
+ --run \`embed serve\`: run the docker command instead of printing it
11035
11154
  --recall \`callers\`: recall-oriented binding (issue #7) \u2014 relaxes
11036
11155
  the JS/TS import gate to unique repo-wide names and labels
11037
11156
  each site corroborated|unique-name
@@ -11072,6 +11191,7 @@ function parseFlags(args2) {
11072
11191
  else if (a === "--no-fuzzy") flags2.fuzzy = false;
11073
11192
  else if (a === "--semantic") flags2.semantic = true;
11074
11193
  else if (a === "--recall") flags2.recall = true;
11194
+ else if (a === "--run") flags2.run = true;
11075
11195
  else if (!a.startsWith("--") && flags2.positional === void 0) flags2.positional = a;
11076
11196
  else throw new Error(`unknown flag: ${a}`);
11077
11197
  }
@@ -11182,18 +11302,37 @@ async function runCli(argv) {
11182
11302
  if (!flags2.positional) throw new Error('search needs a query: cli.mjs search "<query>" --repo <dir>');
11183
11303
  const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
11184
11304
  if (flags2.semantic) {
11185
- const modelDir = resolveEmbedModelDir(flags2.repo);
11186
- const model = modelDir ? loadEmbedModel(modelDir) : void 0;
11187
- if (!model) {
11188
- process.stderr.write(
11189
- "codeindex: semantic search unavailable (no embedding model present) \u2014 returning lexical results; run `codeindex embed pull` to enable it\n"
11190
- );
11305
+ const endpoint = resolveEmbedEndpoint();
11306
+ const lexical = () => {
11191
11307
  const results = searchIndex(scan2, flags2.positional, { limit: flags2.limit, fuzzy: flags2.fuzzy });
11192
11308
  emit(JSON.stringify(results, null, 2) + "\n", flags2.out);
11309
+ };
11310
+ if (endpoint) {
11311
+ try {
11312
+ const index = await buildEndpointIndex(scan2);
11313
+ const queryVec = await encodeQueryViaEndpoint(flags2.positional);
11314
+ const results = searchSemantic(scan2, flags2.positional, index, { queryVec, limit: flags2.limit, fuzzy: flags2.fuzzy });
11315
+ emit(JSON.stringify(results, null, 2) + "\n", flags2.out);
11316
+ } catch (e) {
11317
+ process.stderr.write(
11318
+ `codeindex: embedding endpoint ${endpoint} unavailable (${e instanceof Error ? e.message : e}) \u2014 returning lexical results
11319
+ `
11320
+ );
11321
+ lexical();
11322
+ }
11193
11323
  } else {
11194
- const index = buildEmbeddingIndex(scan2, model);
11195
- const results = searchSemantic(scan2, flags2.positional, index, { model, limit: flags2.limit, fuzzy: flags2.fuzzy });
11196
- emit(JSON.stringify(results, null, 2) + "\n", flags2.out);
11324
+ const modelDir = resolveEmbedModelDir(flags2.repo);
11325
+ const model = modelDir ? loadEmbedModel(modelDir) : void 0;
11326
+ if (!model) {
11327
+ process.stderr.write(
11328
+ "codeindex: semantic search unavailable (no embedding model or endpoint) \u2014 returning lexical results; run `codeindex embed pull` or set CODEINDEX_EMBED_ENDPOINT to enable it\n"
11329
+ );
11330
+ lexical();
11331
+ } else {
11332
+ const index = buildEmbeddingIndex(scan2, model);
11333
+ const results = searchSemantic(scan2, flags2.positional, index, { model, limit: flags2.limit, fuzzy: flags2.fuzzy });
11334
+ emit(JSON.stringify(results, null, 2) + "\n", flags2.out);
11335
+ }
11197
11336
  }
11198
11337
  } else {
11199
11338
  const results = searchIndex(scan2, flags2.positional, { limit: flags2.limit, fuzzy: flags2.fuzzy });
@@ -11204,12 +11343,45 @@ async function runCli(argv) {
11204
11343
  const modelDir = resolveEmbedModelDir(flags2.repo);
11205
11344
  if (sub === "status") {
11206
11345
  const model = modelDir ? loadEmbedModel(modelDir) : void 0;
11346
+ const endpoint = resolveEmbedEndpoint();
11347
+ const mode = endpoint ? "endpoint" : model ? "static" : "none";
11207
11348
  const status = {
11208
11349
  embedVersion: EMBED_VERSION,
11350
+ mode,
11209
11351
  model: model ? { present: true, dir: modelDir, modelId: model.modelId, dim: model.dim, vocabSize: model.vocabSize } : { present: false },
11210
- endpoint: process.env.CODEINDEX_EMBED_ENDPOINT ?? null
11352
+ endpoint: endpoint ?? null
11211
11353
  };
11354
+ if (endpoint) status.endpointReachable = await probeEndpoint(endpoint);
11212
11355
  emit(JSON.stringify(status, null, 2) + "\n", flags2.out);
11356
+ } else if (sub === "serve") {
11357
+ const dockerArgs = ["run", "-d", "-p", "8756:8756", "ghcr.io/maxgfr/codeindex-embed:latest"];
11358
+ const oneLiner = `docker ${dockerArgs.join(" ")}`;
11359
+ if (!have("docker")) {
11360
+ process.stderr.write(
11361
+ "codeindex: docker not found on PATH. Install Docker, then run:\n " + oneLiner + "\n"
11362
+ );
11363
+ process.exitCode = 1;
11364
+ return;
11365
+ }
11366
+ if (flags2.run) {
11367
+ process.stderr.write(`codeindex: starting embedding server \u2192 ${oneLiner}
11368
+ `);
11369
+ const res = sh("docker", dockerArgs);
11370
+ if (res.stdout.trim()) process.stdout.write(res.stdout.trim() + "\n");
11371
+ if (!res.ok) {
11372
+ process.stderr.write(res.stderr || "codeindex: docker run failed\n");
11373
+ process.exitCode = 1;
11374
+ return;
11375
+ }
11376
+ process.stderr.write(
11377
+ 'codeindex: server starting on http://localhost:8756 \u2014 then:\n CODEINDEX_EMBED_ENDPOINT=http://localhost:8756 codeindex search "<query>" --repo . --semantic\n'
11378
+ );
11379
+ } else {
11380
+ process.stdout.write(oneLiner + "\n");
11381
+ process.stderr.write(
11382
+ 'codeindex: run the line above to start the embedding server (or `embed serve --run`), then:\n CODEINDEX_EMBED_ENDPOINT=http://localhost:8756 codeindex search "<query>" --repo . --semantic\n'
11383
+ );
11384
+ }
11213
11385
  } else if (sub === "build") {
11214
11386
  if (!flags2.out) throw new Error("embed build needs --out <dir>");
11215
11387
  if (!modelDir) {
@@ -11256,7 +11428,7 @@ async function runCli(argv) {
11256
11428
  process.stderr.write(`codeindex: model written to ${join12(destDir, "model.json")}
11257
11429
  `);
11258
11430
  } else {
11259
- throw new Error("embed needs a subcommand: pull | build | status");
11431
+ throw new Error("embed needs a subcommand: status | build | pull | serve");
11260
11432
  }
11261
11433
  } else if (cmd === "rules") {
11262
11434
  if (!flags2.config) throw new Error("rules needs --config <codeindex.rules.json>");
@@ -11332,6 +11504,7 @@ export {
11332
11504
  betweennessOf,
11333
11505
  buildCallerIndex,
11334
11506
  buildEmbeddingIndex,
11507
+ buildEndpointIndex,
11335
11508
  buildGraph,
11336
11509
  buildIndexArtifacts,
11337
11510
  buildModules,
@@ -11359,9 +11532,12 @@ export {
11359
11532
  detectWorkspaces,
11360
11533
  diffFiles,
11361
11534
  diffHunks,
11535
+ embedEndpointUrl,
11362
11536
  embedViaEndpoint,
11537
+ embeddingUnits,
11363
11538
  enclosingSymbol,
11364
11539
  encode,
11540
+ encodeQueryViaEndpoint,
11365
11541
  ensureGrammars,
11366
11542
  escapeRegExp,
11367
11543
  extToLang,
@@ -11380,6 +11556,7 @@ export {
11380
11556
  hasEmbedModel,
11381
11557
  have,
11382
11558
  headCommit,
11559
+ healthzUrl,
11383
11560
  insertAfterSymbol,
11384
11561
  insertBeforeSymbol,
11385
11562
  intDot,
@@ -11397,6 +11574,8 @@ export {
11397
11574
  pagerankOf,
11398
11575
  parseGitignore,
11399
11576
  parseRules,
11577
+ probeEndpoint,
11578
+ quantize,
11400
11579
  rankHotspots,
11401
11580
  rankedKeywords,
11402
11581
  readMemory,