kontext-engine 0.1.3 → 0.1.5
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/README.md +131 -84
- package/dist/cli/index.js +1005 -467
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +680 -202
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -802,29 +802,29 @@ async function createLocalEmbedder() {
|
|
|
802
802
|
}
|
|
803
803
|
var VOYAGE_API_URL = "https://api.voyageai.com/v1/embeddings";
|
|
804
804
|
var VOYAGE_MODEL = "voyage-code-3";
|
|
805
|
-
var
|
|
805
|
+
var VOYAGE_DEFAULT_DIMENSIONS = 1024;
|
|
806
806
|
var VOYAGE_BATCH_SIZE = 128;
|
|
807
|
-
function createVoyageEmbedder(apiKey) {
|
|
807
|
+
function createVoyageEmbedder(apiKey, dimensions = VOYAGE_DEFAULT_DIMENSIONS) {
|
|
808
808
|
return {
|
|
809
809
|
name: VOYAGE_MODEL,
|
|
810
|
-
dimensions
|
|
810
|
+
dimensions,
|
|
811
811
|
async embed(texts, onProgress) {
|
|
812
812
|
const results = [];
|
|
813
813
|
for (let i = 0; i < texts.length; i += VOYAGE_BATCH_SIZE) {
|
|
814
814
|
const batch = texts.slice(i, i + VOYAGE_BATCH_SIZE);
|
|
815
|
-
const vectors = await callVoyageAPI(apiKey, batch);
|
|
815
|
+
const vectors = await callVoyageAPI(apiKey, batch, "document", dimensions);
|
|
816
816
|
results.push(...vectors);
|
|
817
817
|
onProgress?.(Math.min(i + batch.length, texts.length), texts.length);
|
|
818
818
|
}
|
|
819
819
|
return results;
|
|
820
820
|
},
|
|
821
821
|
async embedSingle(text) {
|
|
822
|
-
const vectors = await callVoyageAPI(apiKey, [text]);
|
|
822
|
+
const vectors = await callVoyageAPI(apiKey, [text], "query", dimensions);
|
|
823
823
|
return vectors[0];
|
|
824
824
|
}
|
|
825
825
|
};
|
|
826
826
|
}
|
|
827
|
-
async function callVoyageAPI(apiKey, texts) {
|
|
827
|
+
async function callVoyageAPI(apiKey, texts, inputType, dimensions) {
|
|
828
828
|
const response = await fetchWithRetry(VOYAGE_API_URL, {
|
|
829
829
|
method: "POST",
|
|
830
830
|
headers: {
|
|
@@ -834,7 +834,8 @@ async function callVoyageAPI(apiKey, texts) {
|
|
|
834
834
|
body: JSON.stringify({
|
|
835
835
|
model: VOYAGE_MODEL,
|
|
836
836
|
input: texts,
|
|
837
|
-
input_type:
|
|
837
|
+
input_type: inputType,
|
|
838
|
+
output_dimension: dimensions
|
|
838
839
|
})
|
|
839
840
|
});
|
|
840
841
|
const json = await response.json();
|
|
@@ -842,29 +843,29 @@ async function callVoyageAPI(apiKey, texts) {
|
|
|
842
843
|
}
|
|
843
844
|
var OPENAI_API_URL = "https://api.openai.com/v1/embeddings";
|
|
844
845
|
var OPENAI_MODEL = "text-embedding-3-large";
|
|
845
|
-
var
|
|
846
|
+
var OPENAI_DEFAULT_DIMENSIONS = 1024;
|
|
846
847
|
var OPENAI_BATCH_SIZE = 128;
|
|
847
|
-
function createOpenAIEmbedder(apiKey) {
|
|
848
|
+
function createOpenAIEmbedder(apiKey, dimensions = OPENAI_DEFAULT_DIMENSIONS) {
|
|
848
849
|
return {
|
|
849
850
|
name: OPENAI_MODEL,
|
|
850
|
-
dimensions
|
|
851
|
+
dimensions,
|
|
851
852
|
async embed(texts, onProgress) {
|
|
852
853
|
const results = [];
|
|
853
854
|
for (let i = 0; i < texts.length; i += OPENAI_BATCH_SIZE) {
|
|
854
855
|
const batch = texts.slice(i, i + OPENAI_BATCH_SIZE);
|
|
855
|
-
const vectors = await callOpenAIAPI(apiKey, batch);
|
|
856
|
+
const vectors = await callOpenAIAPI(apiKey, batch, dimensions);
|
|
856
857
|
results.push(...vectors);
|
|
857
858
|
onProgress?.(Math.min(i + batch.length, texts.length), texts.length);
|
|
858
859
|
}
|
|
859
860
|
return results;
|
|
860
861
|
},
|
|
861
862
|
async embedSingle(text) {
|
|
862
|
-
const vectors = await callOpenAIAPI(apiKey, [text]);
|
|
863
|
+
const vectors = await callOpenAIAPI(apiKey, [text], dimensions);
|
|
863
864
|
return vectors[0];
|
|
864
865
|
}
|
|
865
866
|
};
|
|
866
867
|
}
|
|
867
|
-
async function callOpenAIAPI(apiKey, texts) {
|
|
868
|
+
async function callOpenAIAPI(apiKey, texts, dimensions) {
|
|
868
869
|
const response = await fetchWithRetry(OPENAI_API_URL, {
|
|
869
870
|
method: "POST",
|
|
870
871
|
headers: {
|
|
@@ -874,7 +875,7 @@ async function callOpenAIAPI(apiKey, texts) {
|
|
|
874
875
|
body: JSON.stringify({
|
|
875
876
|
model: OPENAI_MODEL,
|
|
876
877
|
input: texts,
|
|
877
|
-
dimensions
|
|
878
|
+
dimensions
|
|
878
879
|
})
|
|
879
880
|
});
|
|
880
881
|
const json = await response.json();
|
|
@@ -1038,9 +1039,57 @@ function searchVectors(db, query, limit) {
|
|
|
1038
1039
|
}));
|
|
1039
1040
|
}
|
|
1040
1041
|
|
|
1042
|
+
// src/utils/errors.ts
|
|
1043
|
+
var ErrorCode = {
|
|
1044
|
+
NOT_INITIALIZED: "NOT_INITIALIZED",
|
|
1045
|
+
INDEX_FAILED: "INDEX_FAILED",
|
|
1046
|
+
PARSE_FAILED: "PARSE_FAILED",
|
|
1047
|
+
CHUNK_FAILED: "CHUNK_FAILED",
|
|
1048
|
+
EMBEDDER_FAILED: "EMBEDDER_FAILED",
|
|
1049
|
+
SEARCH_FAILED: "SEARCH_FAILED",
|
|
1050
|
+
CONFIG_INVALID: "CONFIG_INVALID",
|
|
1051
|
+
DB_CORRUPTED: "DB_CORRUPTED",
|
|
1052
|
+
DB_WRITE_FAILED: "DB_WRITE_FAILED",
|
|
1053
|
+
WATCHER_FAILED: "WATCHER_FAILED",
|
|
1054
|
+
LLM_FAILED: "LLM_FAILED"
|
|
1055
|
+
};
|
|
1056
|
+
var KontextError = class extends Error {
|
|
1057
|
+
code;
|
|
1058
|
+
constructor(message, code, cause) {
|
|
1059
|
+
super(message, { cause });
|
|
1060
|
+
this.name = "KontextError";
|
|
1061
|
+
this.code = code;
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
var IndexError = class extends KontextError {
|
|
1065
|
+
constructor(message, code, cause) {
|
|
1066
|
+
super(message, code, cause);
|
|
1067
|
+
this.name = "IndexError";
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
var SearchError = class extends KontextError {
|
|
1071
|
+
constructor(message, code, cause) {
|
|
1072
|
+
super(message, code, cause);
|
|
1073
|
+
this.name = "SearchError";
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
var ConfigError = class extends KontextError {
|
|
1077
|
+
constructor(message, code, cause) {
|
|
1078
|
+
super(message, code, cause);
|
|
1079
|
+
this.name = "ConfigError";
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
var DatabaseError = class extends KontextError {
|
|
1083
|
+
constructor(message, code, cause) {
|
|
1084
|
+
super(message, code, cause);
|
|
1085
|
+
this.name = "DatabaseError";
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1041
1089
|
// src/storage/db.ts
|
|
1042
1090
|
var DEFAULT_DIMENSIONS = 384;
|
|
1043
|
-
|
|
1091
|
+
var VECTOR_DIMENSIONS_META_KEY = "vector_dimensions";
|
|
1092
|
+
function createDatabase(dbPath, dimensions) {
|
|
1044
1093
|
const dir = path3.dirname(dbPath);
|
|
1045
1094
|
if (!fs4.existsSync(dir)) {
|
|
1046
1095
|
fs4.mkdirSync(dir, { recursive: true });
|
|
@@ -1049,7 +1098,8 @@ function createDatabase(dbPath, dimensions = DEFAULT_DIMENSIONS) {
|
|
|
1049
1098
|
db.pragma("journal_mode = WAL");
|
|
1050
1099
|
db.pragma("foreign_keys = ON");
|
|
1051
1100
|
sqliteVec.load(db);
|
|
1052
|
-
initializeSchema(db, dimensions);
|
|
1101
|
+
initializeSchema(db, dimensions ?? DEFAULT_DIMENSIONS);
|
|
1102
|
+
ensureVectorDimensions(db, dimensions);
|
|
1053
1103
|
const stmtUpsertFile = db.prepare(`
|
|
1054
1104
|
INSERT INTO files (path, language, hash, last_indexed, size)
|
|
1055
1105
|
VALUES (@path, @language, @hash, @lastIndexed, @size)
|
|
@@ -1058,6 +1108,7 @@ function createDatabase(dbPath, dimensions = DEFAULT_DIMENSIONS) {
|
|
|
1058
1108
|
hash = excluded.hash,
|
|
1059
1109
|
last_indexed = excluded.last_indexed,
|
|
1060
1110
|
size = excluded.size
|
|
1111
|
+
RETURNING id
|
|
1061
1112
|
`);
|
|
1062
1113
|
const stmtGetFile = db.prepare(
|
|
1063
1114
|
"SELECT id, path, language, hash, last_indexed as lastIndexed, size FROM files WHERE path = ?"
|
|
@@ -1101,18 +1152,20 @@ function createDatabase(dbPath, dimensions = DEFAULT_DIMENSIONS) {
|
|
|
1101
1152
|
);
|
|
1102
1153
|
return {
|
|
1103
1154
|
upsertFile(file) {
|
|
1104
|
-
const
|
|
1155
|
+
const row = stmtUpsertFile.get({
|
|
1105
1156
|
path: file.path,
|
|
1106
1157
|
language: file.language,
|
|
1107
1158
|
hash: file.hash,
|
|
1108
1159
|
lastIndexed: Date.now(),
|
|
1109
1160
|
size: file.size
|
|
1110
1161
|
});
|
|
1111
|
-
if (
|
|
1112
|
-
|
|
1162
|
+
if (!row?.id) {
|
|
1163
|
+
throw new DatabaseError(
|
|
1164
|
+
`Failed to upsert file: ${file.path}`,
|
|
1165
|
+
ErrorCode.DB_WRITE_FAILED
|
|
1166
|
+
);
|
|
1113
1167
|
}
|
|
1114
|
-
|
|
1115
|
-
return existing?.id ?? 0;
|
|
1168
|
+
return row.id;
|
|
1116
1169
|
},
|
|
1117
1170
|
getFile(filePath) {
|
|
1118
1171
|
const row = stmtGetFile.get(filePath);
|
|
@@ -1155,15 +1208,17 @@ function createDatabase(dbPath, dimensions = DEFAULT_DIMENSIONS) {
|
|
|
1155
1208
|
return row.lastIndexed;
|
|
1156
1209
|
},
|
|
1157
1210
|
deleteFile(filePath) {
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1211
|
+
db.transaction(() => {
|
|
1212
|
+
const file = stmtGetFile.get(filePath);
|
|
1213
|
+
if (file) {
|
|
1214
|
+
const chunkRows = stmtGetChunkIdsByFile.all(file.id);
|
|
1215
|
+
const chunkIds = chunkRows.map((r) => r.id);
|
|
1216
|
+
if (chunkIds.length > 0) {
|
|
1217
|
+
deleteVectorsByChunkIds(db, chunkIds);
|
|
1218
|
+
}
|
|
1164
1219
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1220
|
+
stmtDeleteFile.run(filePath);
|
|
1221
|
+
})();
|
|
1167
1222
|
},
|
|
1168
1223
|
insertChunks(fileId, chunks) {
|
|
1169
1224
|
const ids = [];
|
|
@@ -1258,12 +1313,14 @@ function createDatabase(dbPath, dimensions = DEFAULT_DIMENSIONS) {
|
|
|
1258
1313
|
}));
|
|
1259
1314
|
},
|
|
1260
1315
|
deleteChunksByFile(fileId) {
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1316
|
+
db.transaction(() => {
|
|
1317
|
+
const chunkRows = stmtGetChunkIdsByFile.all(fileId);
|
|
1318
|
+
const chunkIds = chunkRows.map((r) => r.id);
|
|
1319
|
+
if (chunkIds.length > 0) {
|
|
1320
|
+
deleteVectorsByChunkIds(db, chunkIds);
|
|
1321
|
+
}
|
|
1322
|
+
stmtDeleteChunksByFile.run(fileId);
|
|
1323
|
+
})();
|
|
1267
1324
|
},
|
|
1268
1325
|
insertDependency(sourceChunkId, targetChunkId, type) {
|
|
1269
1326
|
stmtInsertDep.run(sourceChunkId, targetChunkId, type);
|
|
@@ -1325,6 +1382,59 @@ function getMetaVersion(db) {
|
|
|
1325
1382
|
return 0;
|
|
1326
1383
|
}
|
|
1327
1384
|
}
|
|
1385
|
+
function ensureVectorDimensions(db, expectedDimensions) {
|
|
1386
|
+
const actual = getExistingVectorDimensions(db);
|
|
1387
|
+
const stored = db.prepare("SELECT value FROM meta WHERE key = ?").get(VECTOR_DIMENSIONS_META_KEY);
|
|
1388
|
+
const storedValue = stored?.value;
|
|
1389
|
+
const storedDimensions = storedValue ? Number.parseInt(storedValue, 10) : void 0;
|
|
1390
|
+
if (storedDimensions !== void 0 && (!Number.isInteger(storedDimensions) || storedDimensions <= 0)) {
|
|
1391
|
+
throw new DatabaseError(
|
|
1392
|
+
`Invalid stored vector dimensions metadata: ${storedValue ?? "unknown"}`,
|
|
1393
|
+
ErrorCode.DB_CORRUPTED
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
if (actual !== null && storedDimensions !== void 0 && storedDimensions !== actual) {
|
|
1397
|
+
throw new DatabaseError(
|
|
1398
|
+
`Vector dimensions metadata mismatch: meta=${storedDimensions}, table=${actual}.`,
|
|
1399
|
+
ErrorCode.DB_CORRUPTED
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
if (expectedDimensions === void 0) {
|
|
1403
|
+
if (storedDimensions !== void 0) return;
|
|
1404
|
+
const dimensions = actual ?? DEFAULT_DIMENSIONS;
|
|
1405
|
+
db.prepare(
|
|
1406
|
+
"INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)"
|
|
1407
|
+
).run(VECTOR_DIMENSIONS_META_KEY, String(dimensions));
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
if (!stored) {
|
|
1411
|
+
const dimensions = actual ?? expectedDimensions;
|
|
1412
|
+
db.prepare(
|
|
1413
|
+
"INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)"
|
|
1414
|
+
).run(VECTOR_DIMENSIONS_META_KEY, String(dimensions));
|
|
1415
|
+
if (actual !== null && actual !== expectedDimensions) {
|
|
1416
|
+
throw new DatabaseError(
|
|
1417
|
+
`Vector dimension mismatch: index uses ${actual} dims, but config requests ${expectedDimensions} dims. Rebuild the index.`,
|
|
1418
|
+
ErrorCode.CONFIG_INVALID
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
if (storedDimensions !== expectedDimensions) {
|
|
1424
|
+
throw new DatabaseError(
|
|
1425
|
+
`Vector dimension mismatch: index uses ${storedDimensions} dims, but config requests ${expectedDimensions} dims. Rebuild the index.`,
|
|
1426
|
+
ErrorCode.CONFIG_INVALID
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
function getExistingVectorDimensions(db) {
|
|
1431
|
+
const row = db.prepare("SELECT sql FROM sqlite_master WHERE name = 'chunk_vectors'").get();
|
|
1432
|
+
const sql = row?.sql;
|
|
1433
|
+
if (!sql) return null;
|
|
1434
|
+
const match = sql.match(/embedding\s+float\[(\d+)\]/i);
|
|
1435
|
+
if (!match) return null;
|
|
1436
|
+
return Number.parseInt(match[1], 10);
|
|
1437
|
+
}
|
|
1328
1438
|
|
|
1329
1439
|
// src/search/vector.ts
|
|
1330
1440
|
function distanceToScore(distance) {
|
|
@@ -1365,7 +1475,15 @@ async function vectorSearch(db, embedder, query, limit, filters) {
|
|
|
1365
1475
|
|
|
1366
1476
|
// src/search/fts.ts
|
|
1367
1477
|
function sanitizeFtsQuery(query) {
|
|
1368
|
-
|
|
1478
|
+
const tokenized = query.replace(/[^A-Za-z0-9_*]+/g, " ").trim();
|
|
1479
|
+
if (tokenized.length === 0) return "";
|
|
1480
|
+
const sanitizedTerms = tokenized.split(/\s+/).map((term) => {
|
|
1481
|
+
const hasTrailingWildcard = /\*+$/.test(term);
|
|
1482
|
+
const base = term.replace(/\*/g, "");
|
|
1483
|
+
if (base.length === 0) return "";
|
|
1484
|
+
return hasTrailingWildcard ? `${base}*` : base;
|
|
1485
|
+
}).filter((term) => term.length > 0);
|
|
1486
|
+
return sanitizedTerms.join(" ");
|
|
1369
1487
|
}
|
|
1370
1488
|
function bm25ToScore(rank) {
|
|
1371
1489
|
return 1 / (1 + Math.abs(rank));
|
|
@@ -1510,27 +1628,56 @@ function pathKeywordSearch(db, query, limit) {
|
|
|
1510
1628
|
}
|
|
1511
1629
|
if (scoredPaths.length === 0) return [];
|
|
1512
1630
|
scoredPaths.sort((a, b) => b.score - a.score);
|
|
1513
|
-
const
|
|
1631
|
+
const matchedFiles = [];
|
|
1514
1632
|
for (const { filePath, score } of scoredPaths) {
|
|
1515
|
-
if (results.length >= limit) break;
|
|
1516
1633
|
const file = db.getFile(filePath);
|
|
1517
1634
|
if (!file) continue;
|
|
1518
1635
|
const chunks = db.getChunksByFile(file.id);
|
|
1519
|
-
|
|
1636
|
+
if (chunks.length === 0) continue;
|
|
1637
|
+
matchedFiles.push({
|
|
1638
|
+
filePath: file.path,
|
|
1639
|
+
language: file.language,
|
|
1640
|
+
score,
|
|
1641
|
+
chunks
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
if (matchedFiles.length === 0) return [];
|
|
1645
|
+
const results = [];
|
|
1646
|
+
const pushChunk = (filePath, language, score, chunk) => {
|
|
1647
|
+
results.push({
|
|
1648
|
+
chunkId: chunk.id,
|
|
1649
|
+
filePath,
|
|
1650
|
+
lineStart: chunk.lineStart,
|
|
1651
|
+
lineEnd: chunk.lineEnd,
|
|
1652
|
+
name: chunk.name,
|
|
1653
|
+
type: chunk.type,
|
|
1654
|
+
exported: chunk.exports,
|
|
1655
|
+
text: chunk.text,
|
|
1656
|
+
score,
|
|
1657
|
+
language
|
|
1658
|
+
});
|
|
1659
|
+
};
|
|
1660
|
+
for (const matched of matchedFiles) {
|
|
1661
|
+
if (results.length >= limit) break;
|
|
1662
|
+
pushChunk(
|
|
1663
|
+
matched.filePath,
|
|
1664
|
+
matched.language,
|
|
1665
|
+
matched.score,
|
|
1666
|
+
matched.chunks[0]
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
let offset = 1;
|
|
1670
|
+
while (results.length < limit) {
|
|
1671
|
+
let addedInRound = false;
|
|
1672
|
+
for (const matched of matchedFiles) {
|
|
1520
1673
|
if (results.length >= limit) break;
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
lineEnd: chunk.lineEnd,
|
|
1526
|
-
name: chunk.name,
|
|
1527
|
-
type: chunk.type,
|
|
1528
|
-
exported: chunk.exports,
|
|
1529
|
-
text: chunk.text,
|
|
1530
|
-
score,
|
|
1531
|
-
language: file.language
|
|
1532
|
-
});
|
|
1674
|
+
const chunk = matched.chunks[offset];
|
|
1675
|
+
if (!chunk) continue;
|
|
1676
|
+
pushChunk(matched.filePath, matched.language, matched.score, chunk);
|
|
1677
|
+
addedInRound = true;
|
|
1533
1678
|
}
|
|
1679
|
+
if (!addedInRound) break;
|
|
1680
|
+
offset++;
|
|
1534
1681
|
}
|
|
1535
1682
|
return results;
|
|
1536
1683
|
}
|
|
@@ -1891,8 +2038,91 @@ Related locations:
|
|
|
1891
2038
|
|
|
1892
2039
|
The middleware extracts the Bearer token from the Authorization header before passing it to validateToken.`;
|
|
1893
2040
|
|
|
1894
|
-
// src/steering/
|
|
2041
|
+
// src/steering/classify.ts
|
|
2042
|
+
var SYMBOL_CAMEL_RE = /^[a-z][a-zA-Z0-9]*$/;
|
|
2043
|
+
var SYMBOL_PASCAL_RE = /^[A-Z][a-zA-Z0-9]*$/;
|
|
2044
|
+
var SYMBOL_SNAKE_RE = /^[a-z]+(?:_[a-z]+)+$/;
|
|
2045
|
+
var SYMBOL_UPPER_RE = /^[A-Z]+(?:_[A-Z]+)*$/;
|
|
2046
|
+
var PATH_EXTENSION_RE = /\.(?:ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|swift|rb|php|cs|cpp|c|h|hpp|json|yaml|yml|toml|md|sql|sh|bash)$/i;
|
|
2047
|
+
var QUESTION_WORDS = /* @__PURE__ */ new Set([
|
|
2048
|
+
"how",
|
|
2049
|
+
"what",
|
|
2050
|
+
"where",
|
|
2051
|
+
"why",
|
|
2052
|
+
"when",
|
|
2053
|
+
"which",
|
|
2054
|
+
"show",
|
|
2055
|
+
"explain",
|
|
2056
|
+
"find",
|
|
2057
|
+
"list"
|
|
2058
|
+
]);
|
|
1895
2059
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
2060
|
+
"the",
|
|
2061
|
+
"a",
|
|
2062
|
+
"an",
|
|
2063
|
+
"is",
|
|
2064
|
+
"are",
|
|
2065
|
+
"was",
|
|
2066
|
+
"were",
|
|
2067
|
+
"do",
|
|
2068
|
+
"does",
|
|
2069
|
+
"did",
|
|
2070
|
+
"to",
|
|
2071
|
+
"for",
|
|
2072
|
+
"of",
|
|
2073
|
+
"in",
|
|
2074
|
+
"on",
|
|
2075
|
+
"with",
|
|
2076
|
+
"by",
|
|
2077
|
+
"and",
|
|
2078
|
+
"or"
|
|
2079
|
+
]);
|
|
2080
|
+
function defaultMultipliers() {
|
|
2081
|
+
return {
|
|
2082
|
+
vector: 1,
|
|
2083
|
+
fts: 1,
|
|
2084
|
+
ast: 1,
|
|
2085
|
+
path: 1,
|
|
2086
|
+
dependency: 1
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
function isSymbolQuery(query) {
|
|
2090
|
+
return SYMBOL_CAMEL_RE.test(query) || SYMBOL_PASCAL_RE.test(query) || SYMBOL_SNAKE_RE.test(query) || SYMBOL_UPPER_RE.test(query);
|
|
2091
|
+
}
|
|
2092
|
+
function isPathQuery(query) {
|
|
2093
|
+
return query.includes("/") || PATH_EXTENSION_RE.test(query);
|
|
2094
|
+
}
|
|
2095
|
+
function isNaturalLanguageQuery(query) {
|
|
2096
|
+
const lower = query.toLowerCase();
|
|
2097
|
+
const words = lower.split(/\s+/).filter((w) => w.length > 0);
|
|
2098
|
+
const hasQuestionWord = words.some((w) => QUESTION_WORDS.has(w));
|
|
2099
|
+
const hasStopWord = words.some((w) => STOP_WORDS.has(w));
|
|
2100
|
+
return hasQuestionWord || words.length >= 4 && hasStopWord;
|
|
2101
|
+
}
|
|
2102
|
+
function classifyQuery(query) {
|
|
2103
|
+
const trimmed = query.trim();
|
|
2104
|
+
const multipliers = defaultMultipliers();
|
|
2105
|
+
if (isPathQuery(trimmed)) {
|
|
2106
|
+
multipliers.path = 2;
|
|
2107
|
+
multipliers.ast = 0.5;
|
|
2108
|
+
return { kind: "path", multipliers };
|
|
2109
|
+
}
|
|
2110
|
+
if (isSymbolQuery(trimmed)) {
|
|
2111
|
+
multipliers.ast = 1.5;
|
|
2112
|
+
multipliers.vector = 0.5;
|
|
2113
|
+
return { kind: "symbol", multipliers };
|
|
2114
|
+
}
|
|
2115
|
+
if (isNaturalLanguageQuery(trimmed)) {
|
|
2116
|
+
multipliers.vector = 1.5;
|
|
2117
|
+
multipliers.path = 1.2;
|
|
2118
|
+
multipliers.ast = 0.7;
|
|
2119
|
+
return { kind: "natural_language", multipliers };
|
|
2120
|
+
}
|
|
2121
|
+
return { kind: "keyword", multipliers };
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// src/steering/llm.ts
|
|
2125
|
+
var STOP_WORDS2 = /* @__PURE__ */ new Set([
|
|
1896
2126
|
// Interrogatives & conjunctions
|
|
1897
2127
|
"how",
|
|
1898
2128
|
"does",
|
|
@@ -2003,6 +2233,83 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
2003
2233
|
]);
|
|
2004
2234
|
var CODE_IDENT_RE = /^(?:[a-z]+(?:[A-Z][a-z]*)+|[A-Z][a-zA-Z]+|[a-z]+(?:_[a-z]+)+|[A-Z]+(?:_[A-Z]+)+)$/;
|
|
2005
2235
|
var DOTTED_IDENT_RE = /[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)+/g;
|
|
2236
|
+
var COMMON_STEMS = {
|
|
2237
|
+
authentication: "auth",
|
|
2238
|
+
authorization: "auth",
|
|
2239
|
+
configuration: "config",
|
|
2240
|
+
initialization: "init",
|
|
2241
|
+
initialize: "init",
|
|
2242
|
+
initializing: "init",
|
|
2243
|
+
implementation: "impl",
|
|
2244
|
+
implements: "impl",
|
|
2245
|
+
implementing: "impl",
|
|
2246
|
+
dependency: "dep",
|
|
2247
|
+
dependencies: "dep",
|
|
2248
|
+
middleware: "middleware",
|
|
2249
|
+
validation: "valid",
|
|
2250
|
+
validator: "valid",
|
|
2251
|
+
serialize: "serial",
|
|
2252
|
+
serialization: "serial",
|
|
2253
|
+
deserialize: "deserial",
|
|
2254
|
+
database: "db",
|
|
2255
|
+
logging: "log",
|
|
2256
|
+
logger: "log",
|
|
2257
|
+
testing: "test",
|
|
2258
|
+
handler: "handle",
|
|
2259
|
+
handling: "handle",
|
|
2260
|
+
callback: "callback",
|
|
2261
|
+
subscriber: "subscribe",
|
|
2262
|
+
subscription: "subscribe",
|
|
2263
|
+
rendering: "render",
|
|
2264
|
+
renderer: "render",
|
|
2265
|
+
transformer: "transform",
|
|
2266
|
+
transformation: "transform",
|
|
2267
|
+
connection: "connect",
|
|
2268
|
+
connecting: "connect",
|
|
2269
|
+
connector: "connect",
|
|
2270
|
+
migrating: "migrate",
|
|
2271
|
+
migration: "migrate",
|
|
2272
|
+
scheduling: "schedule",
|
|
2273
|
+
scheduler: "schedule",
|
|
2274
|
+
parsing: "parse",
|
|
2275
|
+
parser: "parse",
|
|
2276
|
+
routing: "route",
|
|
2277
|
+
router: "route",
|
|
2278
|
+
indexing: "index",
|
|
2279
|
+
indexer: "index",
|
|
2280
|
+
subscribing: "subscribe"
|
|
2281
|
+
};
|
|
2282
|
+
var STEM_SUFFIXES = [
|
|
2283
|
+
"tion",
|
|
2284
|
+
"sion",
|
|
2285
|
+
"ment",
|
|
2286
|
+
"ness",
|
|
2287
|
+
"ing",
|
|
2288
|
+
"er",
|
|
2289
|
+
"or",
|
|
2290
|
+
"able",
|
|
2291
|
+
"ible",
|
|
2292
|
+
"ity",
|
|
2293
|
+
"ous",
|
|
2294
|
+
"ive",
|
|
2295
|
+
"ful",
|
|
2296
|
+
"less",
|
|
2297
|
+
"ly"
|
|
2298
|
+
];
|
|
2299
|
+
function getStemVariant(term) {
|
|
2300
|
+
const lower = term.toLowerCase();
|
|
2301
|
+
const mapped = COMMON_STEMS[lower];
|
|
2302
|
+
if (mapped && mapped !== lower) return mapped;
|
|
2303
|
+
if (!/^[a-z][a-z0-9_]*$/.test(lower)) return null;
|
|
2304
|
+
for (const suffix of STEM_SUFFIXES) {
|
|
2305
|
+
if (!lower.endsWith(suffix)) continue;
|
|
2306
|
+
const stem = lower.slice(0, -suffix.length);
|
|
2307
|
+
if (stem.length >= 4 && stem !== lower) {
|
|
2308
|
+
return stem;
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
return null;
|
|
2312
|
+
}
|
|
2006
2313
|
function extractSearchTerms(query) {
|
|
2007
2314
|
const terms = [];
|
|
2008
2315
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2013,16 +2320,23 @@ function extractSearchTerms(query) {
|
|
|
2013
2320
|
terms.push(term);
|
|
2014
2321
|
}
|
|
2015
2322
|
};
|
|
2323
|
+
const addTermAndVariants = (term) => {
|
|
2324
|
+
addUnique(term);
|
|
2325
|
+
const variant = getStemVariant(term);
|
|
2326
|
+
if (variant && variant !== term.toLowerCase()) {
|
|
2327
|
+
addUnique(variant);
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2016
2330
|
const dottedMatches = query.match(DOTTED_IDENT_RE) ?? [];
|
|
2017
|
-
for (const m of dottedMatches)
|
|
2331
|
+
for (const m of dottedMatches) addTermAndVariants(m);
|
|
2018
2332
|
const pathTokens = query.split(/\s+/).filter((t) => t.includes("/"));
|
|
2019
|
-
for (const p of pathTokens)
|
|
2333
|
+
for (const p of pathTokens) addTermAndVariants(p.replace(/[?!,;]+$/g, ""));
|
|
2020
2334
|
const words = query.replace(/[^a-zA-Z0-9_.\s/-]/g, " ").split(/\s+/).filter((w) => w.length >= 2);
|
|
2021
2335
|
for (const w of words) {
|
|
2022
2336
|
const lower = w.toLowerCase();
|
|
2023
2337
|
if (seen.has(lower)) continue;
|
|
2024
|
-
if (
|
|
2025
|
-
|
|
2338
|
+
if (STOP_WORDS2.has(lower) && !CODE_IDENT_RE.test(w)) continue;
|
|
2339
|
+
addTermAndVariants(w);
|
|
2026
2340
|
}
|
|
2027
2341
|
if (terms.length === 0) {
|
|
2028
2342
|
const allWords = query.replace(/[^a-zA-Z0-9_\s]/g, " ").split(/\s+/).filter((w) => w.length >= 2);
|
|
@@ -2039,17 +2353,42 @@ var VALID_STRATEGIES = /* @__PURE__ */ new Set([
|
|
|
2039
2353
|
"dependency"
|
|
2040
2354
|
]);
|
|
2041
2355
|
function buildFallbackPlan(query) {
|
|
2042
|
-
const
|
|
2043
|
-
const strategies = [
|
|
2044
|
-
{ strategy: "fts", query: keywords, weight: 0.8, reason: "Full-text keyword search" },
|
|
2045
|
-
{ strategy: "ast", query: keywords, weight: 0.9, reason: "Structural symbol search" },
|
|
2046
|
-
{ strategy: "path", query: keywords, weight: 0.7, reason: "Path keyword search" }
|
|
2047
|
-
];
|
|
2356
|
+
const strategies = buildFallbackStrategies(query);
|
|
2048
2357
|
return {
|
|
2049
2358
|
interpretation: `Searching for: ${query}`,
|
|
2050
2359
|
strategies
|
|
2051
2360
|
};
|
|
2052
2361
|
}
|
|
2362
|
+
function buildFallbackStrategies(query) {
|
|
2363
|
+
const keywords = extractSearchTerms(query);
|
|
2364
|
+
const { multipliers } = classifyQuery(query);
|
|
2365
|
+
return [
|
|
2366
|
+
{
|
|
2367
|
+
strategy: "vector",
|
|
2368
|
+
query,
|
|
2369
|
+
weight: 1 * multipliers.vector,
|
|
2370
|
+
reason: "Semantic search over natural language intent"
|
|
2371
|
+
},
|
|
2372
|
+
{
|
|
2373
|
+
strategy: "fts",
|
|
2374
|
+
query: keywords,
|
|
2375
|
+
weight: 0.8 * multipliers.fts,
|
|
2376
|
+
reason: "Full-text keyword search"
|
|
2377
|
+
},
|
|
2378
|
+
{
|
|
2379
|
+
strategy: "ast",
|
|
2380
|
+
query: keywords,
|
|
2381
|
+
weight: 0.9 * multipliers.ast,
|
|
2382
|
+
reason: "Structural symbol search"
|
|
2383
|
+
},
|
|
2384
|
+
{
|
|
2385
|
+
strategy: "path",
|
|
2386
|
+
query: keywords,
|
|
2387
|
+
weight: 0.7 * multipliers.path,
|
|
2388
|
+
reason: "Path keyword search"
|
|
2389
|
+
}
|
|
2390
|
+
];
|
|
2391
|
+
}
|
|
2053
2392
|
function parseSearchPlan(raw, query) {
|
|
2054
2393
|
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
|
2055
2394
|
if (!jsonMatch) return buildFallbackPlan(query);
|
|
@@ -2129,55 +2468,8 @@ async function steer(provider, query, limit, searchExecutor) {
|
|
|
2129
2468
|
}
|
|
2130
2469
|
|
|
2131
2470
|
// src/cli/commands/init.ts
|
|
2132
|
-
import
|
|
2133
|
-
import
|
|
2134
|
-
|
|
2135
|
-
// src/utils/errors.ts
|
|
2136
|
-
var ErrorCode = {
|
|
2137
|
-
NOT_INITIALIZED: "NOT_INITIALIZED",
|
|
2138
|
-
INDEX_FAILED: "INDEX_FAILED",
|
|
2139
|
-
PARSE_FAILED: "PARSE_FAILED",
|
|
2140
|
-
CHUNK_FAILED: "CHUNK_FAILED",
|
|
2141
|
-
EMBEDDER_FAILED: "EMBEDDER_FAILED",
|
|
2142
|
-
SEARCH_FAILED: "SEARCH_FAILED",
|
|
2143
|
-
CONFIG_INVALID: "CONFIG_INVALID",
|
|
2144
|
-
DB_CORRUPTED: "DB_CORRUPTED",
|
|
2145
|
-
DB_WRITE_FAILED: "DB_WRITE_FAILED",
|
|
2146
|
-
WATCHER_FAILED: "WATCHER_FAILED",
|
|
2147
|
-
LLM_FAILED: "LLM_FAILED"
|
|
2148
|
-
};
|
|
2149
|
-
var KontextError = class extends Error {
|
|
2150
|
-
code;
|
|
2151
|
-
constructor(message, code, cause) {
|
|
2152
|
-
super(message, { cause });
|
|
2153
|
-
this.name = "KontextError";
|
|
2154
|
-
this.code = code;
|
|
2155
|
-
}
|
|
2156
|
-
};
|
|
2157
|
-
var IndexError = class extends KontextError {
|
|
2158
|
-
constructor(message, code, cause) {
|
|
2159
|
-
super(message, code, cause);
|
|
2160
|
-
this.name = "IndexError";
|
|
2161
|
-
}
|
|
2162
|
-
};
|
|
2163
|
-
var SearchError = class extends KontextError {
|
|
2164
|
-
constructor(message, code, cause) {
|
|
2165
|
-
super(message, code, cause);
|
|
2166
|
-
this.name = "SearchError";
|
|
2167
|
-
}
|
|
2168
|
-
};
|
|
2169
|
-
var ConfigError = class extends KontextError {
|
|
2170
|
-
constructor(message, code, cause) {
|
|
2171
|
-
super(message, code, cause);
|
|
2172
|
-
this.name = "ConfigError";
|
|
2173
|
-
}
|
|
2174
|
-
};
|
|
2175
|
-
var DatabaseError = class extends KontextError {
|
|
2176
|
-
constructor(message, code, cause) {
|
|
2177
|
-
super(message, code, cause);
|
|
2178
|
-
this.name = "DatabaseError";
|
|
2179
|
-
}
|
|
2180
|
-
};
|
|
2471
|
+
import fs6 from "fs";
|
|
2472
|
+
import path5 from "path";
|
|
2181
2473
|
|
|
2182
2474
|
// src/utils/logger.ts
|
|
2183
2475
|
var LogLevel = {
|
|
@@ -2218,33 +2510,180 @@ function createLogger(options) {
|
|
|
2218
2510
|
};
|
|
2219
2511
|
}
|
|
2220
2512
|
|
|
2221
|
-
// src/cli/commands/
|
|
2513
|
+
// src/cli/commands/config.ts
|
|
2514
|
+
import fs5 from "fs";
|
|
2515
|
+
import path4 from "path";
|
|
2222
2516
|
var CTX_DIR = ".ctx";
|
|
2223
|
-
var DB_FILENAME = "index.db";
|
|
2224
2517
|
var CONFIG_FILENAME = "config.json";
|
|
2518
|
+
var DEFAULT_CONFIG = {
|
|
2519
|
+
embedder: {
|
|
2520
|
+
provider: "local",
|
|
2521
|
+
model: "Xenova/all-MiniLM-L6-v2",
|
|
2522
|
+
dimensions: 384
|
|
2523
|
+
},
|
|
2524
|
+
search: {
|
|
2525
|
+
defaultLimit: 10,
|
|
2526
|
+
strategies: ["vector", "fts", "ast", "path"],
|
|
2527
|
+
weights: { vector: 1, fts: 0.8, ast: 0.9, path: 0.7, dependency: 0.6 }
|
|
2528
|
+
},
|
|
2529
|
+
watch: {
|
|
2530
|
+
debounceMs: 500,
|
|
2531
|
+
ignored: []
|
|
2532
|
+
},
|
|
2533
|
+
llm: {
|
|
2534
|
+
provider: null,
|
|
2535
|
+
model: null
|
|
2536
|
+
}
|
|
2537
|
+
};
|
|
2538
|
+
var VALID_EMBEDDER_PROVIDERS = /* @__PURE__ */ new Set(["local", "voyage", "openai"]);
|
|
2539
|
+
var VALID_LLM_PROVIDERS = /* @__PURE__ */ new Set(["gemini", "openai", "anthropic"]);
|
|
2540
|
+
var VALIDATION_RULES = {
|
|
2541
|
+
"embedder.provider": {
|
|
2542
|
+
validate: (v) => typeof v === "string" && VALID_EMBEDDER_PROVIDERS.has(v),
|
|
2543
|
+
message: `Must be one of: ${[...VALID_EMBEDDER_PROVIDERS].join(", ")}`
|
|
2544
|
+
},
|
|
2545
|
+
"embedder.dimensions": {
|
|
2546
|
+
validate: (v) => typeof v === "number" && v > 0 && Number.isInteger(v),
|
|
2547
|
+
message: "Must be a positive integer"
|
|
2548
|
+
},
|
|
2549
|
+
"search.defaultLimit": {
|
|
2550
|
+
validate: (v) => typeof v === "number" && v > 0 && Number.isInteger(v),
|
|
2551
|
+
message: "Must be a positive integer"
|
|
2552
|
+
},
|
|
2553
|
+
"watch.debounceMs": {
|
|
2554
|
+
validate: (v) => typeof v === "number" && v >= 0 && Number.isInteger(v),
|
|
2555
|
+
message: "Must be a non-negative integer"
|
|
2556
|
+
},
|
|
2557
|
+
"llm.provider": {
|
|
2558
|
+
validate: (v) => v === null || typeof v === "string" && VALID_LLM_PROVIDERS.has(v),
|
|
2559
|
+
message: `Must be null or one of: ${[...VALID_LLM_PROVIDERS].join(", ")}`
|
|
2560
|
+
}
|
|
2561
|
+
};
|
|
2562
|
+
function resolveCtxDir(projectPath) {
|
|
2563
|
+
const absoluteRoot = path4.resolve(projectPath);
|
|
2564
|
+
const ctxDir = path4.join(absoluteRoot, CTX_DIR);
|
|
2565
|
+
if (!fs5.existsSync(ctxDir)) {
|
|
2566
|
+
throw new ConfigError(
|
|
2567
|
+
`Project not initialized. Run "ctx init" first. (${CTX_DIR}/ not found)`,
|
|
2568
|
+
ErrorCode.NOT_INITIALIZED
|
|
2569
|
+
);
|
|
2570
|
+
}
|
|
2571
|
+
return ctxDir;
|
|
2572
|
+
}
|
|
2573
|
+
function configPath(ctxDir) {
|
|
2574
|
+
return path4.join(ctxDir, CONFIG_FILENAME);
|
|
2575
|
+
}
|
|
2576
|
+
function readConfig(ctxDir) {
|
|
2577
|
+
const filePath = configPath(ctxDir);
|
|
2578
|
+
if (!fs5.existsSync(filePath)) {
|
|
2579
|
+
writeConfig(ctxDir, DEFAULT_CONFIG);
|
|
2580
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
2581
|
+
}
|
|
2582
|
+
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
2583
|
+
const parsed = JSON.parse(raw);
|
|
2584
|
+
return mergeWithDefaults(parsed);
|
|
2585
|
+
}
|
|
2586
|
+
function writeConfig(ctxDir, config) {
|
|
2587
|
+
fs5.writeFileSync(
|
|
2588
|
+
configPath(ctxDir),
|
|
2589
|
+
JSON.stringify(config, null, 2) + "\n"
|
|
2590
|
+
);
|
|
2591
|
+
}
|
|
2592
|
+
function mergeWithDefaults(partial) {
|
|
2593
|
+
return {
|
|
2594
|
+
embedder: { ...DEFAULT_CONFIG.embedder, ...partial.embedder },
|
|
2595
|
+
search: {
|
|
2596
|
+
...DEFAULT_CONFIG.search,
|
|
2597
|
+
...partial.search,
|
|
2598
|
+
weights: { ...DEFAULT_CONFIG.search.weights, ...partial.search?.weights }
|
|
2599
|
+
},
|
|
2600
|
+
watch: { ...DEFAULT_CONFIG.watch, ...partial.watch },
|
|
2601
|
+
llm: { ...DEFAULT_CONFIG.llm, ...partial.llm }
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
function runConfigShow(projectPath) {
|
|
2605
|
+
const ctxDir = resolveCtxDir(projectPath);
|
|
2606
|
+
const config = readConfig(ctxDir);
|
|
2607
|
+
return {
|
|
2608
|
+
config,
|
|
2609
|
+
text: JSON.stringify(config, null, 2)
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
// src/cli/embedder.ts
|
|
2614
|
+
function getProjectEmbedderConfig(projectPath) {
|
|
2615
|
+
const { config } = runConfigShow(projectPath);
|
|
2616
|
+
return config.embedder;
|
|
2617
|
+
}
|
|
2618
|
+
async function createProjectEmbedder(projectPath) {
|
|
2619
|
+
const config = getProjectEmbedderConfig(projectPath);
|
|
2620
|
+
validateProjectEmbedderConfig(config);
|
|
2621
|
+
switch (config.provider) {
|
|
2622
|
+
case "local":
|
|
2623
|
+
return await createLocalEmbedder();
|
|
2624
|
+
case "voyage": {
|
|
2625
|
+
const apiKey = requireApiKey("CTX_VOYAGE_KEY", "voyage");
|
|
2626
|
+
return createVoyageEmbedder(apiKey, config.dimensions);
|
|
2627
|
+
}
|
|
2628
|
+
case "openai": {
|
|
2629
|
+
const apiKey = requireApiKey("CTX_OPENAI_KEY", "openai");
|
|
2630
|
+
return createOpenAIEmbedder(apiKey, config.dimensions);
|
|
2631
|
+
}
|
|
2632
|
+
default:
|
|
2633
|
+
throw new ConfigError(
|
|
2634
|
+
`Unsupported embedder provider "${config.provider}". Use local, voyage, or openai.`,
|
|
2635
|
+
ErrorCode.CONFIG_INVALID
|
|
2636
|
+
);
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
function requireApiKey(envVar, provider) {
|
|
2640
|
+
const value = process.env[envVar];
|
|
2641
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
2642
|
+
throw new ConfigError(
|
|
2643
|
+
`Embedder provider "${provider}" requires ${envVar}. Export ${envVar} before running this command.`,
|
|
2644
|
+
ErrorCode.CONFIG_INVALID
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
function validateProjectEmbedderConfig(config) {
|
|
2648
|
+
if (!Number.isInteger(config.dimensions) || config.dimensions <= 0) {
|
|
2649
|
+
throw new ConfigError(
|
|
2650
|
+
`Invalid embedder.dimensions (${String(config.dimensions)}). Must be a positive integer.`,
|
|
2651
|
+
ErrorCode.CONFIG_INVALID
|
|
2652
|
+
);
|
|
2653
|
+
}
|
|
2654
|
+
if (config.provider === "local" && config.dimensions !== 384) {
|
|
2655
|
+
throw new ConfigError(
|
|
2656
|
+
'Local embedder requires "embedder.dimensions" = 384. Update config or switch provider.',
|
|
2657
|
+
ErrorCode.CONFIG_INVALID
|
|
2658
|
+
);
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
// src/cli/commands/init.ts
|
|
2663
|
+
var CTX_DIR2 = ".ctx";
|
|
2664
|
+
var DB_FILENAME = "index.db";
|
|
2665
|
+
var CONFIG_FILENAME2 = "config.json";
|
|
2225
2666
|
var GITIGNORE_ENTRY = ".ctx/";
|
|
2226
2667
|
function ensureGitignore(projectRoot) {
|
|
2227
|
-
const gitignorePath =
|
|
2228
|
-
if (
|
|
2229
|
-
const content =
|
|
2668
|
+
const gitignorePath = path5.join(projectRoot, ".gitignore");
|
|
2669
|
+
if (fs6.existsSync(gitignorePath)) {
|
|
2670
|
+
const content = fs6.readFileSync(gitignorePath, "utf-8");
|
|
2230
2671
|
if (content.includes(GITIGNORE_ENTRY)) return;
|
|
2231
2672
|
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
2232
|
-
|
|
2673
|
+
fs6.writeFileSync(gitignorePath, `${content}${suffix}${GITIGNORE_ENTRY}
|
|
2233
2674
|
`);
|
|
2234
2675
|
} else {
|
|
2235
|
-
|
|
2676
|
+
fs6.writeFileSync(gitignorePath, `${GITIGNORE_ENTRY}
|
|
2236
2677
|
`);
|
|
2237
2678
|
}
|
|
2238
2679
|
}
|
|
2239
2680
|
function ensureConfig(ctxDir) {
|
|
2240
|
-
const
|
|
2241
|
-
if (
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
};
|
|
2247
|
-
fs5.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
2681
|
+
const configPath2 = path5.join(ctxDir, CONFIG_FILENAME2);
|
|
2682
|
+
if (fs6.existsSync(configPath2)) return;
|
|
2683
|
+
fs6.writeFileSync(
|
|
2684
|
+
configPath2,
|
|
2685
|
+
JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n"
|
|
2686
|
+
);
|
|
2248
2687
|
}
|
|
2249
2688
|
function formatDuration(ms) {
|
|
2250
2689
|
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
@@ -2261,15 +2700,16 @@ function formatLanguageSummary(counts) {
|
|
|
2261
2700
|
}
|
|
2262
2701
|
async function runInit(projectPath, options = {}) {
|
|
2263
2702
|
const log = options.log ?? console.log;
|
|
2264
|
-
const absoluteRoot =
|
|
2703
|
+
const absoluteRoot = path5.resolve(projectPath);
|
|
2265
2704
|
const start = performance.now();
|
|
2266
2705
|
log(`Indexing ${absoluteRoot}...`);
|
|
2267
|
-
const ctxDir =
|
|
2268
|
-
if (!
|
|
2706
|
+
const ctxDir = path5.join(absoluteRoot, CTX_DIR2);
|
|
2707
|
+
if (!fs6.existsSync(ctxDir)) fs6.mkdirSync(ctxDir, { recursive: true });
|
|
2269
2708
|
ensureGitignore(absoluteRoot);
|
|
2270
2709
|
ensureConfig(ctxDir);
|
|
2271
|
-
const
|
|
2272
|
-
const
|
|
2710
|
+
const embedderConfig = getProjectEmbedderConfig(absoluteRoot);
|
|
2711
|
+
const dbPath = path5.join(ctxDir, DB_FILENAME);
|
|
2712
|
+
const db = createDatabase(dbPath, embedderConfig.dimensions);
|
|
2273
2713
|
try {
|
|
2274
2714
|
const discovered = await discoverFiles({
|
|
2275
2715
|
root: absoluteRoot,
|
|
@@ -2357,7 +2797,7 @@ async function runInit(projectPath, options = {}) {
|
|
|
2357
2797
|
log(` ${allChunksWithMeta.length} chunks created`);
|
|
2358
2798
|
let vectorsCreated = 0;
|
|
2359
2799
|
if (!options.skipEmbedding && allChunksWithMeta.length > 0) {
|
|
2360
|
-
const embedder = await createEmbedder();
|
|
2800
|
+
const embedder = await createEmbedder(absoluteRoot);
|
|
2361
2801
|
const texts = allChunksWithMeta.map(
|
|
2362
2802
|
(cm) => prepareChunkText(cm.fileRelPath, cm.chunk.parent, cm.chunk.text)
|
|
2363
2803
|
);
|
|
@@ -2373,13 +2813,13 @@ async function runInit(projectPath, options = {}) {
|
|
|
2373
2813
|
vectorsCreated = vectors.length;
|
|
2374
2814
|
}
|
|
2375
2815
|
const durationMs = performance.now() - start;
|
|
2376
|
-
const dbSize =
|
|
2816
|
+
const dbSize = fs6.existsSync(dbPath) ? fs6.statSync(dbPath).size : 0;
|
|
2377
2817
|
log("");
|
|
2378
2818
|
log(`\u2713 Indexed in ${formatDuration(durationMs)}`);
|
|
2379
2819
|
log(
|
|
2380
2820
|
` ${discovered.length} files \u2192 ${allChunksWithMeta.length} chunks` + (vectorsCreated > 0 ? ` \u2192 ${vectorsCreated} vectors` : "")
|
|
2381
2821
|
);
|
|
2382
|
-
log(` Database: ${
|
|
2822
|
+
log(` Database: ${CTX_DIR2}/${DB_FILENAME} (${formatBytes(dbSize)})`);
|
|
2383
2823
|
return {
|
|
2384
2824
|
filesDiscovered: discovered.length,
|
|
2385
2825
|
filesAdded: changes.added.length,
|
|
@@ -2395,14 +2835,14 @@ async function runInit(projectPath, options = {}) {
|
|
|
2395
2835
|
db.close();
|
|
2396
2836
|
}
|
|
2397
2837
|
}
|
|
2398
|
-
async function createEmbedder() {
|
|
2399
|
-
return
|
|
2838
|
+
async function createEmbedder(projectPath) {
|
|
2839
|
+
return createProjectEmbedder(projectPath);
|
|
2400
2840
|
}
|
|
2401
2841
|
|
|
2402
2842
|
// src/cli/commands/query.ts
|
|
2403
|
-
import
|
|
2404
|
-
import
|
|
2405
|
-
var
|
|
2843
|
+
import fs7 from "fs";
|
|
2844
|
+
import path6 from "path";
|
|
2845
|
+
var CTX_DIR3 = ".ctx";
|
|
2406
2846
|
var DB_FILENAME2 = "index.db";
|
|
2407
2847
|
var SNIPPET_MAX_LENGTH = 200;
|
|
2408
2848
|
var STRATEGY_WEIGHTS = {
|
|
@@ -2412,6 +2852,20 @@ var STRATEGY_WEIGHTS = {
|
|
|
2412
2852
|
path: 0.7,
|
|
2413
2853
|
dependency: 0.6
|
|
2414
2854
|
};
|
|
2855
|
+
function getEffectiveStrategyWeights(query) {
|
|
2856
|
+
const { multipliers } = classifyQuery(query);
|
|
2857
|
+
return {
|
|
2858
|
+
vector: STRATEGY_WEIGHTS.vector * multipliers.vector,
|
|
2859
|
+
fts: STRATEGY_WEIGHTS.fts * multipliers.fts,
|
|
2860
|
+
ast: STRATEGY_WEIGHTS.ast * multipliers.ast,
|
|
2861
|
+
path: STRATEGY_WEIGHTS.path * multipliers.path,
|
|
2862
|
+
dependency: STRATEGY_WEIGHTS.dependency * multipliers.dependency
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
function normalizeLimit(limit) {
|
|
2866
|
+
if (!Number.isFinite(limit)) return 0;
|
|
2867
|
+
return Math.max(0, Math.trunc(limit));
|
|
2868
|
+
}
|
|
2415
2869
|
function truncateSnippet(text) {
|
|
2416
2870
|
const oneLine = text.replace(/\n/g, " ").replace(/\s+/g, " ").trim();
|
|
2417
2871
|
if (oneLine.length <= SNIPPET_MAX_LENGTH) return oneLine;
|
|
@@ -2452,20 +2906,25 @@ function isPathLike(query) {
|
|
|
2452
2906
|
return query.includes("/") || query.includes("*") || query.includes(".");
|
|
2453
2907
|
}
|
|
2454
2908
|
async function runQuery(projectPath, query, options) {
|
|
2455
|
-
const
|
|
2456
|
-
const
|
|
2457
|
-
|
|
2909
|
+
const limit = normalizeLimit(options.limit);
|
|
2910
|
+
const absoluteRoot = path6.resolve(projectPath);
|
|
2911
|
+
const dbPath = path6.join(absoluteRoot, CTX_DIR3, DB_FILENAME2);
|
|
2912
|
+
if (!fs7.existsSync(dbPath)) {
|
|
2458
2913
|
throw new KontextError(
|
|
2459
|
-
`Project not initialized. Run "ctx init" first. (${
|
|
2914
|
+
`Project not initialized. Run "ctx init" first. (${CTX_DIR3}/${DB_FILENAME2} not found)`,
|
|
2460
2915
|
ErrorCode.NOT_INITIALIZED
|
|
2461
2916
|
);
|
|
2462
2917
|
}
|
|
2463
2918
|
const start = performance.now();
|
|
2464
|
-
const
|
|
2919
|
+
const embedderConfig = getProjectEmbedderConfig(absoluteRoot);
|
|
2920
|
+
const db = createDatabase(dbPath, embedderConfig.dimensions);
|
|
2465
2921
|
try {
|
|
2466
|
-
const strategyResults = await runStrategies(db, query,
|
|
2922
|
+
const strategyResults = await runStrategies(db, absoluteRoot, query, {
|
|
2923
|
+
...options,
|
|
2924
|
+
limit
|
|
2925
|
+
});
|
|
2467
2926
|
const pathBoostTerms = extractPathBoostTerms(query);
|
|
2468
|
-
const fused = fusionMergeWithPathBoost(strategyResults,
|
|
2927
|
+
const fused = fusionMergeWithPathBoost(strategyResults, limit, pathBoostTerms);
|
|
2469
2928
|
const outputResults = fused.map(toOutputResult);
|
|
2470
2929
|
const searchTimeMs = Math.round(performance.now() - start);
|
|
2471
2930
|
const text = options.format === "text" ? formatTextOutput(query, outputResults) : void 0;
|
|
@@ -2483,14 +2942,16 @@ async function runQuery(projectPath, query, options) {
|
|
|
2483
2942
|
db.close();
|
|
2484
2943
|
}
|
|
2485
2944
|
}
|
|
2486
|
-
async function runStrategies(db, query, options) {
|
|
2945
|
+
async function runStrategies(db, projectPath, query, options) {
|
|
2487
2946
|
const results = [];
|
|
2488
2947
|
const filters = options.language ? { language: options.language } : void 0;
|
|
2489
2948
|
const limit = options.limit * 3;
|
|
2949
|
+
const effectiveWeights = getEffectiveStrategyWeights(query);
|
|
2490
2950
|
for (const strategy of options.strategies) {
|
|
2491
|
-
const weight =
|
|
2951
|
+
const weight = effectiveWeights[strategy];
|
|
2492
2952
|
const searchResults = await executeStrategy(
|
|
2493
2953
|
db,
|
|
2954
|
+
projectPath,
|
|
2494
2955
|
strategy,
|
|
2495
2956
|
query,
|
|
2496
2957
|
limit,
|
|
@@ -2502,10 +2963,10 @@ async function runStrategies(db, query, options) {
|
|
|
2502
2963
|
}
|
|
2503
2964
|
return results;
|
|
2504
2965
|
}
|
|
2505
|
-
async function executeStrategy(db, strategy, query, limit, filters) {
|
|
2966
|
+
async function executeStrategy(db, projectPath, strategy, query, limit, filters) {
|
|
2506
2967
|
switch (strategy) {
|
|
2507
2968
|
case "vector": {
|
|
2508
|
-
const embedder = await loadEmbedder();
|
|
2969
|
+
const embedder = await loadEmbedder(projectPath);
|
|
2509
2970
|
return vectorSearch(db, embedder, query, limit, filters);
|
|
2510
2971
|
}
|
|
2511
2972
|
case "fts":
|
|
@@ -2538,19 +2999,30 @@ async function executeStrategy(db, strategy, query, limit, filters) {
|
|
|
2538
2999
|
}
|
|
2539
3000
|
}
|
|
2540
3001
|
var embedderInstance = null;
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
3002
|
+
var embedderKey = null;
|
|
3003
|
+
function getCacheKey(projectPath) {
|
|
3004
|
+
const config = getProjectEmbedderConfig(projectPath);
|
|
3005
|
+
return `${projectPath}:${config.provider}:${config.model}:${config.dimensions}`;
|
|
3006
|
+
}
|
|
3007
|
+
async function loadEmbedder(projectPath) {
|
|
3008
|
+
const cacheKey = getCacheKey(projectPath);
|
|
3009
|
+
if (embedderInstance && embedderKey === cacheKey) return embedderInstance;
|
|
3010
|
+
embedderInstance = await createProjectEmbedder(projectPath);
|
|
3011
|
+
embedderKey = cacheKey;
|
|
2544
3012
|
return embedderInstance;
|
|
2545
3013
|
}
|
|
2546
3014
|
|
|
2547
3015
|
// src/cli/commands/ask.ts
|
|
2548
|
-
import
|
|
2549
|
-
import
|
|
2550
|
-
var
|
|
3016
|
+
import fs8 from "fs";
|
|
3017
|
+
import path7 from "path";
|
|
3018
|
+
var CTX_DIR4 = ".ctx";
|
|
2551
3019
|
var DB_FILENAME3 = "index.db";
|
|
2552
3020
|
var SNIPPET_MAX_LENGTH2 = 200;
|
|
2553
3021
|
var FALLBACK_NOTICE = "No LLM provider configured. Set CTX_GEMINI_KEY, CTX_OPENAI_KEY, or CTX_ANTHROPIC_KEY. Running basic search instead.";
|
|
3022
|
+
function normalizeLimit2(limit) {
|
|
3023
|
+
if (!Number.isFinite(limit)) return 0;
|
|
3024
|
+
return Math.max(0, Math.trunc(limit));
|
|
3025
|
+
}
|
|
2554
3026
|
function truncateSnippet2(text) {
|
|
2555
3027
|
const oneLine = text.replace(/\n/g, " ").replace(/\s+/g, " ").trim();
|
|
2556
3028
|
if (oneLine.length <= SNIPPET_MAX_LENGTH2) return oneLine;
|
|
@@ -2603,13 +3075,13 @@ function formatTextOutput2(output) {
|
|
|
2603
3075
|
);
|
|
2604
3076
|
return lines.join("\n");
|
|
2605
3077
|
}
|
|
2606
|
-
function createSearchExecutor(db, query) {
|
|
3078
|
+
function createSearchExecutor(db, projectPath, query) {
|
|
2607
3079
|
const pathBoostTerms = extractPathBoostTerms(query);
|
|
2608
3080
|
return async (strategies, limit) => {
|
|
2609
3081
|
const strategyResults = [];
|
|
2610
3082
|
const fetchLimit = limit * 3;
|
|
2611
3083
|
for (const plan of strategies) {
|
|
2612
|
-
const results = await executeStrategy2(db, plan, fetchLimit);
|
|
3084
|
+
const results = await executeStrategy2(db, projectPath, plan, fetchLimit);
|
|
2613
3085
|
if (results.length > 0) {
|
|
2614
3086
|
strategyResults.push({
|
|
2615
3087
|
strategy: plan.strategy,
|
|
@@ -2628,10 +3100,10 @@ function extractSymbolNames2(query) {
|
|
|
2628
3100
|
function isPathLike2(query) {
|
|
2629
3101
|
return query.includes("/") || query.includes("*") || query.includes(".");
|
|
2630
3102
|
}
|
|
2631
|
-
async function executeStrategy2(db, plan, limit) {
|
|
3103
|
+
async function executeStrategy2(db, projectPath, plan, limit) {
|
|
2632
3104
|
switch (plan.strategy) {
|
|
2633
3105
|
case "vector": {
|
|
2634
|
-
const embedder = await loadEmbedder2();
|
|
3106
|
+
const embedder = await loadEmbedder2(projectPath);
|
|
2635
3107
|
return vectorSearch(db, embedder, plan.query, limit);
|
|
2636
3108
|
}
|
|
2637
3109
|
case "fts":
|
|
@@ -2660,19 +3132,21 @@ async function executeStrategy2(db, plan, limit) {
|
|
|
2660
3132
|
}
|
|
2661
3133
|
}
|
|
2662
3134
|
var embedderInstance2 = null;
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
3135
|
+
var embedderKey2 = null;
|
|
3136
|
+
function getCacheKey2(projectPath) {
|
|
3137
|
+
const config = getProjectEmbedderConfig(projectPath);
|
|
3138
|
+
return `${projectPath}:${config.provider}:${config.model}:${config.dimensions}`;
|
|
3139
|
+
}
|
|
3140
|
+
async function loadEmbedder2(projectPath) {
|
|
3141
|
+
const cacheKey = getCacheKey2(projectPath);
|
|
3142
|
+
if (embedderInstance2 && embedderKey2 === cacheKey) return embedderInstance2;
|
|
3143
|
+
embedderInstance2 = await createProjectEmbedder(projectPath);
|
|
3144
|
+
embedderKey2 = cacheKey;
|
|
2666
3145
|
return embedderInstance2;
|
|
2667
3146
|
}
|
|
2668
|
-
async function fallbackSearch(db, query, limit) {
|
|
2669
|
-
const executor = createSearchExecutor(db, query);
|
|
2670
|
-
const
|
|
2671
|
-
const fallbackStrategies = [
|
|
2672
|
-
{ strategy: "fts", query: keywords, weight: 0.8, reason: "fallback keyword search" },
|
|
2673
|
-
{ strategy: "ast", query: keywords, weight: 0.9, reason: "fallback structural search" },
|
|
2674
|
-
{ strategy: "path", query: keywords, weight: 0.7, reason: "fallback path search" }
|
|
2675
|
-
];
|
|
3147
|
+
async function fallbackSearch(db, projectPath, query, limit) {
|
|
3148
|
+
const executor = createSearchExecutor(db, projectPath, query);
|
|
3149
|
+
const fallbackStrategies = buildFallbackStrategies(query);
|
|
2676
3150
|
const results = await executor(fallbackStrategies, limit);
|
|
2677
3151
|
return {
|
|
2678
3152
|
query,
|
|
@@ -2689,37 +3163,39 @@ async function fallbackSearch(db, query, limit) {
|
|
|
2689
3163
|
};
|
|
2690
3164
|
}
|
|
2691
3165
|
async function runAsk(projectPath, query, options) {
|
|
2692
|
-
const
|
|
2693
|
-
const
|
|
2694
|
-
|
|
3166
|
+
const limit = normalizeLimit2(options.limit);
|
|
3167
|
+
const absoluteRoot = path7.resolve(projectPath);
|
|
3168
|
+
const dbPath = path7.join(absoluteRoot, CTX_DIR4, DB_FILENAME3);
|
|
3169
|
+
if (!fs8.existsSync(dbPath)) {
|
|
2695
3170
|
throw new KontextError(
|
|
2696
|
-
`Project not initialized. Run "ctx init" first. (${
|
|
3171
|
+
`Project not initialized. Run "ctx init" first. (${CTX_DIR4}/${DB_FILENAME3} not found)`,
|
|
2697
3172
|
ErrorCode.NOT_INITIALIZED
|
|
2698
3173
|
);
|
|
2699
3174
|
}
|
|
2700
|
-
const
|
|
3175
|
+
const embedderConfig = getProjectEmbedderConfig(absoluteRoot);
|
|
3176
|
+
const db = createDatabase(dbPath, embedderConfig.dimensions);
|
|
2701
3177
|
try {
|
|
2702
3178
|
const provider = options.provider ?? null;
|
|
2703
3179
|
if (!provider) {
|
|
2704
|
-
const output = await fallbackSearch(db, query,
|
|
3180
|
+
const output = await fallbackSearch(db, absoluteRoot, query, limit);
|
|
2705
3181
|
output.warning = FALLBACK_NOTICE;
|
|
2706
3182
|
if (options.format === "text") {
|
|
2707
3183
|
output.text = formatTextOutput2(output);
|
|
2708
3184
|
}
|
|
2709
3185
|
return output;
|
|
2710
3186
|
}
|
|
2711
|
-
const executor = createSearchExecutor(db, query);
|
|
3187
|
+
const executor = createSearchExecutor(db, absoluteRoot, query);
|
|
2712
3188
|
if (options.noExplain) {
|
|
2713
|
-
return await runNoExplain(provider, query, options, executor);
|
|
3189
|
+
return await runNoExplain(provider, query, limit, options, executor);
|
|
2714
3190
|
}
|
|
2715
|
-
return await runWithSteering(provider, query, options, executor);
|
|
3191
|
+
return await runWithSteering(provider, query, limit, options, executor);
|
|
2716
3192
|
} finally {
|
|
2717
3193
|
db.close();
|
|
2718
3194
|
}
|
|
2719
3195
|
}
|
|
2720
|
-
async function runNoExplain(provider, query, options, executor) {
|
|
3196
|
+
async function runNoExplain(provider, query, limit, options, executor) {
|
|
2721
3197
|
const plan = await planSearch(provider, query);
|
|
2722
|
-
const results = await executor(plan.strategies,
|
|
3198
|
+
const results = await executor(plan.strategies, limit);
|
|
2723
3199
|
const output = {
|
|
2724
3200
|
query,
|
|
2725
3201
|
interpretation: plan.interpretation,
|
|
@@ -2737,8 +3213,8 @@ async function runNoExplain(provider, query, options, executor) {
|
|
|
2737
3213
|
}
|
|
2738
3214
|
return output;
|
|
2739
3215
|
}
|
|
2740
|
-
async function runWithSteering(provider, query, options, executor) {
|
|
2741
|
-
const result = await steer(provider, query,
|
|
3216
|
+
async function runWithSteering(provider, query, limit, options, executor) {
|
|
3217
|
+
const result = await steer(provider, query, limit, executor);
|
|
2742
3218
|
const output = {
|
|
2743
3219
|
query,
|
|
2744
3220
|
interpretation: result.interpretation,
|
|
@@ -2758,11 +3234,11 @@ async function runWithSteering(provider, query, options, executor) {
|
|
|
2758
3234
|
}
|
|
2759
3235
|
|
|
2760
3236
|
// src/cli/commands/status.ts
|
|
2761
|
-
import
|
|
2762
|
-
import
|
|
2763
|
-
var
|
|
3237
|
+
import fs9 from "fs";
|
|
3238
|
+
import path8 from "path";
|
|
3239
|
+
var CTX_DIR5 = ".ctx";
|
|
2764
3240
|
var DB_FILENAME4 = "index.db";
|
|
2765
|
-
var
|
|
3241
|
+
var CONFIG_FILENAME3 = "config.json";
|
|
2766
3242
|
function formatBytes2(bytes) {
|
|
2767
3243
|
if (bytes < 1024) return `${bytes} B`;
|
|
2768
3244
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -2777,15 +3253,17 @@ function formatTimestamp(raw) {
|
|
|
2777
3253
|
function capitalize(s) {
|
|
2778
3254
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
2779
3255
|
}
|
|
2780
|
-
function
|
|
2781
|
-
const
|
|
2782
|
-
if (!
|
|
3256
|
+
function readConfig2(ctxDir) {
|
|
3257
|
+
const configPath2 = path8.join(ctxDir, CONFIG_FILENAME3);
|
|
3258
|
+
if (!fs9.existsSync(configPath2)) return null;
|
|
2783
3259
|
try {
|
|
2784
|
-
const raw =
|
|
3260
|
+
const raw = fs9.readFileSync(configPath2, "utf-8");
|
|
2785
3261
|
const parsed = JSON.parse(raw);
|
|
3262
|
+
const embedder = parsed.embedder;
|
|
2786
3263
|
return {
|
|
2787
|
-
|
|
2788
|
-
|
|
3264
|
+
provider: embedder?.provider ?? parsed.provider ?? "unknown",
|
|
3265
|
+
model: embedder?.model ?? parsed.model ?? "unknown",
|
|
3266
|
+
dimensions: embedder?.dimensions ?? parsed.dimensions ?? 0
|
|
2789
3267
|
};
|
|
2790
3268
|
} catch {
|
|
2791
3269
|
return null;
|
|
@@ -2804,7 +3282,7 @@ function formatStatus(projectPath, output) {
|
|
|
2804
3282
|
`Kontext Status \u2014 ${projectPath}`,
|
|
2805
3283
|
"",
|
|
2806
3284
|
` Initialized: Yes`,
|
|
2807
|
-
` Database: ${
|
|
3285
|
+
` Database: ${CTX_DIR5}/${DB_FILENAME4} (${formatBytes2(output.dbSizeBytes)})`
|
|
2808
3286
|
];
|
|
2809
3287
|
if (output.lastIndexed) {
|
|
2810
3288
|
lines.push(` Last indexed: ${formatTimestamp(output.lastIndexed)}`);
|
|
@@ -2827,17 +3305,17 @@ function formatStatus(projectPath, output) {
|
|
|
2827
3305
|
if (output.config) {
|
|
2828
3306
|
lines.push("");
|
|
2829
3307
|
lines.push(
|
|
2830
|
-
` Embedder:
|
|
3308
|
+
` Embedder: ${output.config.provider} (${output.config.model}, ${output.config.dimensions} dims)`
|
|
2831
3309
|
);
|
|
2832
3310
|
}
|
|
2833
3311
|
lines.push("");
|
|
2834
3312
|
return lines.join("\n");
|
|
2835
3313
|
}
|
|
2836
3314
|
async function runStatus(projectPath) {
|
|
2837
|
-
const absoluteRoot =
|
|
2838
|
-
const ctxDir =
|
|
2839
|
-
const dbPath =
|
|
2840
|
-
if (!
|
|
3315
|
+
const absoluteRoot = path8.resolve(projectPath);
|
|
3316
|
+
const ctxDir = path8.join(absoluteRoot, CTX_DIR5);
|
|
3317
|
+
const dbPath = path8.join(ctxDir, DB_FILENAME4);
|
|
3318
|
+
if (!fs9.existsSync(dbPath)) {
|
|
2841
3319
|
const output = {
|
|
2842
3320
|
initialized: false,
|
|
2843
3321
|
fileCount: 0,
|
|
@@ -2858,8 +3336,8 @@ async function runStatus(projectPath) {
|
|
|
2858
3336
|
const vectorCount = db.getVectorCount();
|
|
2859
3337
|
const languages = db.getLanguageBreakdown();
|
|
2860
3338
|
const lastIndexed = db.getLastIndexed();
|
|
2861
|
-
const config =
|
|
2862
|
-
const dbSizeBytes =
|
|
3339
|
+
const config = readConfig2(ctxDir);
|
|
3340
|
+
const dbSizeBytes = fs9.statSync(dbPath).size;
|
|
2863
3341
|
const output = {
|
|
2864
3342
|
initialized: true,
|
|
2865
3343
|
fileCount,
|