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/cli/index.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
+
import { createRequire as createRequire2 } from "module";
|
|
5
6
|
|
|
6
7
|
// src/cli/commands/init.ts
|
|
7
|
-
import
|
|
8
|
-
import
|
|
8
|
+
import fs6 from "fs";
|
|
9
|
+
import path5 from "path";
|
|
9
10
|
|
|
10
11
|
// src/indexer/discovery.ts
|
|
11
12
|
import fs from "fs/promises";
|
|
@@ -778,6 +779,25 @@ function prepareChunkText(filePath, parent, text) {
|
|
|
778
779
|
parts.push(text);
|
|
779
780
|
return parts.join("\n");
|
|
780
781
|
}
|
|
782
|
+
var MAX_RETRIES = 3;
|
|
783
|
+
var BASE_DELAY_MS = 500;
|
|
784
|
+
async function fetchWithRetry(url, init) {
|
|
785
|
+
let lastError = null;
|
|
786
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
787
|
+
const response = await fetch(url, init);
|
|
788
|
+
if (response.ok) return response;
|
|
789
|
+
if (response.status === 429 && attempt < MAX_RETRIES) {
|
|
790
|
+
const delay = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
791
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
792
|
+
lastError = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
throw new Error(
|
|
796
|
+
`Embedding API error: HTTP ${response.status} ${response.statusText}`
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
throw lastError ?? new Error("Embedding API request failed after retries");
|
|
800
|
+
}
|
|
781
801
|
var LOCAL_MODEL_ID = "Xenova/all-MiniLM-L6-v2";
|
|
782
802
|
var LOCAL_DIMENSIONS = 384;
|
|
783
803
|
var LOCAL_BATCH_SIZE = 32;
|
|
@@ -835,6 +855,87 @@ async function createLocalEmbedder() {
|
|
|
835
855
|
}
|
|
836
856
|
};
|
|
837
857
|
}
|
|
858
|
+
var VOYAGE_API_URL = "https://api.voyageai.com/v1/embeddings";
|
|
859
|
+
var VOYAGE_MODEL = "voyage-code-3";
|
|
860
|
+
var VOYAGE_DEFAULT_DIMENSIONS = 1024;
|
|
861
|
+
var VOYAGE_BATCH_SIZE = 128;
|
|
862
|
+
function createVoyageEmbedder(apiKey, dimensions = VOYAGE_DEFAULT_DIMENSIONS) {
|
|
863
|
+
return {
|
|
864
|
+
name: VOYAGE_MODEL,
|
|
865
|
+
dimensions,
|
|
866
|
+
async embed(texts, onProgress) {
|
|
867
|
+
const results = [];
|
|
868
|
+
for (let i = 0; i < texts.length; i += VOYAGE_BATCH_SIZE) {
|
|
869
|
+
const batch = texts.slice(i, i + VOYAGE_BATCH_SIZE);
|
|
870
|
+
const vectors = await callVoyageAPI(apiKey, batch, "document", dimensions);
|
|
871
|
+
results.push(...vectors);
|
|
872
|
+
onProgress?.(Math.min(i + batch.length, texts.length), texts.length);
|
|
873
|
+
}
|
|
874
|
+
return results;
|
|
875
|
+
},
|
|
876
|
+
async embedSingle(text) {
|
|
877
|
+
const vectors = await callVoyageAPI(apiKey, [text], "query", dimensions);
|
|
878
|
+
return vectors[0];
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
async function callVoyageAPI(apiKey, texts, inputType, dimensions) {
|
|
883
|
+
const response = await fetchWithRetry(VOYAGE_API_URL, {
|
|
884
|
+
method: "POST",
|
|
885
|
+
headers: {
|
|
886
|
+
"Content-Type": "application/json",
|
|
887
|
+
Authorization: `Bearer ${apiKey}`
|
|
888
|
+
},
|
|
889
|
+
body: JSON.stringify({
|
|
890
|
+
model: VOYAGE_MODEL,
|
|
891
|
+
input: texts,
|
|
892
|
+
input_type: inputType,
|
|
893
|
+
output_dimension: dimensions
|
|
894
|
+
})
|
|
895
|
+
});
|
|
896
|
+
const json = await response.json();
|
|
897
|
+
return json.data.map((d) => normalizeVector(new Float32Array(d.embedding)));
|
|
898
|
+
}
|
|
899
|
+
var OPENAI_API_URL = "https://api.openai.com/v1/embeddings";
|
|
900
|
+
var OPENAI_MODEL = "text-embedding-3-large";
|
|
901
|
+
var OPENAI_DEFAULT_DIMENSIONS = 1024;
|
|
902
|
+
var OPENAI_BATCH_SIZE = 128;
|
|
903
|
+
function createOpenAIEmbedder(apiKey, dimensions = OPENAI_DEFAULT_DIMENSIONS) {
|
|
904
|
+
return {
|
|
905
|
+
name: OPENAI_MODEL,
|
|
906
|
+
dimensions,
|
|
907
|
+
async embed(texts, onProgress) {
|
|
908
|
+
const results = [];
|
|
909
|
+
for (let i = 0; i < texts.length; i += OPENAI_BATCH_SIZE) {
|
|
910
|
+
const batch = texts.slice(i, i + OPENAI_BATCH_SIZE);
|
|
911
|
+
const vectors = await callOpenAIAPI(apiKey, batch, dimensions);
|
|
912
|
+
results.push(...vectors);
|
|
913
|
+
onProgress?.(Math.min(i + batch.length, texts.length), texts.length);
|
|
914
|
+
}
|
|
915
|
+
return results;
|
|
916
|
+
},
|
|
917
|
+
async embedSingle(text) {
|
|
918
|
+
const vectors = await callOpenAIAPI(apiKey, [text], dimensions);
|
|
919
|
+
return vectors[0];
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
async function callOpenAIAPI(apiKey, texts, dimensions) {
|
|
924
|
+
const response = await fetchWithRetry(OPENAI_API_URL, {
|
|
925
|
+
method: "POST",
|
|
926
|
+
headers: {
|
|
927
|
+
"Content-Type": "application/json",
|
|
928
|
+
Authorization: `Bearer ${apiKey}`
|
|
929
|
+
},
|
|
930
|
+
body: JSON.stringify({
|
|
931
|
+
model: OPENAI_MODEL,
|
|
932
|
+
input: texts,
|
|
933
|
+
dimensions
|
|
934
|
+
})
|
|
935
|
+
});
|
|
936
|
+
const json = await response.json();
|
|
937
|
+
return json.data.map((d) => normalizeVector(new Float32Array(d.embedding)));
|
|
938
|
+
}
|
|
838
939
|
|
|
839
940
|
// src/utils/errors.ts
|
|
840
941
|
var ErrorCode = {
|
|
@@ -876,6 +977,12 @@ var ConfigError = class extends KontextError {
|
|
|
876
977
|
this.name = "ConfigError";
|
|
877
978
|
}
|
|
878
979
|
};
|
|
980
|
+
var DatabaseError = class extends KontextError {
|
|
981
|
+
constructor(message, code, cause) {
|
|
982
|
+
super(message, code, cause);
|
|
983
|
+
this.name = "DatabaseError";
|
|
984
|
+
}
|
|
985
|
+
};
|
|
879
986
|
|
|
880
987
|
// src/utils/error-boundary.ts
|
|
881
988
|
function handleCommandError(err, logger, verbose) {
|
|
@@ -1050,7 +1157,8 @@ function searchVectors(db, query, limit) {
|
|
|
1050
1157
|
|
|
1051
1158
|
// src/storage/db.ts
|
|
1052
1159
|
var DEFAULT_DIMENSIONS = 384;
|
|
1053
|
-
|
|
1160
|
+
var VECTOR_DIMENSIONS_META_KEY = "vector_dimensions";
|
|
1161
|
+
function createDatabase(dbPath, dimensions) {
|
|
1054
1162
|
const dir = path3.dirname(dbPath);
|
|
1055
1163
|
if (!fs4.existsSync(dir)) {
|
|
1056
1164
|
fs4.mkdirSync(dir, { recursive: true });
|
|
@@ -1059,7 +1167,8 @@ function createDatabase(dbPath, dimensions = DEFAULT_DIMENSIONS) {
|
|
|
1059
1167
|
db.pragma("journal_mode = WAL");
|
|
1060
1168
|
db.pragma("foreign_keys = ON");
|
|
1061
1169
|
sqliteVec.load(db);
|
|
1062
|
-
initializeSchema(db, dimensions);
|
|
1170
|
+
initializeSchema(db, dimensions ?? DEFAULT_DIMENSIONS);
|
|
1171
|
+
ensureVectorDimensions(db, dimensions);
|
|
1063
1172
|
const stmtUpsertFile = db.prepare(`
|
|
1064
1173
|
INSERT INTO files (path, language, hash, last_indexed, size)
|
|
1065
1174
|
VALUES (@path, @language, @hash, @lastIndexed, @size)
|
|
@@ -1068,6 +1177,7 @@ function createDatabase(dbPath, dimensions = DEFAULT_DIMENSIONS) {
|
|
|
1068
1177
|
hash = excluded.hash,
|
|
1069
1178
|
last_indexed = excluded.last_indexed,
|
|
1070
1179
|
size = excluded.size
|
|
1180
|
+
RETURNING id
|
|
1071
1181
|
`);
|
|
1072
1182
|
const stmtGetFile = db.prepare(
|
|
1073
1183
|
"SELECT id, path, language, hash, last_indexed as lastIndexed, size FROM files WHERE path = ?"
|
|
@@ -1111,18 +1221,20 @@ function createDatabase(dbPath, dimensions = DEFAULT_DIMENSIONS) {
|
|
|
1111
1221
|
);
|
|
1112
1222
|
return {
|
|
1113
1223
|
upsertFile(file) {
|
|
1114
|
-
const
|
|
1224
|
+
const row = stmtUpsertFile.get({
|
|
1115
1225
|
path: file.path,
|
|
1116
1226
|
language: file.language,
|
|
1117
1227
|
hash: file.hash,
|
|
1118
1228
|
lastIndexed: Date.now(),
|
|
1119
1229
|
size: file.size
|
|
1120
1230
|
});
|
|
1121
|
-
if (
|
|
1122
|
-
|
|
1231
|
+
if (!row?.id) {
|
|
1232
|
+
throw new DatabaseError(
|
|
1233
|
+
`Failed to upsert file: ${file.path}`,
|
|
1234
|
+
ErrorCode.DB_WRITE_FAILED
|
|
1235
|
+
);
|
|
1123
1236
|
}
|
|
1124
|
-
|
|
1125
|
-
return existing?.id ?? 0;
|
|
1237
|
+
return row.id;
|
|
1126
1238
|
},
|
|
1127
1239
|
getFile(filePath) {
|
|
1128
1240
|
const row = stmtGetFile.get(filePath);
|
|
@@ -1165,15 +1277,17 @@ function createDatabase(dbPath, dimensions = DEFAULT_DIMENSIONS) {
|
|
|
1165
1277
|
return row.lastIndexed;
|
|
1166
1278
|
},
|
|
1167
1279
|
deleteFile(filePath) {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1280
|
+
db.transaction(() => {
|
|
1281
|
+
const file = stmtGetFile.get(filePath);
|
|
1282
|
+
if (file) {
|
|
1283
|
+
const chunkRows = stmtGetChunkIdsByFile.all(file.id);
|
|
1284
|
+
const chunkIds = chunkRows.map((r) => r.id);
|
|
1285
|
+
if (chunkIds.length > 0) {
|
|
1286
|
+
deleteVectorsByChunkIds(db, chunkIds);
|
|
1287
|
+
}
|
|
1174
1288
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1289
|
+
stmtDeleteFile.run(filePath);
|
|
1290
|
+
})();
|
|
1177
1291
|
},
|
|
1178
1292
|
insertChunks(fileId, chunks) {
|
|
1179
1293
|
const ids = [];
|
|
@@ -1268,12 +1382,14 @@ function createDatabase(dbPath, dimensions = DEFAULT_DIMENSIONS) {
|
|
|
1268
1382
|
}));
|
|
1269
1383
|
},
|
|
1270
1384
|
deleteChunksByFile(fileId) {
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1385
|
+
db.transaction(() => {
|
|
1386
|
+
const chunkRows = stmtGetChunkIdsByFile.all(fileId);
|
|
1387
|
+
const chunkIds = chunkRows.map((r) => r.id);
|
|
1388
|
+
if (chunkIds.length > 0) {
|
|
1389
|
+
deleteVectorsByChunkIds(db, chunkIds);
|
|
1390
|
+
}
|
|
1391
|
+
stmtDeleteChunksByFile.run(fileId);
|
|
1392
|
+
})();
|
|
1277
1393
|
},
|
|
1278
1394
|
insertDependency(sourceChunkId, targetChunkId, type) {
|
|
1279
1395
|
stmtInsertDep.run(sourceChunkId, targetChunkId, type);
|
|
@@ -1335,105 +1451,440 @@ function getMetaVersion(db) {
|
|
|
1335
1451
|
return 0;
|
|
1336
1452
|
}
|
|
1337
1453
|
}
|
|
1454
|
+
function ensureVectorDimensions(db, expectedDimensions) {
|
|
1455
|
+
const actual = getExistingVectorDimensions(db);
|
|
1456
|
+
const stored = db.prepare("SELECT value FROM meta WHERE key = ?").get(VECTOR_DIMENSIONS_META_KEY);
|
|
1457
|
+
const storedValue = stored?.value;
|
|
1458
|
+
const storedDimensions = storedValue ? Number.parseInt(storedValue, 10) : void 0;
|
|
1459
|
+
if (storedDimensions !== void 0 && (!Number.isInteger(storedDimensions) || storedDimensions <= 0)) {
|
|
1460
|
+
throw new DatabaseError(
|
|
1461
|
+
`Invalid stored vector dimensions metadata: ${storedValue ?? "unknown"}`,
|
|
1462
|
+
ErrorCode.DB_CORRUPTED
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
if (actual !== null && storedDimensions !== void 0 && storedDimensions !== actual) {
|
|
1466
|
+
throw new DatabaseError(
|
|
1467
|
+
`Vector dimensions metadata mismatch: meta=${storedDimensions}, table=${actual}.`,
|
|
1468
|
+
ErrorCode.DB_CORRUPTED
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
if (expectedDimensions === void 0) {
|
|
1472
|
+
if (storedDimensions !== void 0) return;
|
|
1473
|
+
const dimensions = actual ?? DEFAULT_DIMENSIONS;
|
|
1474
|
+
db.prepare(
|
|
1475
|
+
"INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)"
|
|
1476
|
+
).run(VECTOR_DIMENSIONS_META_KEY, String(dimensions));
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
if (!stored) {
|
|
1480
|
+
const dimensions = actual ?? expectedDimensions;
|
|
1481
|
+
db.prepare(
|
|
1482
|
+
"INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)"
|
|
1483
|
+
).run(VECTOR_DIMENSIONS_META_KEY, String(dimensions));
|
|
1484
|
+
if (actual !== null && actual !== expectedDimensions) {
|
|
1485
|
+
throw new DatabaseError(
|
|
1486
|
+
`Vector dimension mismatch: index uses ${actual} dims, but config requests ${expectedDimensions} dims. Rebuild the index.`,
|
|
1487
|
+
ErrorCode.CONFIG_INVALID
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
if (storedDimensions !== expectedDimensions) {
|
|
1493
|
+
throw new DatabaseError(
|
|
1494
|
+
`Vector dimension mismatch: index uses ${storedDimensions} dims, but config requests ${expectedDimensions} dims. Rebuild the index.`,
|
|
1495
|
+
ErrorCode.CONFIG_INVALID
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
function getExistingVectorDimensions(db) {
|
|
1500
|
+
const row = db.prepare("SELECT sql FROM sqlite_master WHERE name = 'chunk_vectors'").get();
|
|
1501
|
+
const sql = row?.sql;
|
|
1502
|
+
if (!sql) return null;
|
|
1503
|
+
const match = sql.match(/embedding\s+float\[(\d+)\]/i);
|
|
1504
|
+
if (!match) return null;
|
|
1505
|
+
return Number.parseInt(match[1], 10);
|
|
1506
|
+
}
|
|
1338
1507
|
|
|
1339
|
-
// src/cli/commands/
|
|
1508
|
+
// src/cli/commands/config.ts
|
|
1509
|
+
import fs5 from "fs";
|
|
1510
|
+
import path4 from "path";
|
|
1340
1511
|
var CTX_DIR = ".ctx";
|
|
1341
|
-
var DB_FILENAME = "index.db";
|
|
1342
1512
|
var CONFIG_FILENAME = "config.json";
|
|
1343
|
-
var
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1513
|
+
var DEFAULT_CONFIG = {
|
|
1514
|
+
embedder: {
|
|
1515
|
+
provider: "local",
|
|
1516
|
+
model: "Xenova/all-MiniLM-L6-v2",
|
|
1517
|
+
dimensions: 384
|
|
1518
|
+
},
|
|
1519
|
+
search: {
|
|
1520
|
+
defaultLimit: 10,
|
|
1521
|
+
strategies: ["vector", "fts", "ast", "path"],
|
|
1522
|
+
weights: { vector: 1, fts: 0.8, ast: 0.9, path: 0.7, dependency: 0.6 }
|
|
1523
|
+
},
|
|
1524
|
+
watch: {
|
|
1525
|
+
debounceMs: 500,
|
|
1526
|
+
ignored: []
|
|
1527
|
+
},
|
|
1528
|
+
llm: {
|
|
1529
|
+
provider: null,
|
|
1530
|
+
model: null
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
var VALID_EMBEDDER_PROVIDERS = /* @__PURE__ */ new Set(["local", "voyage", "openai"]);
|
|
1534
|
+
var VALID_LLM_PROVIDERS = /* @__PURE__ */ new Set(["gemini", "openai", "anthropic"]);
|
|
1535
|
+
var VALIDATION_RULES = {
|
|
1536
|
+
"embedder.provider": {
|
|
1537
|
+
validate: (v) => typeof v === "string" && VALID_EMBEDDER_PROVIDERS.has(v),
|
|
1538
|
+
message: `Must be one of: ${[...VALID_EMBEDDER_PROVIDERS].join(", ")}`
|
|
1539
|
+
},
|
|
1540
|
+
"embedder.dimensions": {
|
|
1541
|
+
validate: (v) => typeof v === "number" && v > 0 && Number.isInteger(v),
|
|
1542
|
+
message: "Must be a positive integer"
|
|
1543
|
+
},
|
|
1544
|
+
"search.defaultLimit": {
|
|
1545
|
+
validate: (v) => typeof v === "number" && v > 0 && Number.isInteger(v),
|
|
1546
|
+
message: "Must be a positive integer"
|
|
1547
|
+
},
|
|
1548
|
+
"watch.debounceMs": {
|
|
1549
|
+
validate: (v) => typeof v === "number" && v >= 0 && Number.isInteger(v),
|
|
1550
|
+
message: "Must be a non-negative integer"
|
|
1551
|
+
},
|
|
1552
|
+
"llm.provider": {
|
|
1553
|
+
validate: (v) => v === null || typeof v === "string" && VALID_LLM_PROVIDERS.has(v),
|
|
1554
|
+
message: `Must be null or one of: ${[...VALID_LLM_PROVIDERS].join(", ")}`
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
function resolveCtxDir(projectPath) {
|
|
1558
|
+
const absoluteRoot = path4.resolve(projectPath);
|
|
1559
|
+
const ctxDir = path4.join(absoluteRoot, CTX_DIR);
|
|
1560
|
+
if (!fs5.existsSync(ctxDir)) {
|
|
1561
|
+
throw new ConfigError(
|
|
1562
|
+
`Project not initialized. Run "ctx init" first. (${CTX_DIR}/ not found)`,
|
|
1563
|
+
ErrorCode.NOT_INITIALIZED
|
|
1564
|
+
);
|
|
1355
1565
|
}
|
|
1566
|
+
return ctxDir;
|
|
1356
1567
|
}
|
|
1357
|
-
function
|
|
1358
|
-
|
|
1359
|
-
if (fs5.existsSync(configPath2)) return;
|
|
1360
|
-
const config = {
|
|
1361
|
-
version: 1,
|
|
1362
|
-
dimensions: 384,
|
|
1363
|
-
model: "all-MiniLM-L6-v2"
|
|
1364
|
-
};
|
|
1365
|
-
fs5.writeFileSync(configPath2, JSON.stringify(config, null, 2) + "\n");
|
|
1568
|
+
function configPath(ctxDir) {
|
|
1569
|
+
return path4.join(ctxDir, CONFIG_FILENAME);
|
|
1366
1570
|
}
|
|
1367
|
-
function
|
|
1368
|
-
|
|
1369
|
-
|
|
1571
|
+
function readConfig(ctxDir) {
|
|
1572
|
+
const filePath = configPath(ctxDir);
|
|
1573
|
+
if (!fs5.existsSync(filePath)) {
|
|
1574
|
+
writeConfig(ctxDir, DEFAULT_CONFIG);
|
|
1575
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
1576
|
+
}
|
|
1577
|
+
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
1578
|
+
const parsed = JSON.parse(raw);
|
|
1579
|
+
return mergeWithDefaults(parsed);
|
|
1370
1580
|
}
|
|
1371
|
-
function
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1581
|
+
function writeConfig(ctxDir, config) {
|
|
1582
|
+
fs5.writeFileSync(
|
|
1583
|
+
configPath(ctxDir),
|
|
1584
|
+
JSON.stringify(config, null, 2) + "\n"
|
|
1585
|
+
);
|
|
1375
1586
|
}
|
|
1376
|
-
function
|
|
1377
|
-
|
|
1378
|
-
|
|
1587
|
+
function mergeWithDefaults(partial) {
|
|
1588
|
+
return {
|
|
1589
|
+
embedder: { ...DEFAULT_CONFIG.embedder, ...partial.embedder },
|
|
1590
|
+
search: {
|
|
1591
|
+
...DEFAULT_CONFIG.search,
|
|
1592
|
+
...partial.search,
|
|
1593
|
+
weights: { ...DEFAULT_CONFIG.search.weights, ...partial.search?.weights }
|
|
1594
|
+
},
|
|
1595
|
+
watch: { ...DEFAULT_CONFIG.watch, ...partial.watch },
|
|
1596
|
+
llm: { ...DEFAULT_CONFIG.llm, ...partial.llm }
|
|
1597
|
+
};
|
|
1379
1598
|
}
|
|
1380
|
-
|
|
1381
|
-
const
|
|
1382
|
-
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
if (!fs5.existsSync(ctxDir)) fs5.mkdirSync(ctxDir, { recursive: true });
|
|
1387
|
-
ensureGitignore(absoluteRoot);
|
|
1388
|
-
ensureConfig(ctxDir);
|
|
1389
|
-
const dbPath = path4.join(ctxDir, DB_FILENAME);
|
|
1390
|
-
const db = createDatabase(dbPath);
|
|
1391
|
-
try {
|
|
1392
|
-
const discovered = await discoverFiles({
|
|
1393
|
-
root: absoluteRoot,
|
|
1394
|
-
extraIgnore: [".ctx/"]
|
|
1395
|
-
});
|
|
1396
|
-
const languageCounts = /* @__PURE__ */ new Map();
|
|
1397
|
-
for (const file of discovered) {
|
|
1398
|
-
languageCounts.set(
|
|
1399
|
-
file.language,
|
|
1400
|
-
(languageCounts.get(file.language) ?? 0) + 1
|
|
1401
|
-
);
|
|
1402
|
-
}
|
|
1403
|
-
log(
|
|
1404
|
-
` Discovered ${discovered.length} files` + (discovered.length > 0 ? ` (${formatLanguageSummary(languageCounts)})` : "")
|
|
1405
|
-
);
|
|
1406
|
-
const changes = await computeChanges(discovered, db);
|
|
1407
|
-
const filesToProcess = [
|
|
1408
|
-
...changes.added.map((p) => ({ path: p, reason: "added" })),
|
|
1409
|
-
...changes.modified.map((p) => ({ path: p, reason: "modified" }))
|
|
1410
|
-
];
|
|
1411
|
-
if (changes.unchanged.length > 0) {
|
|
1412
|
-
log(` ${changes.unchanged.length} unchanged files skipped`);
|
|
1599
|
+
function getNestedValue(obj, key) {
|
|
1600
|
+
const parts = key.split(".");
|
|
1601
|
+
let current = obj;
|
|
1602
|
+
for (const part of parts) {
|
|
1603
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
1604
|
+
return void 0;
|
|
1413
1605
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1606
|
+
current = current[part];
|
|
1607
|
+
}
|
|
1608
|
+
return current;
|
|
1609
|
+
}
|
|
1610
|
+
function setNestedValue(obj, key, value) {
|
|
1611
|
+
const parts = key.split(".");
|
|
1612
|
+
let current = obj;
|
|
1613
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1614
|
+
const part = parts[i];
|
|
1615
|
+
if (typeof current[part] !== "object" || current[part] === null) {
|
|
1616
|
+
current[part] = {};
|
|
1416
1617
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1618
|
+
current = current[part];
|
|
1619
|
+
}
|
|
1620
|
+
current[parts[parts.length - 1]] = value;
|
|
1621
|
+
}
|
|
1622
|
+
function hasNestedKey(obj, key) {
|
|
1623
|
+
const parts = key.split(".");
|
|
1624
|
+
let current = obj;
|
|
1625
|
+
for (const part of parts) {
|
|
1626
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
1627
|
+
return false;
|
|
1419
1628
|
}
|
|
1420
|
-
if (
|
|
1421
|
-
|
|
1629
|
+
if (!(part in current)) {
|
|
1630
|
+
return false;
|
|
1422
1631
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1632
|
+
current = current[part];
|
|
1633
|
+
}
|
|
1634
|
+
return true;
|
|
1635
|
+
}
|
|
1636
|
+
function parseValue(rawValue) {
|
|
1637
|
+
if (rawValue === "null") return null;
|
|
1638
|
+
if (rawValue === "true") return true;
|
|
1639
|
+
if (rawValue === "false") return false;
|
|
1640
|
+
const num = Number(rawValue);
|
|
1641
|
+
if (!Number.isNaN(num) && rawValue.trim() !== "") return num;
|
|
1642
|
+
if (rawValue.startsWith("[") || rawValue.startsWith("{")) {
|
|
1643
|
+
try {
|
|
1644
|
+
return JSON.parse(rawValue);
|
|
1645
|
+
} catch {
|
|
1425
1646
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1647
|
+
}
|
|
1648
|
+
return rawValue;
|
|
1649
|
+
}
|
|
1650
|
+
function runConfigShow(projectPath) {
|
|
1651
|
+
const ctxDir = resolveCtxDir(projectPath);
|
|
1652
|
+
const config = readConfig(ctxDir);
|
|
1653
|
+
return {
|
|
1654
|
+
config,
|
|
1655
|
+
text: JSON.stringify(config, null, 2)
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
function runConfigGet(projectPath, key) {
|
|
1659
|
+
const ctxDir = resolveCtxDir(projectPath);
|
|
1660
|
+
const config = readConfig(ctxDir);
|
|
1661
|
+
return getNestedValue(config, key);
|
|
1662
|
+
}
|
|
1663
|
+
function runConfigSet(projectPath, key, rawValue) {
|
|
1664
|
+
const ctxDir = resolveCtxDir(projectPath);
|
|
1665
|
+
const config = readConfig(ctxDir);
|
|
1666
|
+
const value = parseValue(rawValue);
|
|
1667
|
+
const rule = VALIDATION_RULES[key];
|
|
1668
|
+
if (rule && !rule.validate(value)) {
|
|
1669
|
+
throw new ConfigError(`Invalid value for "${key}": ${rule.message}`, ErrorCode.CONFIG_INVALID);
|
|
1670
|
+
}
|
|
1671
|
+
setNestedValue(config, key, value);
|
|
1672
|
+
writeConfig(ctxDir, config);
|
|
1673
|
+
}
|
|
1674
|
+
function runConfigReset(projectPath, key) {
|
|
1675
|
+
const ctxDir = resolveCtxDir(projectPath);
|
|
1676
|
+
if (!key) {
|
|
1677
|
+
writeConfig(ctxDir, structuredClone(DEFAULT_CONFIG));
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
if (!hasNestedKey(DEFAULT_CONFIG, key)) {
|
|
1681
|
+
throw new ConfigError(`Invalid config key: ${key}`, ErrorCode.CONFIG_INVALID);
|
|
1682
|
+
}
|
|
1683
|
+
const config = readConfig(ctxDir);
|
|
1684
|
+
const defaultValue = getNestedValue(
|
|
1685
|
+
DEFAULT_CONFIG,
|
|
1686
|
+
key
|
|
1687
|
+
);
|
|
1688
|
+
setNestedValue(
|
|
1689
|
+
config,
|
|
1690
|
+
key,
|
|
1691
|
+
structuredClone(defaultValue)
|
|
1692
|
+
);
|
|
1693
|
+
writeConfig(ctxDir, config);
|
|
1694
|
+
}
|
|
1695
|
+
function registerConfigCommand(program2) {
|
|
1696
|
+
const cmd = program2.command("config").description("Show or modify configuration");
|
|
1697
|
+
function configErrorHandler(err) {
|
|
1698
|
+
const verbose = program2.opts()["verbose"] === true;
|
|
1699
|
+
const logger = createLogger({ level: verbose ? LogLevel.DEBUG : LogLevel.INFO });
|
|
1700
|
+
process.exitCode = handleCommandError(err, logger, verbose);
|
|
1701
|
+
}
|
|
1702
|
+
cmd.command("show").description("Show current configuration").action(() => {
|
|
1703
|
+
try {
|
|
1704
|
+
const output = runConfigShow(process.cwd());
|
|
1705
|
+
console.log(output.text);
|
|
1706
|
+
} catch (err) {
|
|
1707
|
+
configErrorHandler(err);
|
|
1708
|
+
}
|
|
1709
|
+
});
|
|
1710
|
+
cmd.command("get <key>").description("Get a configuration value (dot notation)").action((key) => {
|
|
1711
|
+
try {
|
|
1712
|
+
const value = runConfigGet(process.cwd(), key);
|
|
1713
|
+
console.log(
|
|
1714
|
+
typeof value === "object" ? JSON.stringify(value, null, 2) : String(value)
|
|
1715
|
+
);
|
|
1716
|
+
} catch (err) {
|
|
1717
|
+
configErrorHandler(err);
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1720
|
+
cmd.command("set <key> <value>").description("Set a configuration value (dot notation)").action((key, value) => {
|
|
1721
|
+
try {
|
|
1722
|
+
runConfigSet(process.cwd(), key, value);
|
|
1723
|
+
console.log(`Set ${key} = ${value}`);
|
|
1724
|
+
} catch (err) {
|
|
1725
|
+
configErrorHandler(err);
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
cmd.command("reset [key]").description("Reset configuration to defaults or reset a specific key").action((key) => {
|
|
1729
|
+
try {
|
|
1730
|
+
runConfigReset(process.cwd(), key);
|
|
1731
|
+
if (key) {
|
|
1732
|
+
console.log(`Reset ${key} to default.`);
|
|
1733
|
+
} else {
|
|
1734
|
+
console.log("Configuration reset to defaults.");
|
|
1735
|
+
}
|
|
1736
|
+
} catch (err) {
|
|
1737
|
+
configErrorHandler(err);
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// src/cli/embedder.ts
|
|
1743
|
+
function getProjectEmbedderConfig(projectPath) {
|
|
1744
|
+
const { config } = runConfigShow(projectPath);
|
|
1745
|
+
return config.embedder;
|
|
1746
|
+
}
|
|
1747
|
+
async function createProjectEmbedder(projectPath) {
|
|
1748
|
+
const config = getProjectEmbedderConfig(projectPath);
|
|
1749
|
+
validateProjectEmbedderConfig(config);
|
|
1750
|
+
switch (config.provider) {
|
|
1751
|
+
case "local":
|
|
1752
|
+
return await createLocalEmbedder();
|
|
1753
|
+
case "voyage": {
|
|
1754
|
+
const apiKey = requireApiKey("CTX_VOYAGE_KEY", "voyage");
|
|
1755
|
+
return createVoyageEmbedder(apiKey, config.dimensions);
|
|
1756
|
+
}
|
|
1757
|
+
case "openai": {
|
|
1758
|
+
const apiKey = requireApiKey("CTX_OPENAI_KEY", "openai");
|
|
1759
|
+
return createOpenAIEmbedder(apiKey, config.dimensions);
|
|
1760
|
+
}
|
|
1761
|
+
default:
|
|
1762
|
+
throw new ConfigError(
|
|
1763
|
+
`Unsupported embedder provider "${config.provider}". Use local, voyage, or openai.`,
|
|
1764
|
+
ErrorCode.CONFIG_INVALID
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
function requireApiKey(envVar, provider) {
|
|
1769
|
+
const value = process.env[envVar];
|
|
1770
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
1771
|
+
throw new ConfigError(
|
|
1772
|
+
`Embedder provider "${provider}" requires ${envVar}. Export ${envVar} before running this command.`,
|
|
1773
|
+
ErrorCode.CONFIG_INVALID
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1776
|
+
function validateProjectEmbedderConfig(config) {
|
|
1777
|
+
if (!Number.isInteger(config.dimensions) || config.dimensions <= 0) {
|
|
1778
|
+
throw new ConfigError(
|
|
1779
|
+
`Invalid embedder.dimensions (${String(config.dimensions)}). Must be a positive integer.`,
|
|
1780
|
+
ErrorCode.CONFIG_INVALID
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
if (config.provider === "local" && config.dimensions !== 384) {
|
|
1784
|
+
throw new ConfigError(
|
|
1785
|
+
'Local embedder requires "embedder.dimensions" = 384. Update config or switch provider.',
|
|
1786
|
+
ErrorCode.CONFIG_INVALID
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// src/cli/commands/init.ts
|
|
1792
|
+
var CTX_DIR2 = ".ctx";
|
|
1793
|
+
var DB_FILENAME = "index.db";
|
|
1794
|
+
var CONFIG_FILENAME2 = "config.json";
|
|
1795
|
+
var GITIGNORE_ENTRY = ".ctx/";
|
|
1796
|
+
function ensureGitignore(projectRoot) {
|
|
1797
|
+
const gitignorePath = path5.join(projectRoot, ".gitignore");
|
|
1798
|
+
if (fs6.existsSync(gitignorePath)) {
|
|
1799
|
+
const content = fs6.readFileSync(gitignorePath, "utf-8");
|
|
1800
|
+
if (content.includes(GITIGNORE_ENTRY)) return;
|
|
1801
|
+
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
1802
|
+
fs6.writeFileSync(gitignorePath, `${content}${suffix}${GITIGNORE_ENTRY}
|
|
1803
|
+
`);
|
|
1804
|
+
} else {
|
|
1805
|
+
fs6.writeFileSync(gitignorePath, `${GITIGNORE_ENTRY}
|
|
1806
|
+
`);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
function ensureConfig(ctxDir) {
|
|
1810
|
+
const configPath2 = path5.join(ctxDir, CONFIG_FILENAME2);
|
|
1811
|
+
if (fs6.existsSync(configPath2)) return;
|
|
1812
|
+
fs6.writeFileSync(
|
|
1813
|
+
configPath2,
|
|
1814
|
+
JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n"
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
function formatDuration(ms) {
|
|
1818
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
1819
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
1820
|
+
}
|
|
1821
|
+
function formatBytes(bytes) {
|
|
1822
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1823
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1824
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1825
|
+
}
|
|
1826
|
+
function formatLanguageSummary(counts) {
|
|
1827
|
+
const entries = [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([lang, count]) => `${lang}: ${count}`);
|
|
1828
|
+
return entries.join(", ");
|
|
1829
|
+
}
|
|
1830
|
+
async function runInit(projectPath, options = {}) {
|
|
1831
|
+
const log = options.log ?? console.log;
|
|
1832
|
+
const absoluteRoot = path5.resolve(projectPath);
|
|
1833
|
+
const start = performance.now();
|
|
1834
|
+
log(`Indexing ${absoluteRoot}...`);
|
|
1835
|
+
const ctxDir = path5.join(absoluteRoot, CTX_DIR2);
|
|
1836
|
+
if (!fs6.existsSync(ctxDir)) fs6.mkdirSync(ctxDir, { recursive: true });
|
|
1837
|
+
ensureGitignore(absoluteRoot);
|
|
1838
|
+
ensureConfig(ctxDir);
|
|
1839
|
+
const embedderConfig = getProjectEmbedderConfig(absoluteRoot);
|
|
1840
|
+
const dbPath = path5.join(ctxDir, DB_FILENAME);
|
|
1841
|
+
const db = createDatabase(dbPath, embedderConfig.dimensions);
|
|
1842
|
+
try {
|
|
1843
|
+
const discovered = await discoverFiles({
|
|
1844
|
+
root: absoluteRoot,
|
|
1845
|
+
extraIgnore: [".ctx/"]
|
|
1846
|
+
});
|
|
1847
|
+
const languageCounts = /* @__PURE__ */ new Map();
|
|
1848
|
+
for (const file of discovered) {
|
|
1849
|
+
languageCounts.set(
|
|
1850
|
+
file.language,
|
|
1851
|
+
(languageCounts.get(file.language) ?? 0) + 1
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
log(
|
|
1855
|
+
` Discovered ${discovered.length} files` + (discovered.length > 0 ? ` (${formatLanguageSummary(languageCounts)})` : "")
|
|
1856
|
+
);
|
|
1857
|
+
const changes = await computeChanges(discovered, db);
|
|
1858
|
+
const filesToProcess = [
|
|
1859
|
+
...changes.added.map((p) => ({ path: p, reason: "added" })),
|
|
1860
|
+
...changes.modified.map((p) => ({ path: p, reason: "modified" }))
|
|
1861
|
+
];
|
|
1862
|
+
if (changes.unchanged.length > 0) {
|
|
1863
|
+
log(` ${changes.unchanged.length} unchanged files skipped`);
|
|
1864
|
+
}
|
|
1865
|
+
if (changes.deleted.length > 0) {
|
|
1866
|
+
log(` ${changes.deleted.length} deleted files removed`);
|
|
1867
|
+
}
|
|
1868
|
+
if (changes.added.length > 0) {
|
|
1869
|
+
log(` ${changes.added.length} new files to index`);
|
|
1870
|
+
}
|
|
1871
|
+
if (changes.modified.length > 0) {
|
|
1872
|
+
log(` ${changes.modified.length} modified files to re-index`);
|
|
1873
|
+
}
|
|
1874
|
+
for (const deletedPath of changes.deleted) {
|
|
1875
|
+
db.deleteFile(deletedPath);
|
|
1876
|
+
}
|
|
1877
|
+
await initParser();
|
|
1878
|
+
const allChunksWithMeta = [];
|
|
1879
|
+
let filesProcessed = 0;
|
|
1880
|
+
for (const { path: relPath } of filesToProcess) {
|
|
1881
|
+
const discovered_file = discovered.find((f) => f.path === relPath);
|
|
1882
|
+
if (!discovered_file) continue;
|
|
1883
|
+
const existingFile = db.getFile(relPath);
|
|
1884
|
+
if (existingFile) {
|
|
1885
|
+
db.deleteChunksByFile(existingFile.id);
|
|
1886
|
+
}
|
|
1887
|
+
let nodes;
|
|
1437
1888
|
try {
|
|
1438
1889
|
nodes = await parseFile(discovered_file.absolutePath, discovered_file.language);
|
|
1439
1890
|
} catch {
|
|
@@ -1475,7 +1926,7 @@ async function runInit(projectPath, options = {}) {
|
|
|
1475
1926
|
log(` ${allChunksWithMeta.length} chunks created`);
|
|
1476
1927
|
let vectorsCreated = 0;
|
|
1477
1928
|
if (!options.skipEmbedding && allChunksWithMeta.length > 0) {
|
|
1478
|
-
const embedder = await createEmbedder();
|
|
1929
|
+
const embedder = await createEmbedder(absoluteRoot);
|
|
1479
1930
|
const texts = allChunksWithMeta.map(
|
|
1480
1931
|
(cm) => prepareChunkText(cm.fileRelPath, cm.chunk.parent, cm.chunk.text)
|
|
1481
1932
|
);
|
|
@@ -1491,13 +1942,13 @@ async function runInit(projectPath, options = {}) {
|
|
|
1491
1942
|
vectorsCreated = vectors.length;
|
|
1492
1943
|
}
|
|
1493
1944
|
const durationMs = performance.now() - start;
|
|
1494
|
-
const dbSize =
|
|
1945
|
+
const dbSize = fs6.existsSync(dbPath) ? fs6.statSync(dbPath).size : 0;
|
|
1495
1946
|
log("");
|
|
1496
1947
|
log(`\u2713 Indexed in ${formatDuration(durationMs)}`);
|
|
1497
1948
|
log(
|
|
1498
1949
|
` ${discovered.length} files \u2192 ${allChunksWithMeta.length} chunks` + (vectorsCreated > 0 ? ` \u2192 ${vectorsCreated} vectors` : "")
|
|
1499
1950
|
);
|
|
1500
|
-
log(` Database: ${
|
|
1951
|
+
log(` Database: ${CTX_DIR2}/${DB_FILENAME} (${formatBytes(dbSize)})`);
|
|
1501
1952
|
return {
|
|
1502
1953
|
filesDiscovered: discovered.length,
|
|
1503
1954
|
filesAdded: changes.added.length,
|
|
@@ -1513,8 +1964,8 @@ async function runInit(projectPath, options = {}) {
|
|
|
1513
1964
|
db.close();
|
|
1514
1965
|
}
|
|
1515
1966
|
}
|
|
1516
|
-
async function createEmbedder() {
|
|
1517
|
-
return
|
|
1967
|
+
async function createEmbedder(projectPath) {
|
|
1968
|
+
return createProjectEmbedder(projectPath);
|
|
1518
1969
|
}
|
|
1519
1970
|
function registerInitCommand(program2) {
|
|
1520
1971
|
program2.command("init [path]").description("Index current directory or specified path").action(async (inputPath) => {
|
|
@@ -1535,8 +1986,8 @@ function registerInitCommand(program2) {
|
|
|
1535
1986
|
}
|
|
1536
1987
|
|
|
1537
1988
|
// src/cli/commands/query.ts
|
|
1538
|
-
import
|
|
1539
|
-
import
|
|
1989
|
+
import fs7 from "fs";
|
|
1990
|
+
import path6 from "path";
|
|
1540
1991
|
|
|
1541
1992
|
// src/search/vector.ts
|
|
1542
1993
|
function distanceToScore(distance) {
|
|
@@ -1577,7 +2028,15 @@ async function vectorSearch(db, embedder, query, limit, filters) {
|
|
|
1577
2028
|
|
|
1578
2029
|
// src/search/fts.ts
|
|
1579
2030
|
function sanitizeFtsQuery(query) {
|
|
1580
|
-
|
|
2031
|
+
const tokenized = query.replace(/[^A-Za-z0-9_*]+/g, " ").trim();
|
|
2032
|
+
if (tokenized.length === 0) return "";
|
|
2033
|
+
const sanitizedTerms = tokenized.split(/\s+/).map((term) => {
|
|
2034
|
+
const hasTrailingWildcard = /\*+$/.test(term);
|
|
2035
|
+
const base = term.replace(/\*/g, "");
|
|
2036
|
+
if (base.length === 0) return "";
|
|
2037
|
+
return hasTrailingWildcard ? `${base}*` : base;
|
|
2038
|
+
}).filter((term) => term.length > 0);
|
|
2039
|
+
return sanitizedTerms.join(" ");
|
|
1581
2040
|
}
|
|
1582
2041
|
function bm25ToScore(rank) {
|
|
1583
2042
|
return 1 / (1 + Math.abs(rank));
|
|
@@ -1722,27 +2181,56 @@ function pathKeywordSearch(db, query, limit) {
|
|
|
1722
2181
|
}
|
|
1723
2182
|
if (scoredPaths.length === 0) return [];
|
|
1724
2183
|
scoredPaths.sort((a, b) => b.score - a.score);
|
|
1725
|
-
const
|
|
2184
|
+
const matchedFiles = [];
|
|
1726
2185
|
for (const { filePath, score } of scoredPaths) {
|
|
1727
|
-
if (results.length >= limit) break;
|
|
1728
2186
|
const file = db.getFile(filePath);
|
|
1729
2187
|
if (!file) continue;
|
|
1730
2188
|
const chunks = db.getChunksByFile(file.id);
|
|
1731
|
-
|
|
2189
|
+
if (chunks.length === 0) continue;
|
|
2190
|
+
matchedFiles.push({
|
|
2191
|
+
filePath: file.path,
|
|
2192
|
+
language: file.language,
|
|
2193
|
+
score,
|
|
2194
|
+
chunks
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
if (matchedFiles.length === 0) return [];
|
|
2198
|
+
const results = [];
|
|
2199
|
+
const pushChunk = (filePath, language, score, chunk) => {
|
|
2200
|
+
results.push({
|
|
2201
|
+
chunkId: chunk.id,
|
|
2202
|
+
filePath,
|
|
2203
|
+
lineStart: chunk.lineStart,
|
|
2204
|
+
lineEnd: chunk.lineEnd,
|
|
2205
|
+
name: chunk.name,
|
|
2206
|
+
type: chunk.type,
|
|
2207
|
+
exported: chunk.exports,
|
|
2208
|
+
text: chunk.text,
|
|
2209
|
+
score,
|
|
2210
|
+
language
|
|
2211
|
+
});
|
|
2212
|
+
};
|
|
2213
|
+
for (const matched of matchedFiles) {
|
|
2214
|
+
if (results.length >= limit) break;
|
|
2215
|
+
pushChunk(
|
|
2216
|
+
matched.filePath,
|
|
2217
|
+
matched.language,
|
|
2218
|
+
matched.score,
|
|
2219
|
+
matched.chunks[0]
|
|
2220
|
+
);
|
|
2221
|
+
}
|
|
2222
|
+
let offset = 1;
|
|
2223
|
+
while (results.length < limit) {
|
|
2224
|
+
let addedInRound = false;
|
|
2225
|
+
for (const matched of matchedFiles) {
|
|
1732
2226
|
if (results.length >= limit) break;
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
lineEnd: chunk.lineEnd,
|
|
1738
|
-
name: chunk.name,
|
|
1739
|
-
type: chunk.type,
|
|
1740
|
-
exported: chunk.exports,
|
|
1741
|
-
text: chunk.text,
|
|
1742
|
-
score,
|
|
1743
|
-
language: file.language
|
|
1744
|
-
});
|
|
2227
|
+
const chunk = matched.chunks[offset];
|
|
2228
|
+
if (!chunk) continue;
|
|
2229
|
+
pushChunk(matched.filePath, matched.language, matched.score, chunk);
|
|
2230
|
+
addedInRound = true;
|
|
1745
2231
|
}
|
|
2232
|
+
if (!addedInRound) break;
|
|
2233
|
+
offset++;
|
|
1746
2234
|
}
|
|
1747
2235
|
return results;
|
|
1748
2236
|
}
|
|
@@ -1941,8 +2429,91 @@ function renormalize(results) {
|
|
|
1941
2429
|
}));
|
|
1942
2430
|
}
|
|
1943
2431
|
|
|
2432
|
+
// src/steering/classify.ts
|
|
2433
|
+
var SYMBOL_CAMEL_RE = /^[a-z][a-zA-Z0-9]*$/;
|
|
2434
|
+
var SYMBOL_PASCAL_RE = /^[A-Z][a-zA-Z0-9]*$/;
|
|
2435
|
+
var SYMBOL_SNAKE_RE = /^[a-z]+(?:_[a-z]+)+$/;
|
|
2436
|
+
var SYMBOL_UPPER_RE = /^[A-Z]+(?:_[A-Z]+)*$/;
|
|
2437
|
+
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;
|
|
2438
|
+
var QUESTION_WORDS = /* @__PURE__ */ new Set([
|
|
2439
|
+
"how",
|
|
2440
|
+
"what",
|
|
2441
|
+
"where",
|
|
2442
|
+
"why",
|
|
2443
|
+
"when",
|
|
2444
|
+
"which",
|
|
2445
|
+
"show",
|
|
2446
|
+
"explain",
|
|
2447
|
+
"find",
|
|
2448
|
+
"list"
|
|
2449
|
+
]);
|
|
2450
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
2451
|
+
"the",
|
|
2452
|
+
"a",
|
|
2453
|
+
"an",
|
|
2454
|
+
"is",
|
|
2455
|
+
"are",
|
|
2456
|
+
"was",
|
|
2457
|
+
"were",
|
|
2458
|
+
"do",
|
|
2459
|
+
"does",
|
|
2460
|
+
"did",
|
|
2461
|
+
"to",
|
|
2462
|
+
"for",
|
|
2463
|
+
"of",
|
|
2464
|
+
"in",
|
|
2465
|
+
"on",
|
|
2466
|
+
"with",
|
|
2467
|
+
"by",
|
|
2468
|
+
"and",
|
|
2469
|
+
"or"
|
|
2470
|
+
]);
|
|
2471
|
+
function defaultMultipliers() {
|
|
2472
|
+
return {
|
|
2473
|
+
vector: 1,
|
|
2474
|
+
fts: 1,
|
|
2475
|
+
ast: 1,
|
|
2476
|
+
path: 1,
|
|
2477
|
+
dependency: 1
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
function isSymbolQuery(query) {
|
|
2481
|
+
return SYMBOL_CAMEL_RE.test(query) || SYMBOL_PASCAL_RE.test(query) || SYMBOL_SNAKE_RE.test(query) || SYMBOL_UPPER_RE.test(query);
|
|
2482
|
+
}
|
|
2483
|
+
function isPathQuery(query) {
|
|
2484
|
+
return query.includes("/") || PATH_EXTENSION_RE.test(query);
|
|
2485
|
+
}
|
|
2486
|
+
function isNaturalLanguageQuery(query) {
|
|
2487
|
+
const lower = query.toLowerCase();
|
|
2488
|
+
const words = lower.split(/\s+/).filter((w) => w.length > 0);
|
|
2489
|
+
const hasQuestionWord = words.some((w) => QUESTION_WORDS.has(w));
|
|
2490
|
+
const hasStopWord = words.some((w) => STOP_WORDS.has(w));
|
|
2491
|
+
return hasQuestionWord || words.length >= 4 && hasStopWord;
|
|
2492
|
+
}
|
|
2493
|
+
function classifyQuery(query) {
|
|
2494
|
+
const trimmed = query.trim();
|
|
2495
|
+
const multipliers = defaultMultipliers();
|
|
2496
|
+
if (isPathQuery(trimmed)) {
|
|
2497
|
+
multipliers.path = 2;
|
|
2498
|
+
multipliers.ast = 0.5;
|
|
2499
|
+
return { kind: "path", multipliers };
|
|
2500
|
+
}
|
|
2501
|
+
if (isSymbolQuery(trimmed)) {
|
|
2502
|
+
multipliers.ast = 1.5;
|
|
2503
|
+
multipliers.vector = 0.5;
|
|
2504
|
+
return { kind: "symbol", multipliers };
|
|
2505
|
+
}
|
|
2506
|
+
if (isNaturalLanguageQuery(trimmed)) {
|
|
2507
|
+
multipliers.vector = 1.5;
|
|
2508
|
+
multipliers.path = 1.2;
|
|
2509
|
+
multipliers.ast = 0.7;
|
|
2510
|
+
return { kind: "natural_language", multipliers };
|
|
2511
|
+
}
|
|
2512
|
+
return { kind: "keyword", multipliers };
|
|
2513
|
+
}
|
|
2514
|
+
|
|
1944
2515
|
// src/cli/commands/query.ts
|
|
1945
|
-
var
|
|
2516
|
+
var CTX_DIR3 = ".ctx";
|
|
1946
2517
|
var DB_FILENAME2 = "index.db";
|
|
1947
2518
|
var SNIPPET_MAX_LENGTH = 200;
|
|
1948
2519
|
var STRATEGY_WEIGHTS = {
|
|
@@ -1952,6 +2523,26 @@ var STRATEGY_WEIGHTS = {
|
|
|
1952
2523
|
path: 0.7,
|
|
1953
2524
|
dependency: 0.6
|
|
1954
2525
|
};
|
|
2526
|
+
function getEffectiveStrategyWeights(query) {
|
|
2527
|
+
const { multipliers } = classifyQuery(query);
|
|
2528
|
+
return {
|
|
2529
|
+
vector: STRATEGY_WEIGHTS.vector * multipliers.vector,
|
|
2530
|
+
fts: STRATEGY_WEIGHTS.fts * multipliers.fts,
|
|
2531
|
+
ast: STRATEGY_WEIGHTS.ast * multipliers.ast,
|
|
2532
|
+
path: STRATEGY_WEIGHTS.path * multipliers.path,
|
|
2533
|
+
dependency: STRATEGY_WEIGHTS.dependency * multipliers.dependency
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
function normalizeLimit(limit) {
|
|
2537
|
+
if (!Number.isFinite(limit)) return 0;
|
|
2538
|
+
return Math.max(0, Math.trunc(limit));
|
|
2539
|
+
}
|
|
2540
|
+
function resolveQueryStrategies(query, strategies, source) {
|
|
2541
|
+
if (source !== "default") return strategies;
|
|
2542
|
+
if (classifyQuery(query).kind !== "natural_language") return strategies;
|
|
2543
|
+
if (strategies.includes("vector")) return strategies;
|
|
2544
|
+
return [...strategies, "vector"];
|
|
2545
|
+
}
|
|
1955
2546
|
function truncateSnippet(text) {
|
|
1956
2547
|
const oneLine = text.replace(/\n/g, " ").replace(/\s+/g, " ").trim();
|
|
1957
2548
|
if (oneLine.length <= SNIPPET_MAX_LENGTH) return oneLine;
|
|
@@ -1992,20 +2583,25 @@ function isPathLike(query) {
|
|
|
1992
2583
|
return query.includes("/") || query.includes("*") || query.includes(".");
|
|
1993
2584
|
}
|
|
1994
2585
|
async function runQuery(projectPath, query, options) {
|
|
1995
|
-
const
|
|
1996
|
-
const
|
|
1997
|
-
|
|
2586
|
+
const limit = normalizeLimit(options.limit);
|
|
2587
|
+
const absoluteRoot = path6.resolve(projectPath);
|
|
2588
|
+
const dbPath = path6.join(absoluteRoot, CTX_DIR3, DB_FILENAME2);
|
|
2589
|
+
if (!fs7.existsSync(dbPath)) {
|
|
1998
2590
|
throw new KontextError(
|
|
1999
|
-
`Project not initialized. Run "ctx init" first. (${
|
|
2591
|
+
`Project not initialized. Run "ctx init" first. (${CTX_DIR3}/${DB_FILENAME2} not found)`,
|
|
2000
2592
|
ErrorCode.NOT_INITIALIZED
|
|
2001
2593
|
);
|
|
2002
2594
|
}
|
|
2003
2595
|
const start = performance.now();
|
|
2004
|
-
const
|
|
2596
|
+
const embedderConfig = getProjectEmbedderConfig(absoluteRoot);
|
|
2597
|
+
const db = createDatabase(dbPath, embedderConfig.dimensions);
|
|
2005
2598
|
try {
|
|
2006
|
-
const strategyResults = await runStrategies(db, query,
|
|
2599
|
+
const strategyResults = await runStrategies(db, absoluteRoot, query, {
|
|
2600
|
+
...options,
|
|
2601
|
+
limit
|
|
2602
|
+
});
|
|
2007
2603
|
const pathBoostTerms = extractPathBoostTerms(query);
|
|
2008
|
-
const fused = fusionMergeWithPathBoost(strategyResults,
|
|
2604
|
+
const fused = fusionMergeWithPathBoost(strategyResults, limit, pathBoostTerms);
|
|
2009
2605
|
const outputResults = fused.map(toOutputResult);
|
|
2010
2606
|
const searchTimeMs = Math.round(performance.now() - start);
|
|
2011
2607
|
const text = options.format === "text" ? formatTextOutput(query, outputResults) : void 0;
|
|
@@ -2023,14 +2619,16 @@ async function runQuery(projectPath, query, options) {
|
|
|
2023
2619
|
db.close();
|
|
2024
2620
|
}
|
|
2025
2621
|
}
|
|
2026
|
-
async function runStrategies(db, query, options) {
|
|
2622
|
+
async function runStrategies(db, projectPath, query, options) {
|
|
2027
2623
|
const results = [];
|
|
2028
2624
|
const filters = options.language ? { language: options.language } : void 0;
|
|
2029
2625
|
const limit = options.limit * 3;
|
|
2626
|
+
const effectiveWeights = getEffectiveStrategyWeights(query);
|
|
2030
2627
|
for (const strategy of options.strategies) {
|
|
2031
|
-
const weight =
|
|
2628
|
+
const weight = effectiveWeights[strategy];
|
|
2032
2629
|
const searchResults = await executeStrategy(
|
|
2033
2630
|
db,
|
|
2631
|
+
projectPath,
|
|
2034
2632
|
strategy,
|
|
2035
2633
|
query,
|
|
2036
2634
|
limit,
|
|
@@ -2042,10 +2640,10 @@ async function runStrategies(db, query, options) {
|
|
|
2042
2640
|
}
|
|
2043
2641
|
return results;
|
|
2044
2642
|
}
|
|
2045
|
-
async function executeStrategy(db, strategy, query, limit, filters) {
|
|
2643
|
+
async function executeStrategy(db, projectPath, strategy, query, limit, filters) {
|
|
2046
2644
|
switch (strategy) {
|
|
2047
2645
|
case "vector": {
|
|
2048
|
-
const embedder = await loadEmbedder();
|
|
2646
|
+
const embedder = await loadEmbedder(projectPath);
|
|
2049
2647
|
return vectorSearch(db, embedder, query, limit, filters);
|
|
2050
2648
|
}
|
|
2051
2649
|
case "fts":
|
|
@@ -2078,24 +2676,38 @@ async function executeStrategy(db, strategy, query, limit, filters) {
|
|
|
2078
2676
|
}
|
|
2079
2677
|
}
|
|
2080
2678
|
var embedderInstance = null;
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2679
|
+
var embedderKey = null;
|
|
2680
|
+
function getCacheKey(projectPath) {
|
|
2681
|
+
const config = getProjectEmbedderConfig(projectPath);
|
|
2682
|
+
return `${projectPath}:${config.provider}:${config.model}:${config.dimensions}`;
|
|
2683
|
+
}
|
|
2684
|
+
async function loadEmbedder(projectPath) {
|
|
2685
|
+
const cacheKey = getCacheKey(projectPath);
|
|
2686
|
+
if (embedderInstance && embedderKey === cacheKey) return embedderInstance;
|
|
2687
|
+
embedderInstance = await createProjectEmbedder(projectPath);
|
|
2688
|
+
embedderKey = cacheKey;
|
|
2084
2689
|
return embedderInstance;
|
|
2085
2690
|
}
|
|
2086
2691
|
function registerQueryCommand(program2) {
|
|
2087
|
-
program2.command("query <query>").description("Multi-strategy code search").option("-l, --limit <n>", "Max results", "10").option(
|
|
2692
|
+
program2.command("query <query>").alias("find").description("Multi-strategy code search").option("-l, --limit <n>", "Max results", "10").option(
|
|
2088
2693
|
"-s, --strategy <list>",
|
|
2089
2694
|
"Comma-separated strategies: vector,fts,ast,path",
|
|
2090
2695
|
"fts,ast,path"
|
|
2091
|
-
).option("--language <lang>", "Filter by language").option("-f, --format <fmt>", "Output format: json|text", "json").option("--no-vectors", "Skip vector search").action(async (query, opts) => {
|
|
2696
|
+
).option("--language <lang>", "Filter by language").option("-f, --format <fmt>", "Output format: json|text", "json").option("--no-vectors", "Skip vector search").action(async (query, opts, command) => {
|
|
2092
2697
|
const projectPath = process.cwd();
|
|
2093
2698
|
const verbose = program2.opts()["verbose"] === true;
|
|
2094
2699
|
const logger = createLogger({ level: verbose ? LogLevel.DEBUG : LogLevel.INFO });
|
|
2095
|
-
const
|
|
2700
|
+
const strategySource = command.getOptionValueSource("strategy");
|
|
2701
|
+
const parsedStrategies = (opts["strategy"] ?? "fts,ast,path").split(",").map((s) => s.trim());
|
|
2702
|
+
const strategies = resolveQueryStrategies(
|
|
2703
|
+
query,
|
|
2704
|
+
parsedStrategies,
|
|
2705
|
+
strategySource === "default" ? "default" : strategySource ? "cli" : "unknown"
|
|
2706
|
+
);
|
|
2707
|
+
const limit = normalizeLimit(parseInt(opts["limit"] ?? "10", 10));
|
|
2096
2708
|
try {
|
|
2097
2709
|
const output = await runQuery(projectPath, query, {
|
|
2098
|
-
limit
|
|
2710
|
+
limit,
|
|
2099
2711
|
strategies,
|
|
2100
2712
|
language: opts["language"],
|
|
2101
2713
|
format: opts["format"] ?? "json"
|
|
@@ -2117,8 +2729,8 @@ function registerQueryCommand(program2) {
|
|
|
2117
2729
|
}
|
|
2118
2730
|
|
|
2119
2731
|
// src/cli/commands/ask.ts
|
|
2120
|
-
import
|
|
2121
|
-
import
|
|
2732
|
+
import fs8 from "fs";
|
|
2733
|
+
import path7 from "path";
|
|
2122
2734
|
|
|
2123
2735
|
// src/steering/prompts.ts
|
|
2124
2736
|
var PLAN_SYSTEM_PROMPT = `You are a code-search strategy planner for a TypeScript/JavaScript codebase.
|
|
@@ -2348,7 +2960,7 @@ function createAnthropicProvider(apiKey) {
|
|
|
2348
2960
|
}
|
|
2349
2961
|
};
|
|
2350
2962
|
}
|
|
2351
|
-
var
|
|
2963
|
+
var STOP_WORDS2 = /* @__PURE__ */ new Set([
|
|
2352
2964
|
// Interrogatives & conjunctions
|
|
2353
2965
|
"how",
|
|
2354
2966
|
"does",
|
|
@@ -2459,6 +3071,83 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
2459
3071
|
]);
|
|
2460
3072
|
var CODE_IDENT_RE = /^(?:[a-z]+(?:[A-Z][a-z]*)+|[A-Z][a-zA-Z]+|[a-z]+(?:_[a-z]+)+|[A-Z]+(?:_[A-Z]+)+)$/;
|
|
2461
3073
|
var DOTTED_IDENT_RE = /[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)+/g;
|
|
3074
|
+
var COMMON_STEMS = {
|
|
3075
|
+
authentication: "auth",
|
|
3076
|
+
authorization: "auth",
|
|
3077
|
+
configuration: "config",
|
|
3078
|
+
initialization: "init",
|
|
3079
|
+
initialize: "init",
|
|
3080
|
+
initializing: "init",
|
|
3081
|
+
implementation: "impl",
|
|
3082
|
+
implements: "impl",
|
|
3083
|
+
implementing: "impl",
|
|
3084
|
+
dependency: "dep",
|
|
3085
|
+
dependencies: "dep",
|
|
3086
|
+
middleware: "middleware",
|
|
3087
|
+
validation: "valid",
|
|
3088
|
+
validator: "valid",
|
|
3089
|
+
serialize: "serial",
|
|
3090
|
+
serialization: "serial",
|
|
3091
|
+
deserialize: "deserial",
|
|
3092
|
+
database: "db",
|
|
3093
|
+
logging: "log",
|
|
3094
|
+
logger: "log",
|
|
3095
|
+
testing: "test",
|
|
3096
|
+
handler: "handle",
|
|
3097
|
+
handling: "handle",
|
|
3098
|
+
callback: "callback",
|
|
3099
|
+
subscriber: "subscribe",
|
|
3100
|
+
subscription: "subscribe",
|
|
3101
|
+
rendering: "render",
|
|
3102
|
+
renderer: "render",
|
|
3103
|
+
transformer: "transform",
|
|
3104
|
+
transformation: "transform",
|
|
3105
|
+
connection: "connect",
|
|
3106
|
+
connecting: "connect",
|
|
3107
|
+
connector: "connect",
|
|
3108
|
+
migrating: "migrate",
|
|
3109
|
+
migration: "migrate",
|
|
3110
|
+
scheduling: "schedule",
|
|
3111
|
+
scheduler: "schedule",
|
|
3112
|
+
parsing: "parse",
|
|
3113
|
+
parser: "parse",
|
|
3114
|
+
routing: "route",
|
|
3115
|
+
router: "route",
|
|
3116
|
+
indexing: "index",
|
|
3117
|
+
indexer: "index",
|
|
3118
|
+
subscribing: "subscribe"
|
|
3119
|
+
};
|
|
3120
|
+
var STEM_SUFFIXES = [
|
|
3121
|
+
"tion",
|
|
3122
|
+
"sion",
|
|
3123
|
+
"ment",
|
|
3124
|
+
"ness",
|
|
3125
|
+
"ing",
|
|
3126
|
+
"er",
|
|
3127
|
+
"or",
|
|
3128
|
+
"able",
|
|
3129
|
+
"ible",
|
|
3130
|
+
"ity",
|
|
3131
|
+
"ous",
|
|
3132
|
+
"ive",
|
|
3133
|
+
"ful",
|
|
3134
|
+
"less",
|
|
3135
|
+
"ly"
|
|
3136
|
+
];
|
|
3137
|
+
function getStemVariant(term) {
|
|
3138
|
+
const lower = term.toLowerCase();
|
|
3139
|
+
const mapped = COMMON_STEMS[lower];
|
|
3140
|
+
if (mapped && mapped !== lower) return mapped;
|
|
3141
|
+
if (!/^[a-z][a-z0-9_]*$/.test(lower)) return null;
|
|
3142
|
+
for (const suffix of STEM_SUFFIXES) {
|
|
3143
|
+
if (!lower.endsWith(suffix)) continue;
|
|
3144
|
+
const stem = lower.slice(0, -suffix.length);
|
|
3145
|
+
if (stem.length >= 4 && stem !== lower) {
|
|
3146
|
+
return stem;
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
return null;
|
|
3150
|
+
}
|
|
2462
3151
|
function extractSearchTerms(query) {
|
|
2463
3152
|
const terms = [];
|
|
2464
3153
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2469,16 +3158,23 @@ function extractSearchTerms(query) {
|
|
|
2469
3158
|
terms.push(term);
|
|
2470
3159
|
}
|
|
2471
3160
|
};
|
|
3161
|
+
const addTermAndVariants = (term) => {
|
|
3162
|
+
addUnique(term);
|
|
3163
|
+
const variant = getStemVariant(term);
|
|
3164
|
+
if (variant && variant !== term.toLowerCase()) {
|
|
3165
|
+
addUnique(variant);
|
|
3166
|
+
}
|
|
3167
|
+
};
|
|
2472
3168
|
const dottedMatches = query.match(DOTTED_IDENT_RE) ?? [];
|
|
2473
|
-
for (const m of dottedMatches)
|
|
3169
|
+
for (const m of dottedMatches) addTermAndVariants(m);
|
|
2474
3170
|
const pathTokens = query.split(/\s+/).filter((t) => t.includes("/"));
|
|
2475
|
-
for (const p of pathTokens)
|
|
3171
|
+
for (const p of pathTokens) addTermAndVariants(p.replace(/[?!,;]+$/g, ""));
|
|
2476
3172
|
const words = query.replace(/[^a-zA-Z0-9_.\s/-]/g, " ").split(/\s+/).filter((w) => w.length >= 2);
|
|
2477
3173
|
for (const w of words) {
|
|
2478
3174
|
const lower = w.toLowerCase();
|
|
2479
3175
|
if (seen.has(lower)) continue;
|
|
2480
|
-
if (
|
|
2481
|
-
|
|
3176
|
+
if (STOP_WORDS2.has(lower) && !CODE_IDENT_RE.test(w)) continue;
|
|
3177
|
+
addTermAndVariants(w);
|
|
2482
3178
|
}
|
|
2483
3179
|
if (terms.length === 0) {
|
|
2484
3180
|
const allWords = query.replace(/[^a-zA-Z0-9_\s]/g, " ").split(/\s+/).filter((w) => w.length >= 2);
|
|
@@ -2495,17 +3191,42 @@ var VALID_STRATEGIES = /* @__PURE__ */ new Set([
|
|
|
2495
3191
|
"dependency"
|
|
2496
3192
|
]);
|
|
2497
3193
|
function buildFallbackPlan(query) {
|
|
2498
|
-
const
|
|
2499
|
-
const strategies = [
|
|
2500
|
-
{ strategy: "fts", query: keywords, weight: 0.8, reason: "Full-text keyword search" },
|
|
2501
|
-
{ strategy: "ast", query: keywords, weight: 0.9, reason: "Structural symbol search" },
|
|
2502
|
-
{ strategy: "path", query: keywords, weight: 0.7, reason: "Path keyword search" }
|
|
2503
|
-
];
|
|
3194
|
+
const strategies = buildFallbackStrategies(query);
|
|
2504
3195
|
return {
|
|
2505
3196
|
interpretation: `Searching for: ${query}`,
|
|
2506
3197
|
strategies
|
|
2507
3198
|
};
|
|
2508
3199
|
}
|
|
3200
|
+
function buildFallbackStrategies(query) {
|
|
3201
|
+
const keywords = extractSearchTerms(query);
|
|
3202
|
+
const { multipliers } = classifyQuery(query);
|
|
3203
|
+
return [
|
|
3204
|
+
{
|
|
3205
|
+
strategy: "vector",
|
|
3206
|
+
query,
|
|
3207
|
+
weight: 1 * multipliers.vector,
|
|
3208
|
+
reason: "Semantic search over natural language intent"
|
|
3209
|
+
},
|
|
3210
|
+
{
|
|
3211
|
+
strategy: "fts",
|
|
3212
|
+
query: keywords,
|
|
3213
|
+
weight: 0.8 * multipliers.fts,
|
|
3214
|
+
reason: "Full-text keyword search"
|
|
3215
|
+
},
|
|
3216
|
+
{
|
|
3217
|
+
strategy: "ast",
|
|
3218
|
+
query: keywords,
|
|
3219
|
+
weight: 0.9 * multipliers.ast,
|
|
3220
|
+
reason: "Structural symbol search"
|
|
3221
|
+
},
|
|
3222
|
+
{
|
|
3223
|
+
strategy: "path",
|
|
3224
|
+
query: keywords,
|
|
3225
|
+
weight: 0.7 * multipliers.path,
|
|
3226
|
+
reason: "Path keyword search"
|
|
3227
|
+
}
|
|
3228
|
+
];
|
|
3229
|
+
}
|
|
2509
3230
|
function parseSearchPlan(raw, query) {
|
|
2510
3231
|
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
|
2511
3232
|
if (!jsonMatch) return buildFallbackPlan(query);
|
|
@@ -2585,10 +3306,14 @@ async function steer(provider, query, limit, searchExecutor) {
|
|
|
2585
3306
|
}
|
|
2586
3307
|
|
|
2587
3308
|
// src/cli/commands/ask.ts
|
|
2588
|
-
var
|
|
3309
|
+
var CTX_DIR4 = ".ctx";
|
|
2589
3310
|
var DB_FILENAME3 = "index.db";
|
|
2590
3311
|
var SNIPPET_MAX_LENGTH2 = 200;
|
|
2591
3312
|
var FALLBACK_NOTICE = "No LLM provider configured. Set CTX_GEMINI_KEY, CTX_OPENAI_KEY, or CTX_ANTHROPIC_KEY. Running basic search instead.";
|
|
3313
|
+
function normalizeLimit2(limit) {
|
|
3314
|
+
if (!Number.isFinite(limit)) return 0;
|
|
3315
|
+
return Math.max(0, Math.trunc(limit));
|
|
3316
|
+
}
|
|
2592
3317
|
var PROVIDER_ENV_MAP = {
|
|
2593
3318
|
gemini: "CTX_GEMINI_KEY",
|
|
2594
3319
|
openai: "CTX_OPENAI_KEY",
|
|
@@ -2670,13 +3395,13 @@ function formatTextOutput2(output) {
|
|
|
2670
3395
|
);
|
|
2671
3396
|
return lines.join("\n");
|
|
2672
3397
|
}
|
|
2673
|
-
function createSearchExecutor(db, query) {
|
|
3398
|
+
function createSearchExecutor(db, projectPath, query) {
|
|
2674
3399
|
const pathBoostTerms = extractPathBoostTerms(query);
|
|
2675
3400
|
return async (strategies, limit) => {
|
|
2676
3401
|
const strategyResults = [];
|
|
2677
3402
|
const fetchLimit = limit * 3;
|
|
2678
3403
|
for (const plan of strategies) {
|
|
2679
|
-
const results = await executeStrategy2(db, plan, fetchLimit);
|
|
3404
|
+
const results = await executeStrategy2(db, projectPath, plan, fetchLimit);
|
|
2680
3405
|
if (results.length > 0) {
|
|
2681
3406
|
strategyResults.push({
|
|
2682
3407
|
strategy: plan.strategy,
|
|
@@ -2695,10 +3420,10 @@ function extractSymbolNames2(query) {
|
|
|
2695
3420
|
function isPathLike2(query) {
|
|
2696
3421
|
return query.includes("/") || query.includes("*") || query.includes(".");
|
|
2697
3422
|
}
|
|
2698
|
-
async function executeStrategy2(db, plan, limit) {
|
|
3423
|
+
async function executeStrategy2(db, projectPath, plan, limit) {
|
|
2699
3424
|
switch (plan.strategy) {
|
|
2700
3425
|
case "vector": {
|
|
2701
|
-
const embedder = await loadEmbedder2();
|
|
3426
|
+
const embedder = await loadEmbedder2(projectPath);
|
|
2702
3427
|
return vectorSearch(db, embedder, plan.query, limit);
|
|
2703
3428
|
}
|
|
2704
3429
|
case "fts":
|
|
@@ -2727,19 +3452,21 @@ async function executeStrategy2(db, plan, limit) {
|
|
|
2727
3452
|
}
|
|
2728
3453
|
}
|
|
2729
3454
|
var embedderInstance2 = null;
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
3455
|
+
var embedderKey2 = null;
|
|
3456
|
+
function getCacheKey2(projectPath) {
|
|
3457
|
+
const config = getProjectEmbedderConfig(projectPath);
|
|
3458
|
+
return `${projectPath}:${config.provider}:${config.model}:${config.dimensions}`;
|
|
3459
|
+
}
|
|
3460
|
+
async function loadEmbedder2(projectPath) {
|
|
3461
|
+
const cacheKey = getCacheKey2(projectPath);
|
|
3462
|
+
if (embedderInstance2 && embedderKey2 === cacheKey) return embedderInstance2;
|
|
3463
|
+
embedderInstance2 = await createProjectEmbedder(projectPath);
|
|
3464
|
+
embedderKey2 = cacheKey;
|
|
2733
3465
|
return embedderInstance2;
|
|
2734
3466
|
}
|
|
2735
|
-
async function fallbackSearch(db, query, limit) {
|
|
2736
|
-
const executor = createSearchExecutor(db, query);
|
|
2737
|
-
const
|
|
2738
|
-
const fallbackStrategies = [
|
|
2739
|
-
{ strategy: "fts", query: keywords, weight: 0.8, reason: "fallback keyword search" },
|
|
2740
|
-
{ strategy: "ast", query: keywords, weight: 0.9, reason: "fallback structural search" },
|
|
2741
|
-
{ strategy: "path", query: keywords, weight: 0.7, reason: "fallback path search" }
|
|
2742
|
-
];
|
|
3467
|
+
async function fallbackSearch(db, projectPath, query, limit) {
|
|
3468
|
+
const executor = createSearchExecutor(db, projectPath, query);
|
|
3469
|
+
const fallbackStrategies = buildFallbackStrategies(query);
|
|
2743
3470
|
const results = await executor(fallbackStrategies, limit);
|
|
2744
3471
|
return {
|
|
2745
3472
|
query,
|
|
@@ -2756,37 +3483,39 @@ async function fallbackSearch(db, query, limit) {
|
|
|
2756
3483
|
};
|
|
2757
3484
|
}
|
|
2758
3485
|
async function runAsk(projectPath, query, options) {
|
|
2759
|
-
const
|
|
2760
|
-
const
|
|
2761
|
-
|
|
3486
|
+
const limit = normalizeLimit2(options.limit);
|
|
3487
|
+
const absoluteRoot = path7.resolve(projectPath);
|
|
3488
|
+
const dbPath = path7.join(absoluteRoot, CTX_DIR4, DB_FILENAME3);
|
|
3489
|
+
if (!fs8.existsSync(dbPath)) {
|
|
2762
3490
|
throw new KontextError(
|
|
2763
|
-
`Project not initialized. Run "ctx init" first. (${
|
|
3491
|
+
`Project not initialized. Run "ctx init" first. (${CTX_DIR4}/${DB_FILENAME3} not found)`,
|
|
2764
3492
|
ErrorCode.NOT_INITIALIZED
|
|
2765
3493
|
);
|
|
2766
3494
|
}
|
|
2767
|
-
const
|
|
3495
|
+
const embedderConfig = getProjectEmbedderConfig(absoluteRoot);
|
|
3496
|
+
const db = createDatabase(dbPath, embedderConfig.dimensions);
|
|
2768
3497
|
try {
|
|
2769
3498
|
const provider = options.provider ?? null;
|
|
2770
3499
|
if (!provider) {
|
|
2771
|
-
const output = await fallbackSearch(db, query,
|
|
3500
|
+
const output = await fallbackSearch(db, absoluteRoot, query, limit);
|
|
2772
3501
|
output.warning = FALLBACK_NOTICE;
|
|
2773
3502
|
if (options.format === "text") {
|
|
2774
3503
|
output.text = formatTextOutput2(output);
|
|
2775
3504
|
}
|
|
2776
3505
|
return output;
|
|
2777
3506
|
}
|
|
2778
|
-
const executor = createSearchExecutor(db, query);
|
|
3507
|
+
const executor = createSearchExecutor(db, absoluteRoot, query);
|
|
2779
3508
|
if (options.noExplain) {
|
|
2780
|
-
return await runNoExplain(provider, query, options, executor);
|
|
3509
|
+
return await runNoExplain(provider, query, limit, options, executor);
|
|
2781
3510
|
}
|
|
2782
|
-
return await runWithSteering(provider, query, options, executor);
|
|
3511
|
+
return await runWithSteering(provider, query, limit, options, executor);
|
|
2783
3512
|
} finally {
|
|
2784
3513
|
db.close();
|
|
2785
3514
|
}
|
|
2786
3515
|
}
|
|
2787
|
-
async function runNoExplain(provider, query, options, executor) {
|
|
3516
|
+
async function runNoExplain(provider, query, limit, options, executor) {
|
|
2788
3517
|
const plan = await planSearch(provider, query);
|
|
2789
|
-
const results = await executor(plan.strategies,
|
|
3518
|
+
const results = await executor(plan.strategies, limit);
|
|
2790
3519
|
const output = {
|
|
2791
3520
|
query,
|
|
2792
3521
|
interpretation: plan.interpretation,
|
|
@@ -2804,8 +3533,8 @@ async function runNoExplain(provider, query, options, executor) {
|
|
|
2804
3533
|
}
|
|
2805
3534
|
return output;
|
|
2806
3535
|
}
|
|
2807
|
-
async function runWithSteering(provider, query, options, executor) {
|
|
2808
|
-
const result = await steer(provider, query,
|
|
3536
|
+
async function runWithSteering(provider, query, limit, options, executor) {
|
|
3537
|
+
const result = await steer(provider, query, limit, executor);
|
|
2809
3538
|
const output = {
|
|
2810
3539
|
query,
|
|
2811
3540
|
interpretation: result.interpretation,
|
|
@@ -2830,9 +3559,10 @@ function registerAskCommand(program2) {
|
|
|
2830
3559
|
const logger = createLogger({ level: verbose ? LogLevel.DEBUG : LogLevel.INFO });
|
|
2831
3560
|
const providerName = opts["provider"];
|
|
2832
3561
|
const provider = detectProvider(providerName);
|
|
3562
|
+
const limit = normalizeLimit2(parseInt(String(opts["limit"] ?? "10"), 10));
|
|
2833
3563
|
try {
|
|
2834
3564
|
const output = await runAsk(projectPath, query, {
|
|
2835
|
-
limit
|
|
3565
|
+
limit,
|
|
2836
3566
|
format: opts["format"] ?? "text",
|
|
2837
3567
|
provider: provider ?? void 0,
|
|
2838
3568
|
noExplain: opts["explain"] === false
|
|
@@ -2856,13 +3586,6 @@ function registerAskCommand(program2) {
|
|
|
2856
3586
|
});
|
|
2857
3587
|
}
|
|
2858
3588
|
|
|
2859
|
-
// src/cli/commands/find.ts
|
|
2860
|
-
function registerFindCommand(program2) {
|
|
2861
|
-
program2.command("find <query>").description("Natural language code search").option("--full", "Include source code in output").option("--json", "Machine-readable JSON output").option("--no-llm", "Skip steering LLM, raw vector search only").option("-l, --limit <n>", "Max results", "5").option("--language <lang>", "Filter by language").action((_query, _options) => {
|
|
2862
|
-
console.log("ctx find \u2014 not yet implemented");
|
|
2863
|
-
});
|
|
2864
|
-
}
|
|
2865
|
-
|
|
2866
3589
|
// src/cli/commands/update.ts
|
|
2867
3590
|
function registerUpdateCommand(program2) {
|
|
2868
3591
|
program2.command("update").description("Incremental re-index of changed files").action(() => {
|
|
@@ -2871,12 +3594,12 @@ function registerUpdateCommand(program2) {
|
|
|
2871
3594
|
}
|
|
2872
3595
|
|
|
2873
3596
|
// src/cli/commands/watch.ts
|
|
2874
|
-
import
|
|
2875
|
-
import
|
|
3597
|
+
import fs9 from "fs";
|
|
3598
|
+
import path9 from "path";
|
|
2876
3599
|
|
|
2877
3600
|
// src/watcher/watcher.ts
|
|
2878
3601
|
import { watch } from "chokidar";
|
|
2879
|
-
import
|
|
3602
|
+
import path8 from "path";
|
|
2880
3603
|
var DEFAULT_DEBOUNCE_MS = 500;
|
|
2881
3604
|
var ALWAYS_IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
2882
3605
|
"node_modules",
|
|
@@ -2888,15 +3611,15 @@ var ALWAYS_IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
2888
3611
|
]);
|
|
2889
3612
|
var WATCHED_EXTENSIONS = new Set(Object.keys(LANGUAGE_MAP));
|
|
2890
3613
|
function isWatchedFile(filePath) {
|
|
2891
|
-
const ext =
|
|
3614
|
+
const ext = path8.extname(filePath).toLowerCase();
|
|
2892
3615
|
return WATCHED_EXTENSIONS.has(ext);
|
|
2893
3616
|
}
|
|
2894
3617
|
function createWatcher(options, events) {
|
|
2895
3618
|
const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
2896
|
-
const projectPath =
|
|
3619
|
+
const projectPath = path8.resolve(options.projectPath);
|
|
2897
3620
|
const extraIgnored = new Set(options.ignored ?? []);
|
|
2898
3621
|
function isIgnored(filePath) {
|
|
2899
|
-
const segments = filePath.split(
|
|
3622
|
+
const segments = filePath.split(path8.sep);
|
|
2900
3623
|
for (const seg of segments) {
|
|
2901
3624
|
if (ALWAYS_IGNORED_DIRS.has(seg)) return true;
|
|
2902
3625
|
if (extraIgnored.has(seg)) return true;
|
|
@@ -2954,13 +3677,13 @@ function createWatcher(options, events) {
|
|
|
2954
3677
|
}
|
|
2955
3678
|
|
|
2956
3679
|
// src/cli/commands/watch.ts
|
|
2957
|
-
var
|
|
3680
|
+
var CTX_DIR5 = ".ctx";
|
|
2958
3681
|
var DB_FILENAME4 = "index.db";
|
|
2959
3682
|
function timestamp() {
|
|
2960
3683
|
return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false });
|
|
2961
3684
|
}
|
|
2962
3685
|
function detectLanguage(filePath) {
|
|
2963
|
-
const ext =
|
|
3686
|
+
const ext = path9.extname(filePath).toLowerCase();
|
|
2964
3687
|
return LANGUAGE_MAP[ext] ?? null;
|
|
2965
3688
|
}
|
|
2966
3689
|
function formatDuration2(ms) {
|
|
@@ -2969,7 +3692,7 @@ function formatDuration2(ms) {
|
|
|
2969
3692
|
}
|
|
2970
3693
|
async function hashFile(absolutePath) {
|
|
2971
3694
|
const { createHash: createHash3 } = await import("crypto");
|
|
2972
|
-
const content =
|
|
3695
|
+
const content = fs9.readFileSync(absolutePath);
|
|
2973
3696
|
return createHash3("sha256").update(content).digest("hex");
|
|
2974
3697
|
}
|
|
2975
3698
|
async function reindexChanges(db, changes, projectPath, options) {
|
|
@@ -2979,25 +3702,21 @@ async function reindexChanges(db, changes, projectPath, options) {
|
|
|
2979
3702
|
let chunksUpdated = 0;
|
|
2980
3703
|
const allChunksWithMeta = [];
|
|
2981
3704
|
for (const change of changes) {
|
|
2982
|
-
const absolutePath =
|
|
3705
|
+
const absolutePath = path9.join(projectPath, change.path);
|
|
2983
3706
|
const language = detectLanguage(change.path);
|
|
2984
3707
|
if (change.type === "unlink") {
|
|
2985
3708
|
log(`[${timestamp()}] Deleted: ${change.path}`);
|
|
2986
|
-
const
|
|
2987
|
-
if (
|
|
3709
|
+
const existingFile = db.getFile(change.path);
|
|
3710
|
+
if (existingFile) {
|
|
2988
3711
|
db.deleteFile(change.path);
|
|
2989
3712
|
}
|
|
2990
3713
|
filesProcessed++;
|
|
2991
3714
|
continue;
|
|
2992
3715
|
}
|
|
2993
3716
|
if (!language) continue;
|
|
2994
|
-
if (!
|
|
3717
|
+
if (!fs9.existsSync(absolutePath)) continue;
|
|
2995
3718
|
const label = change.type === "add" ? "Added" : "Changed";
|
|
2996
3719
|
log(`[${timestamp()}] ${label}: ${change.path}`);
|
|
2997
|
-
const existingFile = db.getFile(change.path);
|
|
2998
|
-
if (existingFile) {
|
|
2999
|
-
db.deleteChunksByFile(existingFile.id);
|
|
3000
|
-
}
|
|
3001
3720
|
let nodes;
|
|
3002
3721
|
try {
|
|
3003
3722
|
nodes = await parseFile(absolutePath, language);
|
|
@@ -3007,27 +3726,32 @@ async function reindexChanges(db, changes, projectPath, options) {
|
|
|
3007
3726
|
}
|
|
3008
3727
|
const chunks = chunkFile(nodes, change.path);
|
|
3009
3728
|
const hash = await hashFile(absolutePath);
|
|
3010
|
-
const size =
|
|
3011
|
-
const
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3729
|
+
const size = fs9.statSync(absolutePath).size;
|
|
3730
|
+
const chunkRows = chunks.map((c) => ({
|
|
3731
|
+
lineStart: c.lineStart,
|
|
3732
|
+
lineEnd: c.lineEnd,
|
|
3733
|
+
type: c.type,
|
|
3734
|
+
name: c.name,
|
|
3735
|
+
parent: c.parent,
|
|
3736
|
+
text: c.text,
|
|
3737
|
+
imports: c.imports,
|
|
3738
|
+
exports: c.exports,
|
|
3739
|
+
hash: c.hash
|
|
3740
|
+
}));
|
|
3741
|
+
let chunkIds = [];
|
|
3742
|
+
db.transaction(() => {
|
|
3743
|
+
const existingFile = db.getFile(change.path);
|
|
3744
|
+
if (existingFile) {
|
|
3745
|
+
db.deleteChunksByFile(existingFile.id);
|
|
3746
|
+
}
|
|
3747
|
+
const fileId = db.upsertFile({
|
|
3748
|
+
path: change.path,
|
|
3749
|
+
language,
|
|
3750
|
+
hash,
|
|
3751
|
+
size
|
|
3752
|
+
});
|
|
3753
|
+
chunkIds = db.insertChunks(fileId, chunkRows);
|
|
3016
3754
|
});
|
|
3017
|
-
const chunkIds = db.insertChunks(
|
|
3018
|
-
fileId,
|
|
3019
|
-
chunks.map((c) => ({
|
|
3020
|
-
lineStart: c.lineStart,
|
|
3021
|
-
lineEnd: c.lineEnd,
|
|
3022
|
-
type: c.type,
|
|
3023
|
-
name: c.name,
|
|
3024
|
-
parent: c.parent,
|
|
3025
|
-
text: c.text,
|
|
3026
|
-
imports: c.imports,
|
|
3027
|
-
exports: c.exports,
|
|
3028
|
-
hash: c.hash
|
|
3029
|
-
}))
|
|
3030
|
-
);
|
|
3031
3755
|
for (let i = 0; i < chunks.length; i++) {
|
|
3032
3756
|
allChunksWithMeta.push({
|
|
3033
3757
|
fileRelPath: change.path,
|
|
@@ -3038,7 +3762,7 @@ async function reindexChanges(db, changes, projectPath, options) {
|
|
|
3038
3762
|
filesProcessed++;
|
|
3039
3763
|
}
|
|
3040
3764
|
if (!options.skipEmbedding && allChunksWithMeta.length > 0) {
|
|
3041
|
-
const embedder = await loadEmbedder3();
|
|
3765
|
+
const embedder = await loadEmbedder3(projectPath);
|
|
3042
3766
|
const texts = allChunksWithMeta.map(
|
|
3043
3767
|
(cm) => prepareChunkText(cm.fileRelPath, cm.chunk.parent, cm.chunk.text)
|
|
3044
3768
|
);
|
|
@@ -3054,27 +3778,36 @@ async function reindexChanges(db, changes, projectPath, options) {
|
|
|
3054
3778
|
return { filesProcessed, chunksUpdated, durationMs };
|
|
3055
3779
|
}
|
|
3056
3780
|
var embedderInstance3 = null;
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3781
|
+
var embedderKey3 = null;
|
|
3782
|
+
function getCacheKey3(projectPath) {
|
|
3783
|
+
const config = getProjectEmbedderConfig(projectPath);
|
|
3784
|
+
return `${projectPath}:${config.provider}:${config.model}:${config.dimensions}`;
|
|
3785
|
+
}
|
|
3786
|
+
async function loadEmbedder3(projectPath) {
|
|
3787
|
+
const cacheKey = getCacheKey3(projectPath);
|
|
3788
|
+
if (embedderInstance3 && embedderKey3 === cacheKey) return embedderInstance3;
|
|
3789
|
+
embedderInstance3 = await createProjectEmbedder(projectPath);
|
|
3790
|
+
embedderKey3 = cacheKey;
|
|
3060
3791
|
return embedderInstance3;
|
|
3061
3792
|
}
|
|
3062
3793
|
async function runWatch(projectPath, options = {}) {
|
|
3063
|
-
const absoluteRoot =
|
|
3064
|
-
const dbPath =
|
|
3794
|
+
const absoluteRoot = path9.resolve(projectPath);
|
|
3795
|
+
const dbPath = path9.join(absoluteRoot, CTX_DIR5, DB_FILENAME4);
|
|
3065
3796
|
const log = options.log ?? console.log;
|
|
3066
3797
|
if (options.init) {
|
|
3067
3798
|
await runInit(absoluteRoot, { log, skipEmbedding: options.skipEmbedding });
|
|
3068
3799
|
}
|
|
3069
|
-
if (!
|
|
3800
|
+
if (!fs9.existsSync(dbPath)) {
|
|
3070
3801
|
throw new KontextError(
|
|
3071
|
-
`Project not initialized. Run "ctx init" first or use --init flag. (${
|
|
3802
|
+
`Project not initialized. Run "ctx init" first or use --init flag. (${CTX_DIR5}/${DB_FILENAME4} not found)`,
|
|
3072
3803
|
ErrorCode.NOT_INITIALIZED
|
|
3073
3804
|
);
|
|
3074
3805
|
}
|
|
3075
3806
|
await initParser();
|
|
3076
|
-
const
|
|
3807
|
+
const embedderConfig = getProjectEmbedderConfig(absoluteRoot);
|
|
3808
|
+
const db = createDatabase(dbPath, embedderConfig.dimensions);
|
|
3077
3809
|
let watcherHandle = null;
|
|
3810
|
+
let reindexQueue = Promise.resolve();
|
|
3078
3811
|
const watcher = createWatcher(
|
|
3079
3812
|
{
|
|
3080
3813
|
projectPath: absoluteRoot,
|
|
@@ -3083,7 +3816,7 @@ async function runWatch(projectPath, options = {}) {
|
|
|
3083
3816
|
},
|
|
3084
3817
|
{
|
|
3085
3818
|
onChange: (changes) => {
|
|
3086
|
-
|
|
3819
|
+
reindexQueue = reindexQueue.then(async () => {
|
|
3087
3820
|
try {
|
|
3088
3821
|
const result = await reindexChanges(db, changes, absoluteRoot, {
|
|
3089
3822
|
skipEmbedding: options.skipEmbedding,
|
|
@@ -3099,7 +3832,7 @@ async function runWatch(projectPath, options = {}) {
|
|
|
3099
3832
|
`[${timestamp()}] Error: ${err instanceof Error ? err.message : String(err)}`
|
|
3100
3833
|
);
|
|
3101
3834
|
}
|
|
3102
|
-
})
|
|
3835
|
+
});
|
|
3103
3836
|
},
|
|
3104
3837
|
onError: (err) => {
|
|
3105
3838
|
log(`[${timestamp()}] Watcher error: ${err.message}`);
|
|
@@ -3115,6 +3848,7 @@ async function runWatch(projectPath, options = {}) {
|
|
|
3115
3848
|
await watcherHandle.stop();
|
|
3116
3849
|
watcherHandle = null;
|
|
3117
3850
|
}
|
|
3851
|
+
await reindexQueue;
|
|
3118
3852
|
db.close();
|
|
3119
3853
|
log("Stopped watching. Database saved.");
|
|
3120
3854
|
}
|
|
@@ -3149,11 +3883,11 @@ function registerWatchCommand(program2) {
|
|
|
3149
3883
|
}
|
|
3150
3884
|
|
|
3151
3885
|
// src/cli/commands/status.ts
|
|
3152
|
-
import
|
|
3153
|
-
import
|
|
3154
|
-
var
|
|
3886
|
+
import fs10 from "fs";
|
|
3887
|
+
import path10 from "path";
|
|
3888
|
+
var CTX_DIR6 = ".ctx";
|
|
3155
3889
|
var DB_FILENAME5 = "index.db";
|
|
3156
|
-
var
|
|
3890
|
+
var CONFIG_FILENAME3 = "config.json";
|
|
3157
3891
|
function formatBytes2(bytes) {
|
|
3158
3892
|
if (bytes < 1024) return `${bytes} B`;
|
|
3159
3893
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -3168,15 +3902,17 @@ function formatTimestamp(raw) {
|
|
|
3168
3902
|
function capitalize(s) {
|
|
3169
3903
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
3170
3904
|
}
|
|
3171
|
-
function
|
|
3172
|
-
const configPath2 =
|
|
3173
|
-
if (!
|
|
3905
|
+
function readConfig2(ctxDir) {
|
|
3906
|
+
const configPath2 = path10.join(ctxDir, CONFIG_FILENAME3);
|
|
3907
|
+
if (!fs10.existsSync(configPath2)) return null;
|
|
3174
3908
|
try {
|
|
3175
|
-
const raw =
|
|
3909
|
+
const raw = fs10.readFileSync(configPath2, "utf-8");
|
|
3176
3910
|
const parsed = JSON.parse(raw);
|
|
3911
|
+
const embedder = parsed.embedder;
|
|
3177
3912
|
return {
|
|
3178
|
-
|
|
3179
|
-
|
|
3913
|
+
provider: embedder?.provider ?? parsed.provider ?? "unknown",
|
|
3914
|
+
model: embedder?.model ?? parsed.model ?? "unknown",
|
|
3915
|
+
dimensions: embedder?.dimensions ?? parsed.dimensions ?? 0
|
|
3180
3916
|
};
|
|
3181
3917
|
} catch {
|
|
3182
3918
|
return null;
|
|
@@ -3195,7 +3931,7 @@ function formatStatus(projectPath, output) {
|
|
|
3195
3931
|
`Kontext Status \u2014 ${projectPath}`,
|
|
3196
3932
|
"",
|
|
3197
3933
|
` Initialized: Yes`,
|
|
3198
|
-
` Database: ${
|
|
3934
|
+
` Database: ${CTX_DIR6}/${DB_FILENAME5} (${formatBytes2(output.dbSizeBytes)})`
|
|
3199
3935
|
];
|
|
3200
3936
|
if (output.lastIndexed) {
|
|
3201
3937
|
lines.push(` Last indexed: ${formatTimestamp(output.lastIndexed)}`);
|
|
@@ -3218,17 +3954,17 @@ function formatStatus(projectPath, output) {
|
|
|
3218
3954
|
if (output.config) {
|
|
3219
3955
|
lines.push("");
|
|
3220
3956
|
lines.push(
|
|
3221
|
-
` Embedder:
|
|
3957
|
+
` Embedder: ${output.config.provider} (${output.config.model}, ${output.config.dimensions} dims)`
|
|
3222
3958
|
);
|
|
3223
3959
|
}
|
|
3224
3960
|
lines.push("");
|
|
3225
3961
|
return lines.join("\n");
|
|
3226
3962
|
}
|
|
3227
3963
|
async function runStatus(projectPath) {
|
|
3228
|
-
const absoluteRoot =
|
|
3229
|
-
const ctxDir =
|
|
3230
|
-
const dbPath =
|
|
3231
|
-
if (!
|
|
3964
|
+
const absoluteRoot = path10.resolve(projectPath);
|
|
3965
|
+
const ctxDir = path10.join(absoluteRoot, CTX_DIR6);
|
|
3966
|
+
const dbPath = path10.join(ctxDir, DB_FILENAME5);
|
|
3967
|
+
if (!fs10.existsSync(dbPath)) {
|
|
3232
3968
|
const output = {
|
|
3233
3969
|
initialized: false,
|
|
3234
3970
|
fileCount: 0,
|
|
@@ -3249,8 +3985,8 @@ async function runStatus(projectPath) {
|
|
|
3249
3985
|
const vectorCount = db.getVectorCount();
|
|
3250
3986
|
const languages = db.getLanguageBreakdown();
|
|
3251
3987
|
const lastIndexed = db.getLastIndexed();
|
|
3252
|
-
const config =
|
|
3253
|
-
const dbSizeBytes =
|
|
3988
|
+
const config = readConfig2(ctxDir);
|
|
3989
|
+
const dbSizeBytes = fs10.statSync(dbPath).size;
|
|
3254
3990
|
const output = {
|
|
3255
3991
|
initialized: true,
|
|
3256
3992
|
fileCount,
|
|
@@ -3303,205 +4039,6 @@ function registerChunkCommand(program2) {
|
|
|
3303
4039
|
});
|
|
3304
4040
|
}
|
|
3305
4041
|
|
|
3306
|
-
// src/cli/commands/config.ts
|
|
3307
|
-
import fs10 from "fs";
|
|
3308
|
-
import path10 from "path";
|
|
3309
|
-
var CTX_DIR6 = ".ctx";
|
|
3310
|
-
var CONFIG_FILENAME3 = "config.json";
|
|
3311
|
-
var DEFAULT_CONFIG = {
|
|
3312
|
-
embedder: {
|
|
3313
|
-
provider: "local",
|
|
3314
|
-
model: "Xenova/all-MiniLM-L6-v2",
|
|
3315
|
-
dimensions: 384
|
|
3316
|
-
},
|
|
3317
|
-
search: {
|
|
3318
|
-
defaultLimit: 10,
|
|
3319
|
-
strategies: ["vector", "fts", "ast", "path"],
|
|
3320
|
-
weights: { vector: 1, fts: 0.8, ast: 0.9, path: 0.7, dependency: 0.6 }
|
|
3321
|
-
},
|
|
3322
|
-
watch: {
|
|
3323
|
-
debounceMs: 500,
|
|
3324
|
-
ignored: []
|
|
3325
|
-
},
|
|
3326
|
-
llm: {
|
|
3327
|
-
provider: null,
|
|
3328
|
-
model: null
|
|
3329
|
-
}
|
|
3330
|
-
};
|
|
3331
|
-
var VALID_EMBEDDER_PROVIDERS = /* @__PURE__ */ new Set(["local", "voyage", "openai"]);
|
|
3332
|
-
var VALID_LLM_PROVIDERS = /* @__PURE__ */ new Set(["gemini", "openai", "anthropic"]);
|
|
3333
|
-
var VALIDATION_RULES = {
|
|
3334
|
-
"embedder.provider": {
|
|
3335
|
-
validate: (v) => typeof v === "string" && VALID_EMBEDDER_PROVIDERS.has(v),
|
|
3336
|
-
message: `Must be one of: ${[...VALID_EMBEDDER_PROVIDERS].join(", ")}`
|
|
3337
|
-
},
|
|
3338
|
-
"embedder.dimensions": {
|
|
3339
|
-
validate: (v) => typeof v === "number" && v > 0 && Number.isInteger(v),
|
|
3340
|
-
message: "Must be a positive integer"
|
|
3341
|
-
},
|
|
3342
|
-
"search.defaultLimit": {
|
|
3343
|
-
validate: (v) => typeof v === "number" && v > 0 && Number.isInteger(v),
|
|
3344
|
-
message: "Must be a positive integer"
|
|
3345
|
-
},
|
|
3346
|
-
"watch.debounceMs": {
|
|
3347
|
-
validate: (v) => typeof v === "number" && v >= 0 && Number.isInteger(v),
|
|
3348
|
-
message: "Must be a non-negative integer"
|
|
3349
|
-
},
|
|
3350
|
-
"llm.provider": {
|
|
3351
|
-
validate: (v) => v === null || typeof v === "string" && VALID_LLM_PROVIDERS.has(v),
|
|
3352
|
-
message: `Must be null or one of: ${[...VALID_LLM_PROVIDERS].join(", ")}`
|
|
3353
|
-
}
|
|
3354
|
-
};
|
|
3355
|
-
function resolveCtxDir(projectPath) {
|
|
3356
|
-
const absoluteRoot = path10.resolve(projectPath);
|
|
3357
|
-
const ctxDir = path10.join(absoluteRoot, CTX_DIR6);
|
|
3358
|
-
if (!fs10.existsSync(ctxDir)) {
|
|
3359
|
-
throw new ConfigError(
|
|
3360
|
-
`Project not initialized. Run "ctx init" first. (${CTX_DIR6}/ not found)`,
|
|
3361
|
-
ErrorCode.NOT_INITIALIZED
|
|
3362
|
-
);
|
|
3363
|
-
}
|
|
3364
|
-
return ctxDir;
|
|
3365
|
-
}
|
|
3366
|
-
function configPath(ctxDir) {
|
|
3367
|
-
return path10.join(ctxDir, CONFIG_FILENAME3);
|
|
3368
|
-
}
|
|
3369
|
-
function readConfig2(ctxDir) {
|
|
3370
|
-
const filePath = configPath(ctxDir);
|
|
3371
|
-
if (!fs10.existsSync(filePath)) {
|
|
3372
|
-
writeConfig(ctxDir, DEFAULT_CONFIG);
|
|
3373
|
-
return structuredClone(DEFAULT_CONFIG);
|
|
3374
|
-
}
|
|
3375
|
-
const raw = fs10.readFileSync(filePath, "utf-8");
|
|
3376
|
-
const parsed = JSON.parse(raw);
|
|
3377
|
-
return mergeWithDefaults(parsed);
|
|
3378
|
-
}
|
|
3379
|
-
function writeConfig(ctxDir, config) {
|
|
3380
|
-
fs10.writeFileSync(
|
|
3381
|
-
configPath(ctxDir),
|
|
3382
|
-
JSON.stringify(config, null, 2) + "\n"
|
|
3383
|
-
);
|
|
3384
|
-
}
|
|
3385
|
-
function mergeWithDefaults(partial) {
|
|
3386
|
-
return {
|
|
3387
|
-
embedder: { ...DEFAULT_CONFIG.embedder, ...partial.embedder },
|
|
3388
|
-
search: {
|
|
3389
|
-
...DEFAULT_CONFIG.search,
|
|
3390
|
-
...partial.search,
|
|
3391
|
-
weights: { ...DEFAULT_CONFIG.search.weights, ...partial.search?.weights }
|
|
3392
|
-
},
|
|
3393
|
-
watch: { ...DEFAULT_CONFIG.watch, ...partial.watch },
|
|
3394
|
-
llm: { ...DEFAULT_CONFIG.llm, ...partial.llm }
|
|
3395
|
-
};
|
|
3396
|
-
}
|
|
3397
|
-
function getNestedValue(obj, key) {
|
|
3398
|
-
const parts = key.split(".");
|
|
3399
|
-
let current = obj;
|
|
3400
|
-
for (const part of parts) {
|
|
3401
|
-
if (current === null || current === void 0 || typeof current !== "object") {
|
|
3402
|
-
return void 0;
|
|
3403
|
-
}
|
|
3404
|
-
current = current[part];
|
|
3405
|
-
}
|
|
3406
|
-
return current;
|
|
3407
|
-
}
|
|
3408
|
-
function setNestedValue(obj, key, value) {
|
|
3409
|
-
const parts = key.split(".");
|
|
3410
|
-
let current = obj;
|
|
3411
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
3412
|
-
const part = parts[i];
|
|
3413
|
-
if (typeof current[part] !== "object" || current[part] === null) {
|
|
3414
|
-
current[part] = {};
|
|
3415
|
-
}
|
|
3416
|
-
current = current[part];
|
|
3417
|
-
}
|
|
3418
|
-
current[parts[parts.length - 1]] = value;
|
|
3419
|
-
}
|
|
3420
|
-
function parseValue(rawValue) {
|
|
3421
|
-
if (rawValue === "null") return null;
|
|
3422
|
-
if (rawValue === "true") return true;
|
|
3423
|
-
if (rawValue === "false") return false;
|
|
3424
|
-
const num = Number(rawValue);
|
|
3425
|
-
if (!Number.isNaN(num) && rawValue.trim() !== "") return num;
|
|
3426
|
-
if (rawValue.startsWith("[") || rawValue.startsWith("{")) {
|
|
3427
|
-
try {
|
|
3428
|
-
return JSON.parse(rawValue);
|
|
3429
|
-
} catch {
|
|
3430
|
-
}
|
|
3431
|
-
}
|
|
3432
|
-
return rawValue;
|
|
3433
|
-
}
|
|
3434
|
-
function runConfigShow(projectPath) {
|
|
3435
|
-
const ctxDir = resolveCtxDir(projectPath);
|
|
3436
|
-
const config = readConfig2(ctxDir);
|
|
3437
|
-
return {
|
|
3438
|
-
config,
|
|
3439
|
-
text: JSON.stringify(config, null, 2)
|
|
3440
|
-
};
|
|
3441
|
-
}
|
|
3442
|
-
function runConfigGet(projectPath, key) {
|
|
3443
|
-
const ctxDir = resolveCtxDir(projectPath);
|
|
3444
|
-
const config = readConfig2(ctxDir);
|
|
3445
|
-
return getNestedValue(config, key);
|
|
3446
|
-
}
|
|
3447
|
-
function runConfigSet(projectPath, key, rawValue) {
|
|
3448
|
-
const ctxDir = resolveCtxDir(projectPath);
|
|
3449
|
-
const config = readConfig2(ctxDir);
|
|
3450
|
-
const value = parseValue(rawValue);
|
|
3451
|
-
const rule = VALIDATION_RULES[key];
|
|
3452
|
-
if (rule && !rule.validate(value)) {
|
|
3453
|
-
throw new ConfigError(`Invalid value for "${key}": ${rule.message}`, ErrorCode.CONFIG_INVALID);
|
|
3454
|
-
}
|
|
3455
|
-
setNestedValue(config, key, value);
|
|
3456
|
-
writeConfig(ctxDir, config);
|
|
3457
|
-
}
|
|
3458
|
-
function runConfigReset(projectPath) {
|
|
3459
|
-
const ctxDir = resolveCtxDir(projectPath);
|
|
3460
|
-
writeConfig(ctxDir, structuredClone(DEFAULT_CONFIG));
|
|
3461
|
-
}
|
|
3462
|
-
function registerConfigCommand(program2) {
|
|
3463
|
-
const cmd = program2.command("config").description("Show or modify configuration");
|
|
3464
|
-
function configErrorHandler(err) {
|
|
3465
|
-
const verbose = program2.opts()["verbose"] === true;
|
|
3466
|
-
const logger = createLogger({ level: verbose ? LogLevel.DEBUG : LogLevel.INFO });
|
|
3467
|
-
process.exitCode = handleCommandError(err, logger, verbose);
|
|
3468
|
-
}
|
|
3469
|
-
cmd.command("show").description("Show current configuration").action(() => {
|
|
3470
|
-
try {
|
|
3471
|
-
const output = runConfigShow(process.cwd());
|
|
3472
|
-
console.log(output.text);
|
|
3473
|
-
} catch (err) {
|
|
3474
|
-
configErrorHandler(err);
|
|
3475
|
-
}
|
|
3476
|
-
});
|
|
3477
|
-
cmd.command("get <key>").description("Get a configuration value (dot notation)").action((key) => {
|
|
3478
|
-
try {
|
|
3479
|
-
const value = runConfigGet(process.cwd(), key);
|
|
3480
|
-
console.log(
|
|
3481
|
-
typeof value === "object" ? JSON.stringify(value, null, 2) : String(value)
|
|
3482
|
-
);
|
|
3483
|
-
} catch (err) {
|
|
3484
|
-
configErrorHandler(err);
|
|
3485
|
-
}
|
|
3486
|
-
});
|
|
3487
|
-
cmd.command("set <key> <value>").description("Set a configuration value (dot notation)").action((key, value) => {
|
|
3488
|
-
try {
|
|
3489
|
-
runConfigSet(process.cwd(), key, value);
|
|
3490
|
-
console.log(`Set ${key} = ${value}`);
|
|
3491
|
-
} catch (err) {
|
|
3492
|
-
configErrorHandler(err);
|
|
3493
|
-
}
|
|
3494
|
-
});
|
|
3495
|
-
cmd.command("reset").description("Reset configuration to defaults").action(() => {
|
|
3496
|
-
try {
|
|
3497
|
-
runConfigReset(process.cwd());
|
|
3498
|
-
console.log("Configuration reset to defaults.");
|
|
3499
|
-
} catch (err) {
|
|
3500
|
-
configErrorHandler(err);
|
|
3501
|
-
}
|
|
3502
|
-
});
|
|
3503
|
-
}
|
|
3504
|
-
|
|
3505
4042
|
// src/cli/commands/auth.ts
|
|
3506
4043
|
function registerAuthCommand(program2) {
|
|
3507
4044
|
program2.command("auth").description("Set API keys for LLM and embedding providers").action(() => {
|
|
@@ -3510,12 +4047,13 @@ function registerAuthCommand(program2) {
|
|
|
3510
4047
|
}
|
|
3511
4048
|
|
|
3512
4049
|
// src/cli/index.ts
|
|
4050
|
+
var require3 = createRequire2(import.meta.url);
|
|
4051
|
+
var packageJson = require3("../../package.json");
|
|
3513
4052
|
var program = new Command();
|
|
3514
|
-
program.name("ctx").description("Kontext \u2014 Context engine for AI coding agents").version("0.
|
|
4053
|
+
program.name("ctx").description("Kontext \u2014 Context engine for AI coding agents").version(packageJson.version ?? "0.0.0").option("--verbose", "Enable verbose/debug output");
|
|
3515
4054
|
registerInitCommand(program);
|
|
3516
4055
|
registerQueryCommand(program);
|
|
3517
4056
|
registerAskCommand(program);
|
|
3518
|
-
registerFindCommand(program);
|
|
3519
4057
|
registerUpdateCommand(program);
|
|
3520
4058
|
registerWatchCommand(program);
|
|
3521
4059
|
registerStatusCommand(program);
|