@maxgfr/codeindex 2.9.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.
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/docs/MIGRATION.md +29 -0
- package/package.json +1 -1
- package/scripts/engine.d.mts +73 -3
- package/scripts/engine.mjs +635 -31
- package/src/embed/encode.ts +143 -0
- package/src/embed/endpoint.ts +144 -0
- package/src/embed/index.ts +141 -0
- package/src/embed/model.ts +117 -0
- package/src/embed/search.ts +96 -0
- package/src/engine-cli.ts +184 -5
- package/src/engine.ts +33 -0
- package/src/extract/code.ts +15 -3
- package/src/mcp.ts +61 -10
- package/src/types.ts +6 -3
package/scripts/engine.mjs
CHANGED
|
@@ -14,9 +14,9 @@ var ENGINE_VERSION, SCHEMA_VERSION, EXTRACTOR_VERSION;
|
|
|
14
14
|
var init_types = __esm({
|
|
15
15
|
"src/types.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
ENGINE_VERSION = "2.
|
|
17
|
+
ENGINE_VERSION = "2.11.0";
|
|
18
18
|
SCHEMA_VERSION = 4;
|
|
19
|
-
EXTRACTOR_VERSION =
|
|
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),
|
|
@@ -9343,6 +9347,376 @@ var init_bm25 = __esm({
|
|
|
9343
9347
|
}
|
|
9344
9348
|
});
|
|
9345
9349
|
|
|
9350
|
+
// src/embed/model.ts
|
|
9351
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
9352
|
+
import { join as join10 } from "path";
|
|
9353
|
+
function resolveEmbedModelDir(repo) {
|
|
9354
|
+
const env = process.env.CODEINDEX_EMBED_DIR;
|
|
9355
|
+
const candidates = [];
|
|
9356
|
+
if (env) candidates.push(env);
|
|
9357
|
+
if (repo) candidates.push(join10(repo, ".codeindex", DEFAULT_EMBED_DIRNAME));
|
|
9358
|
+
candidates.push(join10(process.cwd(), ".codeindex", DEFAULT_EMBED_DIRNAME));
|
|
9359
|
+
for (const c2 of candidates) {
|
|
9360
|
+
if (existsSync3(join10(c2, "model.json"))) return c2;
|
|
9361
|
+
}
|
|
9362
|
+
return void 0;
|
|
9363
|
+
}
|
|
9364
|
+
function hasEmbedModel(repo) {
|
|
9365
|
+
return resolveEmbedModelDir(repo) !== void 0;
|
|
9366
|
+
}
|
|
9367
|
+
function loadEmbedModel(dir) {
|
|
9368
|
+
if (!dir) return void 0;
|
|
9369
|
+
const path = join10(dir, "model.json");
|
|
9370
|
+
if (!existsSync3(path)) return void 0;
|
|
9371
|
+
const raw = JSON.parse(readFileSync5(path, "utf8"));
|
|
9372
|
+
const { modelId, dim, vocab, weights } = raw;
|
|
9373
|
+
if (typeof modelId !== "string" || !modelId) throw new Error(`embed model: missing modelId in ${path}`);
|
|
9374
|
+
if (!Number.isInteger(dim) || dim <= 0) throw new Error(`embed model: bad dim ${dim} in ${path}`);
|
|
9375
|
+
if (!Array.isArray(vocab) || !Array.isArray(weights) || vocab.length !== weights.length) {
|
|
9376
|
+
throw new Error(`embed model: vocab/weights length mismatch in ${path}`);
|
|
9377
|
+
}
|
|
9378
|
+
const vocabSize = vocab.length;
|
|
9379
|
+
const flat = new Float64Array(vocabSize * dim);
|
|
9380
|
+
const vmap = /* @__PURE__ */ new Map();
|
|
9381
|
+
for (let i2 = 0; i2 < vocabSize; i2++) {
|
|
9382
|
+
const tok = vocab[i2];
|
|
9383
|
+
if (typeof tok !== "string") throw new Error(`embed model: non-string vocab entry at ${i2}`);
|
|
9384
|
+
if (!vmap.has(tok)) vmap.set(tok, i2);
|
|
9385
|
+
const row = weights[i2];
|
|
9386
|
+
if (!Array.isArray(row) || row.length !== dim) {
|
|
9387
|
+
throw new Error(`embed model: row ${i2} has length ${row?.length}, expected ${dim}`);
|
|
9388
|
+
}
|
|
9389
|
+
for (let d = 0; d < dim; d++) flat[i2 * dim + d] = Number(row[d]);
|
|
9390
|
+
}
|
|
9391
|
+
const unk = typeof raw.unk === "string" ? raw.unk : "[UNK]";
|
|
9392
|
+
const unkId = vmap.has(unk) ? vmap.get(unk) : -1;
|
|
9393
|
+
return { modelId, dim, unk, unkId, vocabSize, vocab: vmap, weights: flat };
|
|
9394
|
+
}
|
|
9395
|
+
function resolveEmbedPullUrl() {
|
|
9396
|
+
const url = process.env.CODEINDEX_EMBED_URL;
|
|
9397
|
+
return url && url.trim() ? url.trim() : void 0;
|
|
9398
|
+
}
|
|
9399
|
+
var EMBED_VERSION, DEFAULT_EMBED_DIRNAME;
|
|
9400
|
+
var init_model = __esm({
|
|
9401
|
+
"src/embed/model.ts"() {
|
|
9402
|
+
"use strict";
|
|
9403
|
+
EMBED_VERSION = 1;
|
|
9404
|
+
DEFAULT_EMBED_DIRNAME = "models";
|
|
9405
|
+
}
|
|
9406
|
+
});
|
|
9407
|
+
|
|
9408
|
+
// src/embed/encode.ts
|
|
9409
|
+
function basicTokenize(text) {
|
|
9410
|
+
const spaced = foldText(text).replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
|
|
9411
|
+
const out2 = [];
|
|
9412
|
+
for (const part of spaced.toLowerCase().split(/[^a-z0-9]+/)) {
|
|
9413
|
+
if (part) out2.push(part);
|
|
9414
|
+
}
|
|
9415
|
+
return out2;
|
|
9416
|
+
}
|
|
9417
|
+
function wordpiece(word, model) {
|
|
9418
|
+
if (!word) return [];
|
|
9419
|
+
const ids = [];
|
|
9420
|
+
let start2 = 0;
|
|
9421
|
+
const n = word.length;
|
|
9422
|
+
while (start2 < n) {
|
|
9423
|
+
let end = n;
|
|
9424
|
+
let match = -1;
|
|
9425
|
+
while (end > start2) {
|
|
9426
|
+
const piece = start2 === 0 ? word.slice(start2, end) : "##" + word.slice(start2, end);
|
|
9427
|
+
const id = model.vocab.get(piece);
|
|
9428
|
+
if (id !== void 0) {
|
|
9429
|
+
match = id;
|
|
9430
|
+
break;
|
|
9431
|
+
}
|
|
9432
|
+
end--;
|
|
9433
|
+
}
|
|
9434
|
+
if (match === -1) return model.unkId >= 0 ? [model.unkId] : [];
|
|
9435
|
+
ids.push(match);
|
|
9436
|
+
start2 = end;
|
|
9437
|
+
}
|
|
9438
|
+
return ids;
|
|
9439
|
+
}
|
|
9440
|
+
function tokenize(text, model) {
|
|
9441
|
+
const ids = [];
|
|
9442
|
+
for (const word of basicTokenize(text)) {
|
|
9443
|
+
for (const id of wordpiece(word, model)) ids.push(id);
|
|
9444
|
+
}
|
|
9445
|
+
return ids;
|
|
9446
|
+
}
|
|
9447
|
+
function roundHalfToEven(x) {
|
|
9448
|
+
const f = Math.floor(x);
|
|
9449
|
+
const diff = x - f;
|
|
9450
|
+
if (diff < 0.5) return f;
|
|
9451
|
+
if (diff > 0.5) return f + 1;
|
|
9452
|
+
return f % 2 === 0 ? f : f + 1;
|
|
9453
|
+
}
|
|
9454
|
+
function quantize(vec) {
|
|
9455
|
+
const dim = vec.length;
|
|
9456
|
+
const out2 = new Int8Array(dim);
|
|
9457
|
+
let sumsq = 0;
|
|
9458
|
+
for (let d = 0; d < dim; d++) sumsq += vec[d] * vec[d];
|
|
9459
|
+
const norm2 = Math.sqrt(sumsq);
|
|
9460
|
+
if (norm2 === 0) return out2;
|
|
9461
|
+
for (let d = 0; d < dim; d++) {
|
|
9462
|
+
let q = roundHalfToEven(vec[d] / norm2 * QUANT);
|
|
9463
|
+
if (q > QUANT) q = QUANT;
|
|
9464
|
+
else if (q < -QUANT) q = -QUANT;
|
|
9465
|
+
out2[d] = q;
|
|
9466
|
+
}
|
|
9467
|
+
return out2;
|
|
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
|
+
}
|
|
9482
|
+
function intDot(a, b) {
|
|
9483
|
+
const n = Math.min(a.length, b.length);
|
|
9484
|
+
let dot = 0;
|
|
9485
|
+
for (let i2 = 0; i2 < n; i2++) dot += a[i2] * b[i2];
|
|
9486
|
+
return dot;
|
|
9487
|
+
}
|
|
9488
|
+
var QUANT;
|
|
9489
|
+
var init_encode = __esm({
|
|
9490
|
+
"src/embed/encode.ts"() {
|
|
9491
|
+
"use strict";
|
|
9492
|
+
init_util();
|
|
9493
|
+
QUANT = 127;
|
|
9494
|
+
}
|
|
9495
|
+
});
|
|
9496
|
+
|
|
9497
|
+
// src/embed/index.ts
|
|
9498
|
+
function symbolText(rel, name2, signature, summary) {
|
|
9499
|
+
return [name2, signature ?? "", summary ?? "", rel.replace(/\//g, " ")].join("\n");
|
|
9500
|
+
}
|
|
9501
|
+
function fileText(rel, title, summary, headings) {
|
|
9502
|
+
return [title ?? "", summary ?? "", ...headings, rel.replace(/\//g, " ")].join("\n");
|
|
9503
|
+
}
|
|
9504
|
+
function embeddingUnits(scan2) {
|
|
9505
|
+
const units = [];
|
|
9506
|
+
for (const f of scan2.files) {
|
|
9507
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9508
|
+
let hadSymbol = false;
|
|
9509
|
+
for (const s of f.symbols) {
|
|
9510
|
+
if (seen.has(s.name)) continue;
|
|
9511
|
+
seen.add(s.name);
|
|
9512
|
+
hadSymbol = true;
|
|
9513
|
+
units.push({ file: f.rel, symbol: s.name, line: s.line, text: symbolText(f.rel, s.name, s.signature, f.summary) });
|
|
9514
|
+
}
|
|
9515
|
+
if (!hadSymbol) {
|
|
9516
|
+
const text = fileText(f.rel, f.title, f.summary, f.headings);
|
|
9517
|
+
if (text.replace(/\s+/g, "")) units.push({ file: f.rel, text });
|
|
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
|
+
});
|
|
9529
|
+
return { embedVersion: EMBED_VERSION, modelId: model.modelId, dim: model.dim, records };
|
|
9530
|
+
}
|
|
9531
|
+
function serializeEmbeddings(index) {
|
|
9532
|
+
const header = JSON.stringify({
|
|
9533
|
+
embedVersion: index.embedVersion,
|
|
9534
|
+
modelId: index.modelId,
|
|
9535
|
+
dim: index.dim,
|
|
9536
|
+
count: index.records.length,
|
|
9537
|
+
records: index.records.map((r) => ({ file: r.file, symbol: r.symbol ?? "", line: r.line ?? 0 }))
|
|
9538
|
+
});
|
|
9539
|
+
const headerBuf = Buffer.from(header, "utf8");
|
|
9540
|
+
const body2 = Buffer.alloc(index.records.length * index.dim);
|
|
9541
|
+
let off = 0;
|
|
9542
|
+
for (const r of index.records) {
|
|
9543
|
+
for (let d = 0; d < index.dim; d++) body2.writeInt8(r.vec[d] ?? 0, off++);
|
|
9544
|
+
}
|
|
9545
|
+
const out2 = Buffer.alloc(8 + headerBuf.length + body2.length);
|
|
9546
|
+
out2.write(MAGIC, 0, "ascii");
|
|
9547
|
+
out2.writeUInt32LE(headerBuf.length, 4);
|
|
9548
|
+
headerBuf.copy(out2, 8);
|
|
9549
|
+
body2.copy(out2, 8 + headerBuf.length);
|
|
9550
|
+
return out2;
|
|
9551
|
+
}
|
|
9552
|
+
function deserializeEmbeddings(bytes) {
|
|
9553
|
+
const buf = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
9554
|
+
if (buf.length < 8 || buf.toString("ascii", 0, 4) !== MAGIC) {
|
|
9555
|
+
throw new Error("embeddings.bin: bad magic (not a codeindex embeddings artifact)");
|
|
9556
|
+
}
|
|
9557
|
+
const headerLen = buf.readUInt32LE(4);
|
|
9558
|
+
const header = JSON.parse(buf.toString("utf8", 8, 8 + headerLen));
|
|
9559
|
+
const bodyOff = 8 + headerLen;
|
|
9560
|
+
const { dim } = header;
|
|
9561
|
+
const records = header.records.map((m, i2) => {
|
|
9562
|
+
const vec = new Int8Array(dim);
|
|
9563
|
+
for (let d = 0; d < dim; d++) vec[d] = buf.readInt8(bodyOff + i2 * dim + d);
|
|
9564
|
+
const rec = { file: m.file, vec };
|
|
9565
|
+
if (m.symbol) rec.symbol = m.symbol;
|
|
9566
|
+
if (m.line) rec.line = m.line;
|
|
9567
|
+
return rec;
|
|
9568
|
+
});
|
|
9569
|
+
return { embedVersion: header.embedVersion, modelId: header.modelId, dim, records };
|
|
9570
|
+
}
|
|
9571
|
+
var MAGIC;
|
|
9572
|
+
var init_embed = __esm({
|
|
9573
|
+
"src/embed/index.ts"() {
|
|
9574
|
+
"use strict";
|
|
9575
|
+
init_encode();
|
|
9576
|
+
init_model();
|
|
9577
|
+
MAGIC = "CIE1";
|
|
9578
|
+
}
|
|
9579
|
+
});
|
|
9580
|
+
|
|
9581
|
+
// src/embed/search.ts
|
|
9582
|
+
function searchSemantic(scan2, query, index, opts = {}) {
|
|
9583
|
+
const limit = opts.limit ?? DEFAULT_LIMIT2;
|
|
9584
|
+
const lexical = searchIndex(scan2, query, { limit: Math.max(limit, 50), fuzzy: opts.fuzzy });
|
|
9585
|
+
const q = opts.queryVec ?? (opts.model ? encode(opts.model, query) : void 0);
|
|
9586
|
+
if (!q || !index || index.records.length === 0) {
|
|
9587
|
+
return lexical.slice(0, limit);
|
|
9588
|
+
}
|
|
9589
|
+
const bestByFile = /* @__PURE__ */ new Map();
|
|
9590
|
+
for (const r of index.records) {
|
|
9591
|
+
const dot = intDot(q, r.vec);
|
|
9592
|
+
const prev = bestByFile.get(r.file);
|
|
9593
|
+
if (!prev || dot > prev.score) bestByFile.set(r.file, { score: dot, symbol: r.symbol });
|
|
9594
|
+
}
|
|
9595
|
+
const semList = [...bestByFile.entries()].filter(([, v]) => v.score > 0).sort((a, b) => b[1].score - a[1].score || byStr(a[0], b[0])).map(([file]) => file);
|
|
9596
|
+
const lexList = lexical.map((r) => r.file);
|
|
9597
|
+
const fused = rrf([lexList, semList], (f) => f, opts.rrfK ?? RRF_K);
|
|
9598
|
+
const lexByFile = new Map(lexical.map((r) => [r.file, r]));
|
|
9599
|
+
const results = [...fused.entries()].sort((a, b) => b[1] - a[1] || byStr(a[0], b[0])).map(([file, score]) => {
|
|
9600
|
+
const lex = lexByFile.get(file);
|
|
9601
|
+
const res = {
|
|
9602
|
+
file,
|
|
9603
|
+
score: Number(score.toFixed(4)),
|
|
9604
|
+
matchedTerms: lex?.matchedTerms ?? [],
|
|
9605
|
+
topSymbols: lex?.topSymbols ?? []
|
|
9606
|
+
};
|
|
9607
|
+
const sem = bestByFile.get(file);
|
|
9608
|
+
if (sem?.symbol) res.semanticSymbol = sem.symbol;
|
|
9609
|
+
if (lex?.fuzzyTerms) res.fuzzyTerms = lex.fuzzyTerms;
|
|
9610
|
+
return res;
|
|
9611
|
+
});
|
|
9612
|
+
return results.slice(0, limit);
|
|
9613
|
+
}
|
|
9614
|
+
var DEFAULT_LIMIT2, RRF_K;
|
|
9615
|
+
var init_search = __esm({
|
|
9616
|
+
"src/embed/search.ts"() {
|
|
9617
|
+
"use strict";
|
|
9618
|
+
init_util();
|
|
9619
|
+
init_sort();
|
|
9620
|
+
init_bm25();
|
|
9621
|
+
init_encode();
|
|
9622
|
+
DEFAULT_LIMIT2 = 20;
|
|
9623
|
+
RRF_K = 60;
|
|
9624
|
+
}
|
|
9625
|
+
});
|
|
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
|
+
|
|
9346
9720
|
// src/rules.ts
|
|
9347
9721
|
function isEntrypointLike(rel) {
|
|
9348
9722
|
const base = rel.split("/").pop();
|
|
@@ -9647,7 +10021,7 @@ var init_deadcode = __esm({
|
|
|
9647
10021
|
});
|
|
9648
10022
|
|
|
9649
10023
|
// src/complexity.ts
|
|
9650
|
-
import { join as
|
|
10024
|
+
import { join as join11 } from "path";
|
|
9651
10025
|
function complexityOfSource(source) {
|
|
9652
10026
|
return 1 + (source.match(BRANCH_RE) ?? []).length;
|
|
9653
10027
|
}
|
|
@@ -9657,7 +10031,7 @@ function symbolComplexity(scan2, rel, top = 50) {
|
|
|
9657
10031
|
if (f.kind !== "code") continue;
|
|
9658
10032
|
if (rel && f.rel !== rel) continue;
|
|
9659
10033
|
if (!f.symbols.length) continue;
|
|
9660
|
-
const lines = readText(
|
|
10034
|
+
const lines = readText(join11(scan2.root, f.rel)).split("\n");
|
|
9661
10035
|
for (const s of f.symbols) {
|
|
9662
10036
|
if (s.kind === "reexport" || s.kind === "reexport-all") continue;
|
|
9663
10037
|
const end = s.endLine ?? s.line;
|
|
@@ -9672,7 +10046,7 @@ function symbolComplexity(scan2, rel, top = 50) {
|
|
|
9672
10046
|
}
|
|
9673
10047
|
function riskHotspots(scan2, churn, top = 20) {
|
|
9674
10048
|
const out2 = scan2.files.filter((f) => f.kind === "code").map((f) => {
|
|
9675
|
-
const complexity = complexityOfSource(readText(
|
|
10049
|
+
const complexity = complexityOfSource(readText(join11(scan2.root, f.rel)));
|
|
9676
10050
|
const commits = churn.get(f.rel) ?? 0;
|
|
9677
10051
|
return { file: f.rel, complexity, commits, score: (commits + 1) * complexity };
|
|
9678
10052
|
});
|
|
@@ -9738,7 +10112,7 @@ function str(v) {
|
|
|
9738
10112
|
function strArray(v) {
|
|
9739
10113
|
return Array.isArray(v) && v.every((x) => typeof x === "string") && v.length ? v : void 0;
|
|
9740
10114
|
}
|
|
9741
|
-
function callTool(name2, args2) {
|
|
10115
|
+
async function callTool(name2, args2) {
|
|
9742
10116
|
const repo = str(args2.repo);
|
|
9743
10117
|
if (!repo) throw new Error("`repo` is required (absolute path to the repository root)");
|
|
9744
10118
|
const scanOpts = { scope: str(args2.scope), include: strArray(args2.include), exclude: strArray(args2.exclude) };
|
|
@@ -9871,11 +10245,42 @@ function callTool(name2, args2) {
|
|
|
9871
10245
|
if (name2 === "search") {
|
|
9872
10246
|
const query = str(args2.query);
|
|
9873
10247
|
if (!query) throw new Error("`query` is required");
|
|
9874
|
-
const
|
|
9875
|
-
|
|
9876
|
-
|
|
9877
|
-
|
|
9878
|
-
|
|
10248
|
+
const scan2 = scanRepo(repo, scanOpts);
|
|
10249
|
+
const limit = typeof args2.limit === "number" ? args2.limit : void 0;
|
|
10250
|
+
const fuzzy = typeof args2.fuzzy === "boolean" ? args2.fuzzy : void 0;
|
|
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
|
+
}
|
|
10262
|
+
const modelDir = resolveEmbedModelDir(repo);
|
|
10263
|
+
const model = modelDir ? loadEmbedModel(modelDir) : void 0;
|
|
10264
|
+
if (model) {
|
|
10265
|
+
const index = buildEmbeddingIndex(scan2, model);
|
|
10266
|
+
return JSON.stringify(searchSemantic(scan2, query, index, { model, limit, fuzzy }), null, 2);
|
|
10267
|
+
}
|
|
10268
|
+
}
|
|
10269
|
+
return JSON.stringify(searchIndex(scan2, query, { limit, fuzzy }), null, 2);
|
|
10270
|
+
}
|
|
10271
|
+
if (name2 === "embed_status") {
|
|
10272
|
+
const modelDir = resolveEmbedModelDir(repo);
|
|
10273
|
+
const model = modelDir ? loadEmbedModel(modelDir) : void 0;
|
|
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);
|
|
9879
10284
|
}
|
|
9880
10285
|
if (name2 === "check_rules") {
|
|
9881
10286
|
const rules = parseRules(args2.rules);
|
|
@@ -9901,9 +10306,9 @@ async function runMcpServer() {
|
|
|
9901
10306
|
continue;
|
|
9902
10307
|
}
|
|
9903
10308
|
const requests = Array.isArray(parsed) ? parsed : [parsed];
|
|
9904
|
-
for (const req of requests) handle2(req);
|
|
10309
|
+
for (const req of requests) await handle2(req);
|
|
9905
10310
|
}
|
|
9906
|
-
function handle2(req) {
|
|
10311
|
+
async function handle2(req) {
|
|
9907
10312
|
if (req.id === void 0 || req.id === null) return;
|
|
9908
10313
|
try {
|
|
9909
10314
|
if (req.method === "initialize") {
|
|
@@ -9924,7 +10329,7 @@ async function runMcpServer() {
|
|
|
9924
10329
|
const name2 = str(params.name) ?? "";
|
|
9925
10330
|
const args2 = params.arguments ?? {};
|
|
9926
10331
|
try {
|
|
9927
|
-
const text = callTool(name2, args2);
|
|
10332
|
+
const text = await callTool(name2, args2);
|
|
9928
10333
|
send({ id: req.id, result: { content: [{ type: "text", text }] } });
|
|
9929
10334
|
} catch (e) {
|
|
9930
10335
|
send({
|
|
@@ -9963,6 +10368,10 @@ var init_mcp = __esm({
|
|
|
9963
10368
|
init_memory();
|
|
9964
10369
|
init_bm25();
|
|
9965
10370
|
init_rules();
|
|
10371
|
+
init_model();
|
|
10372
|
+
init_embed();
|
|
10373
|
+
init_search();
|
|
10374
|
+
init_endpoint();
|
|
9966
10375
|
repoProp = { repo: { type: "string", description: "Absolute path to the repository root" } };
|
|
9967
10376
|
scopeProps = {
|
|
9968
10377
|
scope: { type: "string", description: "Restrict to one directory (repo-relative)" },
|
|
@@ -10175,7 +10584,7 @@ var init_mcp = __esm({
|
|
|
10175
10584
|
},
|
|
10176
10585
|
{
|
|
10177
10586
|
name: "search",
|
|
10178
|
-
description: 'Natural-language-ish lexical search: BM25 ranking (k1=1.2, b=0.75) over symbol names (camelCase/snake_case subtokens), file path segments, markdown headings and summary lines. NOT embeddings \u2014 deterministic, diacritic-folded, zero API keys. Answers "where is auth handled?"-style queries with ranked files, matched terms and top symbols. Query terms with zero document frequency get a deterministic trigram-fuzzy fallback (typo-tolerant) unless `fuzzy: false`.',
|
|
10587
|
+
description: 'Natural-language-ish lexical search: BM25 ranking (k1=1.2, b=0.75) over symbol names (camelCase/snake_case subtokens), file path segments, markdown headings and summary lines. NOT embeddings by default \u2014 deterministic, diacritic-folded, zero API keys. Answers "where is auth handled?"-style queries with ranked files, matched terms and top symbols. Query terms with zero document frequency get a deterministic trigram-fuzzy fallback (typo-tolerant) unless `fuzzy: false`. Set `semantic: true` to RRF-fuse the deterministic static-embedding tier when a model asset is present (degrades to lexical otherwise \u2014 see embed_status).',
|
|
10179
10588
|
inputSchema: {
|
|
10180
10589
|
type: "object",
|
|
10181
10590
|
properties: {
|
|
@@ -10186,11 +10595,20 @@ var init_mcp = __esm({
|
|
|
10186
10595
|
fuzzy: {
|
|
10187
10596
|
type: "boolean",
|
|
10188
10597
|
description: "Trigram fuzzy fallback for query terms with zero document frequency (default true)"
|
|
10598
|
+
},
|
|
10599
|
+
semantic: {
|
|
10600
|
+
type: "boolean",
|
|
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."
|
|
10189
10602
|
}
|
|
10190
10603
|
},
|
|
10191
10604
|
required: ["repo", "query"]
|
|
10192
10605
|
}
|
|
10193
10606
|
},
|
|
10607
|
+
{
|
|
10608
|
+
name: "embed_status",
|
|
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.",
|
|
10610
|
+
inputSchema: { type: "object", properties: { ...repoProp }, required: ["repo"] }
|
|
10611
|
+
},
|
|
10194
10612
|
{
|
|
10195
10613
|
name: "check_rules",
|
|
10196
10614
|
description: 'Validate dependency-cruiser-style architecture rules against the link-graph. Rules (inline JSON array): forbidden edges {name, from, to, kind?, severity?, comment?} with glob paths, plus builtins {name, builtin: "cycles"|"orphans"} (module-level import cycles; edge-less code files). Returns deterministic violations with severity error|warn \u2014 a CI gate.',
|
|
@@ -10605,6 +11023,11 @@ init_pipeline();
|
|
|
10605
11023
|
init_git();
|
|
10606
11024
|
init_grep();
|
|
10607
11025
|
init_bm25();
|
|
11026
|
+
init_model();
|
|
11027
|
+
init_encode();
|
|
11028
|
+
init_embed();
|
|
11029
|
+
init_search();
|
|
11030
|
+
init_endpoint();
|
|
10608
11031
|
init_rules();
|
|
10609
11032
|
init_coupling();
|
|
10610
11033
|
init_repomap();
|
|
@@ -10623,8 +11046,8 @@ init_loader();
|
|
|
10623
11046
|
init_pipeline();
|
|
10624
11047
|
init_graph_json();
|
|
10625
11048
|
init_symbols_json();
|
|
10626
|
-
import { existsSync as
|
|
10627
|
-
import { join as
|
|
11049
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
11050
|
+
import { join as join12, resolve } from "path";
|
|
10628
11051
|
init_scan();
|
|
10629
11052
|
init_callers();
|
|
10630
11053
|
init_workspaces();
|
|
@@ -10637,6 +11060,11 @@ init_complexity();
|
|
|
10637
11060
|
init_viz();
|
|
10638
11061
|
init_bm25();
|
|
10639
11062
|
init_rules();
|
|
11063
|
+
init_model();
|
|
11064
|
+
init_embed();
|
|
11065
|
+
init_search();
|
|
11066
|
+
init_endpoint();
|
|
11067
|
+
init_util();
|
|
10640
11068
|
var HELP = `codeindex engine v${ENGINE_VERSION} \u2014 deterministic repo indexing
|
|
10641
11069
|
|
|
10642
11070
|
Usage: engine.mjs <command> [flags]
|
|
@@ -10654,7 +11082,18 @@ Commands:
|
|
|
10654
11082
|
churn Per-file git commit counts (JSON; --since <ref> to bound)
|
|
10655
11083
|
grep Search: cli.mjs grep <pattern> --repo <dir> (JSON hits)
|
|
10656
11084
|
search Keyless BM25 lexical search over symbol names, path segments,
|
|
10657
|
-
markdown headings and summaries: cli.mjs search "<query>" --repo <dir
|
|
11085
|
+
markdown headings and summaries: cli.mjs search "<query>" --repo <dir>.
|
|
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)
|
|
11093
|
+
embed pull Fetch the model asset into CODEINDEX_EMBED_DIR (or
|
|
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)
|
|
10658
11097
|
rules Architecture rules (forbidden edges, cycles, orphans) validated
|
|
10659
11098
|
against the link-graph: --config <codeindex.rules.json>; exits 1
|
|
10660
11099
|
on any error-severity violation (a CI gate)
|
|
@@ -10682,12 +11121,16 @@ Flags:
|
|
|
10682
11121
|
--limit <n> Max results for \`search\` (default 20)
|
|
10683
11122
|
--no-fuzzy \`search\`: disable trigram fuzzy fallback for query terms
|
|
10684
11123
|
with zero document frequency (default: enabled)
|
|
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
|
|
10685
11128
|
--recall \`callers\`: recall-oriented binding (issue #7) \u2014 relaxes
|
|
10686
11129
|
the JS/TS import gate to unique repo-wide names and labels
|
|
10687
11130
|
each site corroborated|unique-name
|
|
10688
11131
|
`;
|
|
10689
11132
|
function parseFlags(args2) {
|
|
10690
|
-
const flags2 = { repo: process.cwd(), include: [], exclude: [], gitignore: true, noAst: false, fuzzy: true };
|
|
11133
|
+
const flags2 = { repo: process.cwd(), include: [], exclude: [], gitignore: true, noAst: false, fuzzy: true, semantic: false };
|
|
10691
11134
|
for (let i2 = 0; i2 < args2.length; i2++) {
|
|
10692
11135
|
const a = args2[i2];
|
|
10693
11136
|
const next = () => {
|
|
@@ -10720,7 +11163,9 @@ function parseFlags(args2) {
|
|
|
10720
11163
|
else if (a === "--config") flags2.config = resolve(next());
|
|
10721
11164
|
else if (a === "--limit") flags2.limit = num();
|
|
10722
11165
|
else if (a === "--no-fuzzy") flags2.fuzzy = false;
|
|
11166
|
+
else if (a === "--semantic") flags2.semantic = true;
|
|
10723
11167
|
else if (a === "--recall") flags2.recall = true;
|
|
11168
|
+
else if (a === "--run") flags2.run = true;
|
|
10724
11169
|
else if (!a.startsWith("--") && flags2.positional === void 0) flags2.positional = a;
|
|
10725
11170
|
else throw new Error(`unknown flag: ${a}`);
|
|
10726
11171
|
}
|
|
@@ -10756,24 +11201,24 @@ async function runCli(argv) {
|
|
|
10756
11201
|
return;
|
|
10757
11202
|
}
|
|
10758
11203
|
const flags2 = parseFlags(rest);
|
|
10759
|
-
if (!
|
|
11204
|
+
if (!existsSync4(flags2.repo)) throw new Error(`--repo path does not exist: ${flags2.repo}`);
|
|
10760
11205
|
if (!flags2.noAst) await ensureGrammars(allGrammarKeys());
|
|
10761
11206
|
if (cmd === "index") {
|
|
10762
11207
|
if (!flags2.out) throw new Error("index needs --out <dir>");
|
|
10763
11208
|
const outDir = flags2.out;
|
|
10764
11209
|
mkdirSync2(outDir, { recursive: true });
|
|
10765
|
-
const cachePath =
|
|
11210
|
+
const cachePath = join12(outDir, "cache.json");
|
|
10766
11211
|
let cache;
|
|
10767
11212
|
try {
|
|
10768
|
-
const parsed = JSON.parse(
|
|
11213
|
+
const parsed = JSON.parse(readFileSync6(cachePath, "utf8"));
|
|
10769
11214
|
if (parsed.schemaVersion === SCHEMA_VERSION && parsed.extractorVersion === EXTRACTOR_VERSION) {
|
|
10770
11215
|
cache = new Map(Object.entries(parsed.files));
|
|
10771
11216
|
}
|
|
10772
11217
|
} catch {
|
|
10773
11218
|
}
|
|
10774
11219
|
const { scan: scan2, graph, symbols } = buildIndexArtifacts(flags2.repo, { ...scanOptions(flags2), cache, out: outDir });
|
|
10775
|
-
writeFileSync3(
|
|
10776
|
-
writeFileSync3(
|
|
11220
|
+
writeFileSync3(join12(outDir, "graph.json"), renderGraphJson(graph));
|
|
11221
|
+
writeFileSync3(join12(outDir, "symbols.json"), renderSymbolsJson(symbols));
|
|
10777
11222
|
const files = {};
|
|
10778
11223
|
for (const f of scan2.files) {
|
|
10779
11224
|
const entry = { hash: f.hash, record: f, size: f.size };
|
|
@@ -10785,7 +11230,15 @@ async function runCli(argv) {
|
|
|
10785
11230
|
cachePath,
|
|
10786
11231
|
JSON.stringify({ schemaVersion: SCHEMA_VERSION, extractorVersion: EXTRACTOR_VERSION, files }) + "\n"
|
|
10787
11232
|
);
|
|
10788
|
-
|
|
11233
|
+
let embedNote = "";
|
|
11234
|
+
const modelDir = resolveEmbedModelDir(flags2.repo);
|
|
11235
|
+
const model = modelDir ? loadEmbedModel(modelDir) : void 0;
|
|
11236
|
+
if (model) {
|
|
11237
|
+
const index = buildEmbeddingIndex(scan2, model);
|
|
11238
|
+
writeFileSync3(join12(outDir, "embeddings.bin"), serializeEmbeddings(index));
|
|
11239
|
+
embedNote = ` + embeddings.bin (${index.records.length} records, model ${model.modelId})`;
|
|
11240
|
+
}
|
|
11241
|
+
process.stderr.write(`codeindex: ${scan2.files.length} files \u2192 ${outDir}/graph.json + symbols.json${embedNote}${scan2.capped ? " (capped)" : ""}
|
|
10789
11242
|
`);
|
|
10790
11243
|
} else if (cmd === "scan") {
|
|
10791
11244
|
const { scan: scan2 } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
|
|
@@ -10822,11 +11275,138 @@ async function runCli(argv) {
|
|
|
10822
11275
|
} else if (cmd === "search") {
|
|
10823
11276
|
if (!flags2.positional) throw new Error('search needs a query: cli.mjs search "<query>" --repo <dir>');
|
|
10824
11277
|
const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
|
|
10825
|
-
|
|
10826
|
-
|
|
11278
|
+
if (flags2.semantic) {
|
|
11279
|
+
const endpoint = resolveEmbedEndpoint();
|
|
11280
|
+
const lexical = () => {
|
|
11281
|
+
const results = searchIndex(scan2, flags2.positional, { limit: flags2.limit, fuzzy: flags2.fuzzy });
|
|
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
|
+
}
|
|
11297
|
+
} else {
|
|
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
|
+
}
|
|
11310
|
+
}
|
|
11311
|
+
} else {
|
|
11312
|
+
const results = searchIndex(scan2, flags2.positional, { limit: flags2.limit, fuzzy: flags2.fuzzy });
|
|
11313
|
+
emit(JSON.stringify(results, null, 2) + "\n", flags2.out);
|
|
11314
|
+
}
|
|
11315
|
+
} else if (cmd === "embed") {
|
|
11316
|
+
const sub = flags2.positional;
|
|
11317
|
+
const modelDir = resolveEmbedModelDir(flags2.repo);
|
|
11318
|
+
if (sub === "status") {
|
|
11319
|
+
const model = modelDir ? loadEmbedModel(modelDir) : void 0;
|
|
11320
|
+
const endpoint = resolveEmbedEndpoint();
|
|
11321
|
+
const mode = endpoint ? "endpoint" : model ? "static" : "none";
|
|
11322
|
+
const status = {
|
|
11323
|
+
embedVersion: EMBED_VERSION,
|
|
11324
|
+
mode,
|
|
11325
|
+
model: model ? { present: true, dir: modelDir, modelId: model.modelId, dim: model.dim, vocabSize: model.vocabSize } : { present: false },
|
|
11326
|
+
endpoint: endpoint ?? null
|
|
11327
|
+
};
|
|
11328
|
+
if (endpoint) status.endpointReachable = await probeEndpoint(endpoint);
|
|
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
|
+
}
|
|
11359
|
+
} else if (sub === "build") {
|
|
11360
|
+
if (!flags2.out) throw new Error("embed build needs --out <dir>");
|
|
11361
|
+
if (!modelDir) {
|
|
11362
|
+
process.stderr.write("codeindex: no embedding model present \u2014 run `codeindex embed pull` first (nothing written)\n");
|
|
11363
|
+
process.exitCode = 1;
|
|
11364
|
+
return;
|
|
11365
|
+
}
|
|
11366
|
+
const model = loadEmbedModel(modelDir);
|
|
11367
|
+
mkdirSync2(flags2.out, { recursive: true });
|
|
11368
|
+
const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
|
|
11369
|
+
const index = buildEmbeddingIndex(scan2, model);
|
|
11370
|
+
writeFileSync3(join12(flags2.out, "embeddings.bin"), serializeEmbeddings(index));
|
|
11371
|
+
process.stderr.write(`codeindex: ${index.records.length} embedding records \u2192 ${flags2.out}/embeddings.bin (model ${model.modelId})
|
|
11372
|
+
`);
|
|
11373
|
+
} else if (sub === "pull") {
|
|
11374
|
+
const url = resolveEmbedPullUrl();
|
|
11375
|
+
if (!url) {
|
|
11376
|
+
process.stderr.write(
|
|
11377
|
+
"codeindex: no model URL configured. The official static-embedding asset is not published yet.\nSet CODEINDEX_EMBED_URL to a model.json URL (optionally CODEINDEX_EMBED_DIR as the destination), then re-run `codeindex embed pull`.\n"
|
|
11378
|
+
);
|
|
11379
|
+
process.exitCode = 1;
|
|
11380
|
+
return;
|
|
11381
|
+
}
|
|
11382
|
+
const destDir = process.env.CODEINDEX_EMBED_DIR ?? join12(flags2.repo, ".codeindex", "models");
|
|
11383
|
+
mkdirSync2(destDir, { recursive: true });
|
|
11384
|
+
process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${join12(destDir, "model.json")}
|
|
11385
|
+
`);
|
|
11386
|
+
const res = await fetch(url);
|
|
11387
|
+
if (!res.ok) {
|
|
11388
|
+
process.stderr.write(`codeindex: pull failed \u2014 HTTP ${res.status} from ${url}
|
|
11389
|
+
`);
|
|
11390
|
+
process.exitCode = 1;
|
|
11391
|
+
return;
|
|
11392
|
+
}
|
|
11393
|
+
const body2 = await res.text();
|
|
11394
|
+
try {
|
|
11395
|
+
JSON.parse(body2);
|
|
11396
|
+
} catch {
|
|
11397
|
+
process.stderr.write("codeindex: pull failed \u2014 response is not a valid model.json (expected JSON)\n");
|
|
11398
|
+
process.exitCode = 1;
|
|
11399
|
+
return;
|
|
11400
|
+
}
|
|
11401
|
+
writeFileSync3(join12(destDir, "model.json"), body2);
|
|
11402
|
+
process.stderr.write(`codeindex: model written to ${join12(destDir, "model.json")}
|
|
11403
|
+
`);
|
|
11404
|
+
} else {
|
|
11405
|
+
throw new Error("embed needs a subcommand: status | build | pull | serve");
|
|
11406
|
+
}
|
|
10827
11407
|
} else if (cmd === "rules") {
|
|
10828
11408
|
if (!flags2.config) throw new Error("rules needs --config <codeindex.rules.json>");
|
|
10829
|
-
const rules = parseRules(JSON.parse(
|
|
11409
|
+
const rules = parseRules(JSON.parse(readFileSync6(flags2.config, "utf8")));
|
|
10830
11410
|
const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
|
|
10831
11411
|
const violations = checkRules(graph, rules);
|
|
10832
11412
|
const errors = violations.filter((v) => v.severity === "error").length;
|
|
@@ -10887,14 +11467,18 @@ ${HELP}`);
|
|
|
10887
11467
|
}
|
|
10888
11468
|
export {
|
|
10889
11469
|
DEFAULT_MAX_FILES,
|
|
11470
|
+
EMBED_VERSION,
|
|
10890
11471
|
ENGINE_VERSION,
|
|
10891
11472
|
EXTRACTOR_VERSION,
|
|
10892
11473
|
MARKDOWN_EXT,
|
|
10893
11474
|
SCHEMA_VERSION,
|
|
10894
11475
|
allGrammarKeys,
|
|
10895
11476
|
applyCentrality,
|
|
11477
|
+
basicTokenize,
|
|
10896
11478
|
betweennessOf,
|
|
10897
11479
|
buildCallerIndex,
|
|
11480
|
+
buildEmbeddingIndex,
|
|
11481
|
+
buildEndpointIndex,
|
|
10898
11482
|
buildGraph,
|
|
10899
11483
|
buildIndexArtifacts,
|
|
10900
11484
|
buildModules,
|
|
@@ -10917,11 +11501,17 @@ export {
|
|
|
10917
11501
|
computeSymbolRefs,
|
|
10918
11502
|
computeTestMap,
|
|
10919
11503
|
deleteMemory,
|
|
11504
|
+
deserializeEmbeddings,
|
|
10920
11505
|
detectCommunities,
|
|
10921
11506
|
detectWorkspaces,
|
|
10922
11507
|
diffFiles,
|
|
10923
11508
|
diffHunks,
|
|
11509
|
+
embedEndpointUrl,
|
|
11510
|
+
embedViaEndpoint,
|
|
11511
|
+
embeddingUnits,
|
|
10924
11512
|
enclosingSymbol,
|
|
11513
|
+
encode,
|
|
11514
|
+
encodeQueryViaEndpoint,
|
|
10925
11515
|
ensureGrammars,
|
|
10926
11516
|
escapeRegExp,
|
|
10927
11517
|
extToLang,
|
|
@@ -10937,10 +11527,13 @@ export {
|
|
|
10937
11527
|
grammarKeyForExt,
|
|
10938
11528
|
grammarReady,
|
|
10939
11529
|
grepRepo,
|
|
11530
|
+
hasEmbedModel,
|
|
10940
11531
|
have,
|
|
10941
11532
|
headCommit,
|
|
11533
|
+
healthzUrl,
|
|
10942
11534
|
insertAfterSymbol,
|
|
10943
11535
|
insertBeforeSymbol,
|
|
11536
|
+
intDot,
|
|
10944
11537
|
isCode,
|
|
10945
11538
|
isDoc,
|
|
10946
11539
|
isGitWorktree,
|
|
@@ -10951,9 +11544,12 @@ export {
|
|
|
10951
11544
|
keywords,
|
|
10952
11545
|
languageOf,
|
|
10953
11546
|
listMemories,
|
|
11547
|
+
loadEmbedModel,
|
|
10954
11548
|
pagerankOf,
|
|
10955
11549
|
parseGitignore,
|
|
10956
11550
|
parseRules,
|
|
11551
|
+
probeEndpoint,
|
|
11552
|
+
quantize,
|
|
10957
11553
|
rankHotspots,
|
|
10958
11554
|
rankedKeywords,
|
|
10959
11555
|
readMemory,
|
|
@@ -10967,14 +11563,20 @@ export {
|
|
|
10967
11563
|
resolveBaseRef,
|
|
10968
11564
|
resolveCallEdges,
|
|
10969
11565
|
resolveDocLink,
|
|
11566
|
+
resolveEmbedEndpoint,
|
|
11567
|
+
resolveEmbedModelDir,
|
|
11568
|
+
resolveEmbedPullUrl,
|
|
10970
11569
|
resolveImport,
|
|
10971
11570
|
resolveUniqueSymbol,
|
|
10972
11571
|
riskHotspots,
|
|
11572
|
+
roundHalfToEven,
|
|
10973
11573
|
rrf,
|
|
10974
11574
|
runCli,
|
|
10975
11575
|
runMcpServer,
|
|
10976
11576
|
scanRepo,
|
|
10977
11577
|
searchIndex,
|
|
11578
|
+
searchSemantic,
|
|
11579
|
+
serializeEmbeddings,
|
|
10978
11580
|
sh,
|
|
10979
11581
|
sha1,
|
|
10980
11582
|
shortHash,
|
|
@@ -10984,10 +11586,12 @@ export {
|
|
|
10984
11586
|
symbolsOverview,
|
|
10985
11587
|
testsForModule,
|
|
10986
11588
|
tierForPath,
|
|
11589
|
+
tokenize,
|
|
10987
11590
|
uniqueSymbolDefs,
|
|
10988
11591
|
untestedModules,
|
|
10989
11592
|
untrackedFiles,
|
|
10990
11593
|
walk,
|
|
11594
|
+
wordpiece,
|
|
10991
11595
|
writeMemory
|
|
10992
11596
|
};
|
|
10993
11597
|
// "Copyright" and "@license" are already caught by DIRECTIVE_RE.
|