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/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 VOYAGE_DIMENSIONS = 1024;
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: VOYAGE_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: "document"
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 OPENAI_DIMENSIONS = 1024;
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: OPENAI_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: OPENAI_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
- function createDatabase(dbPath, dimensions = DEFAULT_DIMENSIONS) {
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 result = stmtUpsertFile.run({
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 (result.changes > 0 && result.lastInsertRowid) {
1112
- return Number(result.lastInsertRowid);
1162
+ if (!row?.id) {
1163
+ throw new DatabaseError(
1164
+ `Failed to upsert file: ${file.path}`,
1165
+ ErrorCode.DB_WRITE_FAILED
1166
+ );
1113
1167
  }
1114
- const existing = stmtGetFile.get(file.path);
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
- const file = stmtGetFile.get(filePath);
1159
- if (file) {
1160
- const chunkRows = stmtGetChunkIdsByFile.all(file.id);
1161
- const chunkIds = chunkRows.map((r) => r.id);
1162
- if (chunkIds.length > 0) {
1163
- deleteVectorsByChunkIds(db, chunkIds);
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
- stmtDeleteFile.run(filePath);
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
- const chunkRows = stmtGetChunkIdsByFile.all(fileId);
1262
- const chunkIds = chunkRows.map((r) => r.id);
1263
- if (chunkIds.length > 0) {
1264
- deleteVectorsByChunkIds(db, chunkIds);
1265
- }
1266
- stmtDeleteChunksByFile.run(fileId);
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
- return query.replace(/[?()":^~{}!+\-\\]/g, " ").replace(/(?<!\w)\*/g, " ").replace(/\s+/g, " ").trim();
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 results = [];
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
- for (const chunk of chunks) {
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
- results.push({
1522
- chunkId: chunk.id,
1523
- filePath: file.path,
1524
- lineStart: chunk.lineStart,
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/llm.ts
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) addUnique(m);
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) addUnique(p.replace(/[?!,;]+$/g, ""));
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 (STOP_WORDS.has(lower) && !CODE_IDENT_RE.test(w)) continue;
2025
- addUnique(w);
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 keywords = extractSearchTerms(query);
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 fs5 from "fs";
2133
- import path4 from "path";
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/init.ts
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 = path4.join(projectRoot, ".gitignore");
2228
- if (fs5.existsSync(gitignorePath)) {
2229
- const content = fs5.readFileSync(gitignorePath, "utf-8");
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
- fs5.writeFileSync(gitignorePath, `${content}${suffix}${GITIGNORE_ENTRY}
2673
+ fs6.writeFileSync(gitignorePath, `${content}${suffix}${GITIGNORE_ENTRY}
2233
2674
  `);
2234
2675
  } else {
2235
- fs5.writeFileSync(gitignorePath, `${GITIGNORE_ENTRY}
2676
+ fs6.writeFileSync(gitignorePath, `${GITIGNORE_ENTRY}
2236
2677
  `);
2237
2678
  }
2238
2679
  }
2239
2680
  function ensureConfig(ctxDir) {
2240
- const configPath = path4.join(ctxDir, CONFIG_FILENAME);
2241
- if (fs5.existsSync(configPath)) return;
2242
- const config = {
2243
- version: 1,
2244
- dimensions: 384,
2245
- model: "all-MiniLM-L6-v2"
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 = path4.resolve(projectPath);
2703
+ const absoluteRoot = path5.resolve(projectPath);
2265
2704
  const start = performance.now();
2266
2705
  log(`Indexing ${absoluteRoot}...`);
2267
- const ctxDir = path4.join(absoluteRoot, CTX_DIR);
2268
- if (!fs5.existsSync(ctxDir)) fs5.mkdirSync(ctxDir, { recursive: true });
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 dbPath = path4.join(ctxDir, DB_FILENAME);
2272
- const db = createDatabase(dbPath);
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 = fs5.existsSync(dbPath) ? fs5.statSync(dbPath).size : 0;
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: ${CTX_DIR}/${DB_FILENAME} (${formatBytes(dbSize)})`);
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 createLocalEmbedder();
2838
+ async function createEmbedder(projectPath) {
2839
+ return createProjectEmbedder(projectPath);
2400
2840
  }
2401
2841
 
2402
2842
  // src/cli/commands/query.ts
2403
- import fs6 from "fs";
2404
- import path5 from "path";
2405
- var CTX_DIR2 = ".ctx";
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 absoluteRoot = path5.resolve(projectPath);
2456
- const dbPath = path5.join(absoluteRoot, CTX_DIR2, DB_FILENAME2);
2457
- if (!fs6.existsSync(dbPath)) {
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. (${CTX_DIR2}/${DB_FILENAME2} not found)`,
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 db = createDatabase(dbPath);
2919
+ const embedderConfig = getProjectEmbedderConfig(absoluteRoot);
2920
+ const db = createDatabase(dbPath, embedderConfig.dimensions);
2465
2921
  try {
2466
- const strategyResults = await runStrategies(db, query, options);
2922
+ const strategyResults = await runStrategies(db, absoluteRoot, query, {
2923
+ ...options,
2924
+ limit
2925
+ });
2467
2926
  const pathBoostTerms = extractPathBoostTerms(query);
2468
- const fused = fusionMergeWithPathBoost(strategyResults, options.limit, pathBoostTerms);
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 = STRATEGY_WEIGHTS[strategy];
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
- async function loadEmbedder() {
2542
- if (embedderInstance) return embedderInstance;
2543
- embedderInstance = await createLocalEmbedder();
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 fs7 from "fs";
2549
- import path6 from "path";
2550
- var CTX_DIR3 = ".ctx";
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
- async function loadEmbedder2() {
2664
- if (embedderInstance2) return embedderInstance2;
2665
- embedderInstance2 = await createLocalEmbedder();
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 keywords = extractSearchTerms(query);
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 absoluteRoot = path6.resolve(projectPath);
2693
- const dbPath = path6.join(absoluteRoot, CTX_DIR3, DB_FILENAME3);
2694
- if (!fs7.existsSync(dbPath)) {
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. (${CTX_DIR3}/${DB_FILENAME3} not found)`,
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 db = createDatabase(dbPath);
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, options.limit);
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, options.limit);
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, options.limit, executor);
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 fs8 from "fs";
2762
- import path7 from "path";
2763
- var CTX_DIR4 = ".ctx";
3237
+ import fs9 from "fs";
3238
+ import path8 from "path";
3239
+ var CTX_DIR5 = ".ctx";
2764
3240
  var DB_FILENAME4 = "index.db";
2765
- var CONFIG_FILENAME2 = "config.json";
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 readConfig(ctxDir) {
2781
- const configPath = path7.join(ctxDir, CONFIG_FILENAME2);
2782
- if (!fs8.existsSync(configPath)) return null;
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 = fs8.readFileSync(configPath, "utf-8");
3260
+ const raw = fs9.readFileSync(configPath2, "utf-8");
2785
3261
  const parsed = JSON.parse(raw);
3262
+ const embedder = parsed.embedder;
2786
3263
  return {
2787
- model: parsed.model ?? "unknown",
2788
- dimensions: parsed.dimensions ?? 0
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: ${CTX_DIR4}/${DB_FILENAME4} (${formatBytes2(output.dbSizeBytes)})`
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: local (${output.config.model}, ${output.config.dimensions} dims)`
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 = path7.resolve(projectPath);
2838
- const ctxDir = path7.join(absoluteRoot, CTX_DIR4);
2839
- const dbPath = path7.join(ctxDir, DB_FILENAME4);
2840
- if (!fs8.existsSync(dbPath)) {
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 = readConfig(ctxDir);
2862
- const dbSizeBytes = fs8.statSync(dbPath).size;
3339
+ const config = readConfig2(ctxDir);
3340
+ const dbSizeBytes = fs9.statSync(dbPath).size;
2863
3341
  const output = {
2864
3342
  initialized: true,
2865
3343
  fileCount,