@maxgfr/codeindex 2.10.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,9 +14,9 @@ var ENGINE_VERSION, SCHEMA_VERSION, EXTRACTOR_VERSION;
14
14
  var init_types = __esm({
15
15
  "src/types.ts"() {
16
16
  "use strict";
17
- ENGINE_VERSION = "2.10.0";
17
+ ENGINE_VERSION = "2.11.0";
18
18
  SCHEMA_VERSION = 4;
19
- EXTRACTOR_VERSION = 6;
19
+ EXTRACTOR_VERSION = 7;
20
20
  }
21
21
  });
22
22
 
@@ -6340,12 +6340,14 @@ function extractImports(ext, content) {
6340
6340
  }
6341
6341
  return [...specs].map((spec) => ({ kind: "import", spec }));
6342
6342
  }
6343
- function extractReexports(rel, content) {
6343
+ function extractReexports(rel, content, localSymbols) {
6344
6344
  if (!JS_TS.has(rel.slice(rel.lastIndexOf(".")))) return [];
6345
6345
  const lang = /\.(ts|tsx|mts|cts)$/.test(rel) ? "typescript" : "javascript";
6346
6346
  const out2 = [];
6347
6347
  const seen = /* @__PURE__ */ new Set();
6348
6348
  const lineAt = (idx) => content.slice(0, idx).split(/\r?\n/).length;
6349
+ const localKindOf = /* @__PURE__ */ new Map();
6350
+ for (const s of localSymbols) if (!localKindOf.has(s.name)) localKindOf.set(s.name, s.kind);
6349
6351
  const named = /export\s*\{([\s\S]*?)\}\s*(?:from\s*['"]([^'"]+)['"])?\s*;?/g;
6350
6352
  let m;
6351
6353
  while ((m = named.exec(content)) && out2.length < 60) {
@@ -6353,12 +6355,14 @@ function extractReexports(rel, content) {
6353
6355
  for (const part of m[1].split(",")) {
6354
6356
  const p = part.trim().replace(/^type\s+/, "");
6355
6357
  const as = /^(\S+)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(p);
6358
+ const orig = as ? as[1] : p;
6356
6359
  const name2 = as ? as[2] : p;
6357
6360
  if (!/^[A-Za-z_$][\w$]*$/.test(name2) || name2 === "default" || seen.has(name2)) continue;
6358
6361
  seen.add(name2);
6362
+ const mirroredKind = !from ? localKindOf.get(orig) : void 0;
6359
6363
  out2.push({
6360
6364
  name: name2,
6361
- kind: "reexport",
6365
+ kind: mirroredKind ?? "reexport",
6362
6366
  file: rel,
6363
6367
  line: lineAt(m.index),
6364
6368
  signature: from ? `export { ${name2} } from "${from}"` : `export { ${name2} }`,
@@ -6411,7 +6415,7 @@ function extractCode(rel, ext, content) {
6411
6415
  const ast = extractAst(rel, ext, content);
6412
6416
  const symbols = (ast ? ast.symbols : extractSymbols(rel, ext, content)).slice(0, 400);
6413
6417
  const known = new Set(symbols.map((s) => s.name));
6414
- const reexports = extractReexports(rel, content).filter((s) => !known.has(s.name));
6418
+ const reexports = extractReexports(rel, content, symbols).filter((s) => !known.has(s.name));
6415
6419
  return {
6416
6420
  symbols: [...symbols, ...reexports],
6417
6421
  summary: topDocComment(content),
@@ -9447,30 +9451,34 @@ function roundHalfToEven(x) {
9447
9451
  if (diff > 0.5) return f + 1;
9448
9452
  return f % 2 === 0 ? f : f + 1;
9449
9453
  }
9450
- function encode(model, text) {
9451
- const { dim, weights } = model;
9454
+ function quantize(vec) {
9455
+ const dim = vec.length;
9452
9456
  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
9457
  let sumsq = 0;
9463
- for (let d = 0; d < dim; d++) sumsq += pooled[d] * pooled[d];
9458
+ for (let d = 0; d < dim; d++) sumsq += vec[d] * vec[d];
9464
9459
  const norm2 = Math.sqrt(sumsq);
9465
9460
  if (norm2 === 0) return out2;
9466
9461
  for (let d = 0; d < dim; d++) {
9467
- let q = roundHalfToEven(pooled[d] / norm2 * QUANT);
9462
+ let q = roundHalfToEven(vec[d] / norm2 * QUANT);
9468
9463
  if (q > QUANT) q = QUANT;
9469
9464
  else if (q < -QUANT) q = -QUANT;
9470
9465
  out2[d] = q;
9471
9466
  }
9472
9467
  return out2;
9473
9468
  }
9469
+ function encode(model, text) {
9470
+ const { dim, weights } = model;
9471
+ const ids = tokenize(text, model);
9472
+ if (ids.length === 0) return new Int8Array(dim);
9473
+ const pooled = new Float64Array(dim);
9474
+ for (const id of ids) {
9475
+ const base = id * dim;
9476
+ for (let d = 0; d < dim; d++) pooled[d] += weights[base + d];
9477
+ }
9478
+ const inv = 1 / ids.length;
9479
+ for (let d = 0; d < dim; d++) pooled[d] *= inv;
9480
+ return quantize(pooled);
9481
+ }
9474
9482
  function intDot(a, b) {
9475
9483
  const n = Math.min(a.length, b.length);
9476
9484
  let dot = 0;
@@ -9493,8 +9501,8 @@ function symbolText(rel, name2, signature, summary) {
9493
9501
  function fileText(rel, title, summary, headings) {
9494
9502
  return [title ?? "", summary ?? "", ...headings, rel.replace(/\//g, " ")].join("\n");
9495
9503
  }
9496
- function buildEmbeddingIndex(scan2, model) {
9497
- const records = [];
9504
+ function embeddingUnits(scan2) {
9505
+ const units = [];
9498
9506
  for (const f of scan2.files) {
9499
9507
  const seen = /* @__PURE__ */ new Set();
9500
9508
  let hadSymbol = false;
@@ -9502,20 +9510,22 @@ function buildEmbeddingIndex(scan2, model) {
9502
9510
  if (seen.has(s.name)) continue;
9503
9511
  seen.add(s.name);
9504
9512
  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
- });
9513
+ units.push({ file: f.rel, symbol: s.name, line: s.line, text: symbolText(f.rel, s.name, s.signature, f.summary) });
9511
9514
  }
9512
9515
  if (!hadSymbol) {
9513
9516
  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
- }
9517
+ if (text.replace(/\s+/g, "")) units.push({ file: f.rel, text });
9517
9518
  }
9518
9519
  }
9520
+ return units;
9521
+ }
9522
+ function buildEmbeddingIndex(scan2, model) {
9523
+ const records = embeddingUnits(scan2).map((u) => {
9524
+ const rec = { file: u.file, vec: encode(model, u.text) };
9525
+ if (u.symbol !== void 0) rec.symbol = u.symbol;
9526
+ if (u.line !== void 0) rec.line = u.line;
9527
+ return rec;
9528
+ });
9519
9529
  return { embedVersion: EMBED_VERSION, modelId: model.modelId, dim: model.dim, records };
9520
9530
  }
9521
9531
  function serializeEmbeddings(index) {
@@ -9572,10 +9582,10 @@ var init_embed = __esm({
9572
9582
  function searchSemantic(scan2, query, index, opts = {}) {
9573
9583
  const limit = opts.limit ?? DEFAULT_LIMIT2;
9574
9584
  const lexical = searchIndex(scan2, query, { limit: Math.max(limit, 50), fuzzy: opts.fuzzy });
9575
- if (!opts.model || !index || index.records.length === 0) {
9585
+ const q = opts.queryVec ?? (opts.model ? encode(opts.model, query) : void 0);
9586
+ if (!q || !index || index.records.length === 0) {
9576
9587
  return lexical.slice(0, limit);
9577
9588
  }
9578
- const q = encode(opts.model, query);
9579
9589
  const bestByFile = /* @__PURE__ */ new Map();
9580
9590
  for (const r of index.records) {
9581
9591
  const dot = intDot(q, r.vec);
@@ -9614,6 +9624,99 @@ var init_search = __esm({
9614
9624
  }
9615
9625
  });
9616
9626
 
9627
+ // src/embed/endpoint.ts
9628
+ function resolveEmbedEndpoint(opts = {}) {
9629
+ const url = opts.url ?? process.env.CODEINDEX_EMBED_ENDPOINT;
9630
+ return url && url.trim() ? url.trim() : void 0;
9631
+ }
9632
+ function stripTrailingSlash(url) {
9633
+ return url.replace(/\/+$/, "");
9634
+ }
9635
+ function embedEndpointUrl(base) {
9636
+ const b = stripTrailingSlash(base);
9637
+ return b.endsWith("/embed") ? b : b + "/embed";
9638
+ }
9639
+ function healthzUrl(base) {
9640
+ return stripTrailingSlash(base).replace(/\/embed$/, "") + "/healthz";
9641
+ }
9642
+ function resolveTimeout(opts) {
9643
+ if (typeof opts.timeoutMs === "number") return opts.timeoutMs;
9644
+ const env = Number(process.env.CODEINDEX_EMBED_TIMEOUT_MS);
9645
+ return Number.isFinite(env) && env > 0 ? env : 3e4;
9646
+ }
9647
+ async function embedViaEndpoint(texts, opts = {}) {
9648
+ const base = resolveEmbedEndpoint(opts);
9649
+ if (!base) throw new Error("no embedding endpoint configured (set CODEINDEX_EMBED_ENDPOINT or pass opts.url)");
9650
+ const url = embedEndpointUrl(base);
9651
+ const controller = new AbortController();
9652
+ const timer = setTimeout(() => controller.abort(), resolveTimeout(opts));
9653
+ try {
9654
+ const res = await fetch(url, {
9655
+ method: "POST",
9656
+ headers: { "content-type": "application/json", ...opts.headers ?? {} },
9657
+ body: JSON.stringify({ texts }),
9658
+ signal: controller.signal
9659
+ });
9660
+ if (!res.ok) throw new Error(`embedding endpoint ${url} returned HTTP ${res.status}`);
9661
+ const data = await res.json();
9662
+ const vectors = data.vectors;
9663
+ if (!Array.isArray(vectors) || !vectors.every((v) => Array.isArray(v) && v.every((x) => typeof x === "number"))) {
9664
+ throw new Error(`embedding endpoint ${url} returned a malformed { vectors } payload`);
9665
+ }
9666
+ return vectors;
9667
+ } finally {
9668
+ clearTimeout(timer);
9669
+ }
9670
+ }
9671
+ async function probeEndpoint(base, opts = {}) {
9672
+ const controller = new AbortController();
9673
+ const timer = setTimeout(() => controller.abort(), resolveTimeout(opts));
9674
+ try {
9675
+ const res = await fetch(healthzUrl(base), { signal: controller.signal, headers: opts.headers });
9676
+ return res.ok;
9677
+ } catch {
9678
+ return false;
9679
+ } finally {
9680
+ clearTimeout(timer);
9681
+ }
9682
+ }
9683
+ async function encodeQueryViaEndpoint(query, opts = {}) {
9684
+ const [vec] = await embedViaEndpoint([query], opts);
9685
+ if (!vec) throw new Error("embedding endpoint returned no vector for the query");
9686
+ return quantize(vec);
9687
+ }
9688
+ async function buildEndpointIndex(scan2, opts = {}) {
9689
+ const units = embeddingUnits(scan2);
9690
+ const batchSize = opts.batchSize && opts.batchSize > 0 ? opts.batchSize : 64;
9691
+ const records = [];
9692
+ let dim = 0;
9693
+ for (let i2 = 0; i2 < units.length; i2 += batchSize) {
9694
+ const batch = units.slice(i2, i2 + batchSize);
9695
+ const vectors = await embedViaEndpoint(batch.map((u) => u.text), opts);
9696
+ if (vectors.length !== batch.length) {
9697
+ throw new Error(`embedding endpoint returned ${vectors.length} vectors for ${batch.length} texts`);
9698
+ }
9699
+ for (let j = 0; j < batch.length; j++) {
9700
+ const u = batch[j];
9701
+ const vec = quantize(vectors[j]);
9702
+ if (vec.length > dim) dim = vec.length;
9703
+ const rec = { file: u.file, vec };
9704
+ if (u.symbol !== void 0) rec.symbol = u.symbol;
9705
+ if (u.line !== void 0) rec.line = u.line;
9706
+ records.push(rec);
9707
+ }
9708
+ }
9709
+ return { embedVersion: EMBED_VERSION, modelId: "endpoint", dim, records };
9710
+ }
9711
+ var init_endpoint = __esm({
9712
+ "src/embed/endpoint.ts"() {
9713
+ "use strict";
9714
+ init_encode();
9715
+ init_model();
9716
+ init_embed();
9717
+ }
9718
+ });
9719
+
9617
9720
  // src/rules.ts
9618
9721
  function isEntrypointLike(rel) {
9619
9722
  const base = rel.split("/").pop();
@@ -10009,7 +10112,7 @@ function str(v) {
10009
10112
  function strArray(v) {
10010
10113
  return Array.isArray(v) && v.every((x) => typeof x === "string") && v.length ? v : void 0;
10011
10114
  }
10012
- function callTool(name2, args2) {
10115
+ async function callTool(name2, args2) {
10013
10116
  const repo = str(args2.repo);
10014
10117
  if (!repo) throw new Error("`repo` is required (absolute path to the repository root)");
10015
10118
  const scanOpts = { scope: str(args2.scope), include: strArray(args2.include), exclude: strArray(args2.exclude) };
@@ -10146,6 +10249,16 @@ function callTool(name2, args2) {
10146
10249
  const limit = typeof args2.limit === "number" ? args2.limit : void 0;
10147
10250
  const fuzzy = typeof args2.fuzzy === "boolean" ? args2.fuzzy : void 0;
10148
10251
  if (args2.semantic === true) {
10252
+ const endpoint = resolveEmbedEndpoint();
10253
+ if (endpoint) {
10254
+ try {
10255
+ const index = await buildEndpointIndex(scan2);
10256
+ const queryVec = await encodeQueryViaEndpoint(query);
10257
+ return JSON.stringify(searchSemantic(scan2, query, index, { queryVec, limit, fuzzy }), null, 2);
10258
+ } catch {
10259
+ return JSON.stringify(searchIndex(scan2, query, { limit, fuzzy }), null, 2);
10260
+ }
10261
+ }
10149
10262
  const modelDir = resolveEmbedModelDir(repo);
10150
10263
  const model = modelDir ? loadEmbedModel(modelDir) : void 0;
10151
10264
  if (model) {
@@ -10158,15 +10271,16 @@ function callTool(name2, args2) {
10158
10271
  if (name2 === "embed_status") {
10159
10272
  const modelDir = resolveEmbedModelDir(repo);
10160
10273
  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
- );
10274
+ const endpoint = resolveEmbedEndpoint();
10275
+ const mode = endpoint ? "endpoint" : model ? "static" : "none";
10276
+ const status = {
10277
+ embedVersion: EMBED_VERSION,
10278
+ mode,
10279
+ model: model ? { present: true, dir: modelDir, modelId: model.modelId, dim: model.dim, vocabSize: model.vocabSize } : { present: false },
10280
+ endpoint: endpoint ?? null
10281
+ };
10282
+ if (endpoint) status.endpointReachable = await probeEndpoint(endpoint);
10283
+ return JSON.stringify(status, null, 2);
10170
10284
  }
10171
10285
  if (name2 === "check_rules") {
10172
10286
  const rules = parseRules(args2.rules);
@@ -10192,9 +10306,9 @@ async function runMcpServer() {
10192
10306
  continue;
10193
10307
  }
10194
10308
  const requests = Array.isArray(parsed) ? parsed : [parsed];
10195
- for (const req of requests) handle2(req);
10309
+ for (const req of requests) await handle2(req);
10196
10310
  }
10197
- function handle2(req) {
10311
+ async function handle2(req) {
10198
10312
  if (req.id === void 0 || req.id === null) return;
10199
10313
  try {
10200
10314
  if (req.method === "initialize") {
@@ -10215,7 +10329,7 @@ async function runMcpServer() {
10215
10329
  const name2 = str(params.name) ?? "";
10216
10330
  const args2 = params.arguments ?? {};
10217
10331
  try {
10218
- const text = callTool(name2, args2);
10332
+ const text = await callTool(name2, args2);
10219
10333
  send({ id: req.id, result: { content: [{ type: "text", text }] } });
10220
10334
  } catch (e) {
10221
10335
  send({
@@ -10257,6 +10371,7 @@ var init_mcp = __esm({
10257
10371
  init_model();
10258
10372
  init_embed();
10259
10373
  init_search();
10374
+ init_endpoint();
10260
10375
  repoProp = { repo: { type: "string", description: "Absolute path to the repository root" } };
10261
10376
  scopeProps = {
10262
10377
  scope: { type: "string", description: "Restrict to one directory (repo-relative)" },
@@ -10483,7 +10598,7 @@ var init_mcp = __esm({
10483
10598
  },
10484
10599
  semantic: {
10485
10600
  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)"
10601
+ 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
10602
  }
10488
10603
  },
10489
10604
  required: ["repo", "query"]
@@ -10491,7 +10606,7 @@ var init_mcp = __esm({
10491
10606
  },
10492
10607
  {
10493
10608
  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.",
10609
+ 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
10610
  inputSchema: { type: "object", properties: { ...repoProp }, required: ["repo"] }
10496
10611
  },
10497
10612
  {
@@ -10912,37 +11027,7 @@ init_model();
10912
11027
  init_encode();
10913
11028
  init_embed();
10914
11029
  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
11030
+ init_endpoint();
10946
11031
  init_rules();
10947
11032
  init_coupling();
10948
11033
  init_repomap();
@@ -10978,6 +11063,8 @@ init_rules();
10978
11063
  init_model();
10979
11064
  init_embed();
10980
11065
  init_search();
11066
+ init_endpoint();
11067
+ init_util();
10981
11068
  var HELP = `codeindex engine v${ENGINE_VERSION} \u2014 deterministic repo indexing
10982
11069
 
10983
11070
  Usage: engine.mjs <command> [flags]
@@ -10996,13 +11083,17 @@ Commands:
10996
11083
  grep Search: cli.mjs grep <pattern> --repo <dir> (JSON hits)
10997
11084
  search Keyless BM25 lexical search over symbol names, path segments,
10998
11085
  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
11086
+ --semantic fuses in an embedding tier (RRF) \u2014 the HTTP endpoint
11087
+ (CODEINDEX_EMBED_ENDPOINT) if set, else a local static model;
11088
+ degrades to lexical (exit 0) when neither is available/reachable
11089
+ embed Embedding tiers (opt-in). Precedence: endpoint > static model:
11090
+ embed status Effective mode (none/static/endpoint), model +
11091
+ EMBED_VERSION, and endpoint reachability (JSON)
11092
+ embed build Write embeddings.bin into --out <dir> (static tier)
11004
11093
  embed pull Fetch the model asset into CODEINDEX_EMBED_DIR (or
11005
11094
  <repo>/.codeindex/models/) \u2014 needs CODEINDEX_EMBED_URL
11095
+ embed serve Print (or --run) the docker command that starts the
11096
+ containerized embedding server (rich tier)
11006
11097
  rules Architecture rules (forbidden edges, cycles, orphans) validated
11007
11098
  against the link-graph: --config <codeindex.rules.json>; exits 1
11008
11099
  on any error-severity violation (a CI gate)
@@ -11030,8 +11121,10 @@ Flags:
11030
11121
  --limit <n> Max results for \`search\` (default 20)
11031
11122
  --no-fuzzy \`search\`: disable trigram fuzzy fallback for query terms
11032
11123
  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)
11124
+ --semantic \`search\`: RRF-fuse an embedding tier with lexical \u2014 the
11125
+ HTTP endpoint if CODEINDEX_EMBED_ENDPOINT is set, else a
11126
+ local static model (lexical-only when neither is available)
11127
+ --run \`embed serve\`: run the docker command instead of printing it
11035
11128
  --recall \`callers\`: recall-oriented binding (issue #7) \u2014 relaxes
11036
11129
  the JS/TS import gate to unique repo-wide names and labels
11037
11130
  each site corroborated|unique-name
@@ -11072,6 +11165,7 @@ function parseFlags(args2) {
11072
11165
  else if (a === "--no-fuzzy") flags2.fuzzy = false;
11073
11166
  else if (a === "--semantic") flags2.semantic = true;
11074
11167
  else if (a === "--recall") flags2.recall = true;
11168
+ else if (a === "--run") flags2.run = true;
11075
11169
  else if (!a.startsWith("--") && flags2.positional === void 0) flags2.positional = a;
11076
11170
  else throw new Error(`unknown flag: ${a}`);
11077
11171
  }
@@ -11182,18 +11276,37 @@ async function runCli(argv) {
11182
11276
  if (!flags2.positional) throw new Error('search needs a query: cli.mjs search "<query>" --repo <dir>');
11183
11277
  const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
11184
11278
  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
- );
11279
+ const endpoint = resolveEmbedEndpoint();
11280
+ const lexical = () => {
11191
11281
  const results = searchIndex(scan2, flags2.positional, { limit: flags2.limit, fuzzy: flags2.fuzzy });
11192
11282
  emit(JSON.stringify(results, null, 2) + "\n", flags2.out);
11283
+ };
11284
+ if (endpoint) {
11285
+ try {
11286
+ const index = await buildEndpointIndex(scan2);
11287
+ const queryVec = await encodeQueryViaEndpoint(flags2.positional);
11288
+ const results = searchSemantic(scan2, flags2.positional, index, { queryVec, limit: flags2.limit, fuzzy: flags2.fuzzy });
11289
+ emit(JSON.stringify(results, null, 2) + "\n", flags2.out);
11290
+ } catch (e) {
11291
+ process.stderr.write(
11292
+ `codeindex: embedding endpoint ${endpoint} unavailable (${e instanceof Error ? e.message : e}) \u2014 returning lexical results
11293
+ `
11294
+ );
11295
+ lexical();
11296
+ }
11193
11297
  } 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);
11298
+ const modelDir = resolveEmbedModelDir(flags2.repo);
11299
+ const model = modelDir ? loadEmbedModel(modelDir) : void 0;
11300
+ if (!model) {
11301
+ process.stderr.write(
11302
+ "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"
11303
+ );
11304
+ lexical();
11305
+ } else {
11306
+ const index = buildEmbeddingIndex(scan2, model);
11307
+ const results = searchSemantic(scan2, flags2.positional, index, { model, limit: flags2.limit, fuzzy: flags2.fuzzy });
11308
+ emit(JSON.stringify(results, null, 2) + "\n", flags2.out);
11309
+ }
11197
11310
  }
11198
11311
  } else {
11199
11312
  const results = searchIndex(scan2, flags2.positional, { limit: flags2.limit, fuzzy: flags2.fuzzy });
@@ -11204,12 +11317,45 @@ async function runCli(argv) {
11204
11317
  const modelDir = resolveEmbedModelDir(flags2.repo);
11205
11318
  if (sub === "status") {
11206
11319
  const model = modelDir ? loadEmbedModel(modelDir) : void 0;
11320
+ const endpoint = resolveEmbedEndpoint();
11321
+ const mode = endpoint ? "endpoint" : model ? "static" : "none";
11207
11322
  const status = {
11208
11323
  embedVersion: EMBED_VERSION,
11324
+ mode,
11209
11325
  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
11326
+ endpoint: endpoint ?? null
11211
11327
  };
11328
+ if (endpoint) status.endpointReachable = await probeEndpoint(endpoint);
11212
11329
  emit(JSON.stringify(status, null, 2) + "\n", flags2.out);
11330
+ } else if (sub === "serve") {
11331
+ const dockerArgs = ["run", "-d", "-p", "8756:8756", "ghcr.io/maxgfr/codeindex-embed:latest"];
11332
+ const oneLiner = `docker ${dockerArgs.join(" ")}`;
11333
+ if (!have("docker")) {
11334
+ process.stderr.write(
11335
+ "codeindex: docker not found on PATH. Install Docker, then run:\n " + oneLiner + "\n"
11336
+ );
11337
+ process.exitCode = 1;
11338
+ return;
11339
+ }
11340
+ if (flags2.run) {
11341
+ process.stderr.write(`codeindex: starting embedding server \u2192 ${oneLiner}
11342
+ `);
11343
+ const res = sh("docker", dockerArgs);
11344
+ if (res.stdout.trim()) process.stdout.write(res.stdout.trim() + "\n");
11345
+ if (!res.ok) {
11346
+ process.stderr.write(res.stderr || "codeindex: docker run failed\n");
11347
+ process.exitCode = 1;
11348
+ return;
11349
+ }
11350
+ process.stderr.write(
11351
+ 'codeindex: server starting on http://localhost:8756 \u2014 then:\n CODEINDEX_EMBED_ENDPOINT=http://localhost:8756 codeindex search "<query>" --repo . --semantic\n'
11352
+ );
11353
+ } else {
11354
+ process.stdout.write(oneLiner + "\n");
11355
+ process.stderr.write(
11356
+ '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'
11357
+ );
11358
+ }
11213
11359
  } else if (sub === "build") {
11214
11360
  if (!flags2.out) throw new Error("embed build needs --out <dir>");
11215
11361
  if (!modelDir) {
@@ -11256,7 +11402,7 @@ async function runCli(argv) {
11256
11402
  process.stderr.write(`codeindex: model written to ${join12(destDir, "model.json")}
11257
11403
  `);
11258
11404
  } else {
11259
- throw new Error("embed needs a subcommand: pull | build | status");
11405
+ throw new Error("embed needs a subcommand: status | build | pull | serve");
11260
11406
  }
11261
11407
  } else if (cmd === "rules") {
11262
11408
  if (!flags2.config) throw new Error("rules needs --config <codeindex.rules.json>");
@@ -11332,6 +11478,7 @@ export {
11332
11478
  betweennessOf,
11333
11479
  buildCallerIndex,
11334
11480
  buildEmbeddingIndex,
11481
+ buildEndpointIndex,
11335
11482
  buildGraph,
11336
11483
  buildIndexArtifacts,
11337
11484
  buildModules,
@@ -11359,9 +11506,12 @@ export {
11359
11506
  detectWorkspaces,
11360
11507
  diffFiles,
11361
11508
  diffHunks,
11509
+ embedEndpointUrl,
11362
11510
  embedViaEndpoint,
11511
+ embeddingUnits,
11363
11512
  enclosingSymbol,
11364
11513
  encode,
11514
+ encodeQueryViaEndpoint,
11365
11515
  ensureGrammars,
11366
11516
  escapeRegExp,
11367
11517
  extToLang,
@@ -11380,6 +11530,7 @@ export {
11380
11530
  hasEmbedModel,
11381
11531
  have,
11382
11532
  headCommit,
11533
+ healthzUrl,
11383
11534
  insertAfterSymbol,
11384
11535
  insertBeforeSymbol,
11385
11536
  intDot,
@@ -11397,6 +11548,8 @@ export {
11397
11548
  pagerankOf,
11398
11549
  parseGitignore,
11399
11550
  parseRules,
11551
+ probeEndpoint,
11552
+ quantize,
11400
11553
  rankHotspots,
11401
11554
  rankedKeywords,
11402
11555
  readMemory,
@@ -87,14 +87,36 @@ export function roundHalfToEven(x: number): number {
87
87
  return f % 2 === 0 ? f : f + 1; // exactly .5 → nearest even
88
88
  }
89
89
 
90
+ // L2-normalize a float vector and quantize it to int8 at the fixed 1/127 scale
91
+ // (round-half-to-even, clamp to [-127,127]) — the SHARED tail of both tiers. The
92
+ // static encoder feeds its mean-pooled vector here; the endpoint tier feeds each
93
+ // float embedding it receives. Because the quantization is byte-identical, a
94
+ // given float vector maps to the same int8 bytes and the integer-dot ranking is
95
+ // consistent across tiers. A zero-norm input yields an all-zero vector (never
96
+ // NaN — it simply ranks last).
97
+ export function quantize(vec: ArrayLike<number>): Int8Array {
98
+ const dim = vec.length;
99
+ const out = new Int8Array(dim);
100
+ let sumsq = 0;
101
+ for (let d = 0; d < dim; d++) sumsq += vec[d]! * vec[d]!;
102
+ const norm = Math.sqrt(sumsq);
103
+ if (norm === 0) return out; // zero vector — nothing to rank on
104
+ for (let d = 0; d < dim; d++) {
105
+ let q = roundHalfToEven((vec[d]! / norm) * QUANT);
106
+ if (q > QUANT) q = QUANT;
107
+ else if (q < -QUANT) q = -QUANT;
108
+ out[d] = q;
109
+ }
110
+ return out;
111
+ }
112
+
90
113
  // Encode a text to an int8 unit-ish vector. Empty / all-OOV / zero-norm inputs
91
114
  // yield an all-zero vector (dot-products against it are 0 — it simply ranks
92
115
  // last), never NaN. Pure and deterministic: same model + text → same bytes.
93
116
  export function encode(model: StaticEmbedModel, text: string): Int8Array {
94
117
  const { dim, weights } = model;
95
- const out = new Int8Array(dim);
96
118
  const ids = tokenize(text, model);
97
- if (ids.length === 0) return out;
119
+ if (ids.length === 0) return new Int8Array(dim);
98
120
 
99
121
  // mean-pool in double precision, fixed order.
100
122
  const pooled = new Float64Array(dim);
@@ -105,20 +127,8 @@ export function encode(model: StaticEmbedModel, text: string): Int8Array {
105
127
  const inv = 1 / ids.length;
106
128
  for (let d = 0; d < dim; d++) pooled[d]! *= inv;
107
129
 
108
- // L2-normalize.
109
- let sumsq = 0;
110
- for (let d = 0; d < dim; d++) sumsq += pooled[d]! * pooled[d]!;
111
- const norm = Math.sqrt(sumsq);
112
- if (norm === 0) return out; // zero vector — nothing to rank on
113
-
114
- // quantize to int8 at the fixed 1/127 scale, round-half-to-even, clamp.
115
- for (let d = 0; d < dim; d++) {
116
- let q = roundHalfToEven((pooled[d]! / norm) * QUANT);
117
- if (q > QUANT) q = QUANT;
118
- else if (q < -QUANT) q = -QUANT;
119
- out[d] = q;
120
- }
121
- return out;
130
+ // L2-normalize + int8-quantize (the shared tail, identical to the endpoint tier).
131
+ return quantize(pooled);
122
132
  }
123
133
 
124
134
  // Integer dot product of two int8 vectors (the ranking primitive). Widened to a