kontext-engine 0.1.4 → 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) {
@@ -2155,7 +2265,9 @@ var COMMON_STEMS = {
2155
2265
  transformer: "transform",
2156
2266
  transformation: "transform",
2157
2267
  connection: "connect",
2268
+ connecting: "connect",
2158
2269
  connector: "connect",
2270
+ migrating: "migrate",
2159
2271
  migration: "migrate",
2160
2272
  scheduling: "schedule",
2161
2273
  scheduler: "schedule",
@@ -2164,7 +2276,8 @@ var COMMON_STEMS = {
2164
2276
  routing: "route",
2165
2277
  router: "route",
2166
2278
  indexing: "index",
2167
- indexer: "index"
2279
+ indexer: "index",
2280
+ subscribing: "subscribe"
2168
2281
  };
2169
2282
  var STEM_SUFFIXES = [
2170
2283
  "tion",
@@ -2358,53 +2471,6 @@ async function steer(provider, query, limit, searchExecutor) {
2358
2471
  import fs6 from "fs";
2359
2472
  import path5 from "path";
2360
2473
 
2361
- // src/utils/errors.ts
2362
- var ErrorCode = {
2363
- NOT_INITIALIZED: "NOT_INITIALIZED",
2364
- INDEX_FAILED: "INDEX_FAILED",
2365
- PARSE_FAILED: "PARSE_FAILED",
2366
- CHUNK_FAILED: "CHUNK_FAILED",
2367
- EMBEDDER_FAILED: "EMBEDDER_FAILED",
2368
- SEARCH_FAILED: "SEARCH_FAILED",
2369
- CONFIG_INVALID: "CONFIG_INVALID",
2370
- DB_CORRUPTED: "DB_CORRUPTED",
2371
- DB_WRITE_FAILED: "DB_WRITE_FAILED",
2372
- WATCHER_FAILED: "WATCHER_FAILED",
2373
- LLM_FAILED: "LLM_FAILED"
2374
- };
2375
- var KontextError = class extends Error {
2376
- code;
2377
- constructor(message, code, cause) {
2378
- super(message, { cause });
2379
- this.name = "KontextError";
2380
- this.code = code;
2381
- }
2382
- };
2383
- var IndexError = class extends KontextError {
2384
- constructor(message, code, cause) {
2385
- super(message, code, cause);
2386
- this.name = "IndexError";
2387
- }
2388
- };
2389
- var SearchError = class extends KontextError {
2390
- constructor(message, code, cause) {
2391
- super(message, code, cause);
2392
- this.name = "SearchError";
2393
- }
2394
- };
2395
- var ConfigError = class extends KontextError {
2396
- constructor(message, code, cause) {
2397
- super(message, code, cause);
2398
- this.name = "ConfigError";
2399
- }
2400
- };
2401
- var DatabaseError = class extends KontextError {
2402
- constructor(message, code, cause) {
2403
- super(message, code, cause);
2404
- this.name = "DatabaseError";
2405
- }
2406
- };
2407
-
2408
2474
  // src/utils/logger.ts
2409
2475
  var LogLevel = {
2410
2476
  DEBUG: 0,
@@ -2447,6 +2513,8 @@ function createLogger(options) {
2447
2513
  // src/cli/commands/config.ts
2448
2514
  import fs5 from "fs";
2449
2515
  import path4 from "path";
2516
+ var CTX_DIR = ".ctx";
2517
+ var CONFIG_FILENAME = "config.json";
2450
2518
  var DEFAULT_CONFIG = {
2451
2519
  embedder: {
2452
2520
  provider: "local",
@@ -2491,11 +2559,110 @@ var VALIDATION_RULES = {
2491
2559
  message: `Must be null or one of: ${[...VALID_LLM_PROVIDERS].join(", ")}`
2492
2560
  }
2493
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
+ }
2494
2661
 
2495
2662
  // src/cli/commands/init.ts
2496
- var CTX_DIR = ".ctx";
2663
+ var CTX_DIR2 = ".ctx";
2497
2664
  var DB_FILENAME = "index.db";
2498
- var CONFIG_FILENAME = "config.json";
2665
+ var CONFIG_FILENAME2 = "config.json";
2499
2666
  var GITIGNORE_ENTRY = ".ctx/";
2500
2667
  function ensureGitignore(projectRoot) {
2501
2668
  const gitignorePath = path5.join(projectRoot, ".gitignore");
@@ -2511,10 +2678,10 @@ function ensureGitignore(projectRoot) {
2511
2678
  }
2512
2679
  }
2513
2680
  function ensureConfig(ctxDir) {
2514
- const configPath = path5.join(ctxDir, CONFIG_FILENAME);
2515
- if (fs6.existsSync(configPath)) return;
2681
+ const configPath2 = path5.join(ctxDir, CONFIG_FILENAME2);
2682
+ if (fs6.existsSync(configPath2)) return;
2516
2683
  fs6.writeFileSync(
2517
- configPath,
2684
+ configPath2,
2518
2685
  JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n"
2519
2686
  );
2520
2687
  }
@@ -2536,12 +2703,13 @@ async function runInit(projectPath, options = {}) {
2536
2703
  const absoluteRoot = path5.resolve(projectPath);
2537
2704
  const start = performance.now();
2538
2705
  log(`Indexing ${absoluteRoot}...`);
2539
- const ctxDir = path5.join(absoluteRoot, CTX_DIR);
2706
+ const ctxDir = path5.join(absoluteRoot, CTX_DIR2);
2540
2707
  if (!fs6.existsSync(ctxDir)) fs6.mkdirSync(ctxDir, { recursive: true });
2541
2708
  ensureGitignore(absoluteRoot);
2542
2709
  ensureConfig(ctxDir);
2710
+ const embedderConfig = getProjectEmbedderConfig(absoluteRoot);
2543
2711
  const dbPath = path5.join(ctxDir, DB_FILENAME);
2544
- const db = createDatabase(dbPath);
2712
+ const db = createDatabase(dbPath, embedderConfig.dimensions);
2545
2713
  try {
2546
2714
  const discovered = await discoverFiles({
2547
2715
  root: absoluteRoot,
@@ -2629,7 +2797,7 @@ async function runInit(projectPath, options = {}) {
2629
2797
  log(` ${allChunksWithMeta.length} chunks created`);
2630
2798
  let vectorsCreated = 0;
2631
2799
  if (!options.skipEmbedding && allChunksWithMeta.length > 0) {
2632
- const embedder = await createEmbedder();
2800
+ const embedder = await createEmbedder(absoluteRoot);
2633
2801
  const texts = allChunksWithMeta.map(
2634
2802
  (cm) => prepareChunkText(cm.fileRelPath, cm.chunk.parent, cm.chunk.text)
2635
2803
  );
@@ -2651,7 +2819,7 @@ async function runInit(projectPath, options = {}) {
2651
2819
  log(
2652
2820
  ` ${discovered.length} files \u2192 ${allChunksWithMeta.length} chunks` + (vectorsCreated > 0 ? ` \u2192 ${vectorsCreated} vectors` : "")
2653
2821
  );
2654
- log(` Database: ${CTX_DIR}/${DB_FILENAME} (${formatBytes(dbSize)})`);
2822
+ log(` Database: ${CTX_DIR2}/${DB_FILENAME} (${formatBytes(dbSize)})`);
2655
2823
  return {
2656
2824
  filesDiscovered: discovered.length,
2657
2825
  filesAdded: changes.added.length,
@@ -2667,14 +2835,14 @@ async function runInit(projectPath, options = {}) {
2667
2835
  db.close();
2668
2836
  }
2669
2837
  }
2670
- async function createEmbedder() {
2671
- return createLocalEmbedder();
2838
+ async function createEmbedder(projectPath) {
2839
+ return createProjectEmbedder(projectPath);
2672
2840
  }
2673
2841
 
2674
2842
  // src/cli/commands/query.ts
2675
2843
  import fs7 from "fs";
2676
2844
  import path6 from "path";
2677
- var CTX_DIR2 = ".ctx";
2845
+ var CTX_DIR3 = ".ctx";
2678
2846
  var DB_FILENAME2 = "index.db";
2679
2847
  var SNIPPET_MAX_LENGTH = 200;
2680
2848
  var STRATEGY_WEIGHTS = {
@@ -2740,17 +2908,21 @@ function isPathLike(query) {
2740
2908
  async function runQuery(projectPath, query, options) {
2741
2909
  const limit = normalizeLimit(options.limit);
2742
2910
  const absoluteRoot = path6.resolve(projectPath);
2743
- const dbPath = path6.join(absoluteRoot, CTX_DIR2, DB_FILENAME2);
2911
+ const dbPath = path6.join(absoluteRoot, CTX_DIR3, DB_FILENAME2);
2744
2912
  if (!fs7.existsSync(dbPath)) {
2745
2913
  throw new KontextError(
2746
- `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)`,
2747
2915
  ErrorCode.NOT_INITIALIZED
2748
2916
  );
2749
2917
  }
2750
2918
  const start = performance.now();
2751
- const db = createDatabase(dbPath);
2919
+ const embedderConfig = getProjectEmbedderConfig(absoluteRoot);
2920
+ const db = createDatabase(dbPath, embedderConfig.dimensions);
2752
2921
  try {
2753
- const strategyResults = await runStrategies(db, query, { ...options, limit });
2922
+ const strategyResults = await runStrategies(db, absoluteRoot, query, {
2923
+ ...options,
2924
+ limit
2925
+ });
2754
2926
  const pathBoostTerms = extractPathBoostTerms(query);
2755
2927
  const fused = fusionMergeWithPathBoost(strategyResults, limit, pathBoostTerms);
2756
2928
  const outputResults = fused.map(toOutputResult);
@@ -2770,7 +2942,7 @@ async function runQuery(projectPath, query, options) {
2770
2942
  db.close();
2771
2943
  }
2772
2944
  }
2773
- async function runStrategies(db, query, options) {
2945
+ async function runStrategies(db, projectPath, query, options) {
2774
2946
  const results = [];
2775
2947
  const filters = options.language ? { language: options.language } : void 0;
2776
2948
  const limit = options.limit * 3;
@@ -2779,6 +2951,7 @@ async function runStrategies(db, query, options) {
2779
2951
  const weight = effectiveWeights[strategy];
2780
2952
  const searchResults = await executeStrategy(
2781
2953
  db,
2954
+ projectPath,
2782
2955
  strategy,
2783
2956
  query,
2784
2957
  limit,
@@ -2790,10 +2963,10 @@ async function runStrategies(db, query, options) {
2790
2963
  }
2791
2964
  return results;
2792
2965
  }
2793
- async function executeStrategy(db, strategy, query, limit, filters) {
2966
+ async function executeStrategy(db, projectPath, strategy, query, limit, filters) {
2794
2967
  switch (strategy) {
2795
2968
  case "vector": {
2796
- const embedder = await loadEmbedder();
2969
+ const embedder = await loadEmbedder(projectPath);
2797
2970
  return vectorSearch(db, embedder, query, limit, filters);
2798
2971
  }
2799
2972
  case "fts":
@@ -2826,16 +2999,23 @@ async function executeStrategy(db, strategy, query, limit, filters) {
2826
2999
  }
2827
3000
  }
2828
3001
  var embedderInstance = null;
2829
- async function loadEmbedder() {
2830
- if (embedderInstance) return embedderInstance;
2831
- 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;
2832
3012
  return embedderInstance;
2833
3013
  }
2834
3014
 
2835
3015
  // src/cli/commands/ask.ts
2836
3016
  import fs8 from "fs";
2837
3017
  import path7 from "path";
2838
- var CTX_DIR3 = ".ctx";
3018
+ var CTX_DIR4 = ".ctx";
2839
3019
  var DB_FILENAME3 = "index.db";
2840
3020
  var SNIPPET_MAX_LENGTH2 = 200;
2841
3021
  var FALLBACK_NOTICE = "No LLM provider configured. Set CTX_GEMINI_KEY, CTX_OPENAI_KEY, or CTX_ANTHROPIC_KEY. Running basic search instead.";
@@ -2895,13 +3075,13 @@ function formatTextOutput2(output) {
2895
3075
  );
2896
3076
  return lines.join("\n");
2897
3077
  }
2898
- function createSearchExecutor(db, query) {
3078
+ function createSearchExecutor(db, projectPath, query) {
2899
3079
  const pathBoostTerms = extractPathBoostTerms(query);
2900
3080
  return async (strategies, limit) => {
2901
3081
  const strategyResults = [];
2902
3082
  const fetchLimit = limit * 3;
2903
3083
  for (const plan of strategies) {
2904
- const results = await executeStrategy2(db, plan, fetchLimit);
3084
+ const results = await executeStrategy2(db, projectPath, plan, fetchLimit);
2905
3085
  if (results.length > 0) {
2906
3086
  strategyResults.push({
2907
3087
  strategy: plan.strategy,
@@ -2920,10 +3100,10 @@ function extractSymbolNames2(query) {
2920
3100
  function isPathLike2(query) {
2921
3101
  return query.includes("/") || query.includes("*") || query.includes(".");
2922
3102
  }
2923
- async function executeStrategy2(db, plan, limit) {
3103
+ async function executeStrategy2(db, projectPath, plan, limit) {
2924
3104
  switch (plan.strategy) {
2925
3105
  case "vector": {
2926
- const embedder = await loadEmbedder2();
3106
+ const embedder = await loadEmbedder2(projectPath);
2927
3107
  return vectorSearch(db, embedder, plan.query, limit);
2928
3108
  }
2929
3109
  case "fts":
@@ -2952,13 +3132,20 @@ async function executeStrategy2(db, plan, limit) {
2952
3132
  }
2953
3133
  }
2954
3134
  var embedderInstance2 = null;
2955
- async function loadEmbedder2() {
2956
- if (embedderInstance2) return embedderInstance2;
2957
- 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;
2958
3145
  return embedderInstance2;
2959
3146
  }
2960
- async function fallbackSearch(db, query, limit) {
2961
- const executor = createSearchExecutor(db, query);
3147
+ async function fallbackSearch(db, projectPath, query, limit) {
3148
+ const executor = createSearchExecutor(db, projectPath, query);
2962
3149
  const fallbackStrategies = buildFallbackStrategies(query);
2963
3150
  const results = await executor(fallbackStrategies, limit);
2964
3151
  return {
@@ -2978,25 +3165,26 @@ async function fallbackSearch(db, query, limit) {
2978
3165
  async function runAsk(projectPath, query, options) {
2979
3166
  const limit = normalizeLimit2(options.limit);
2980
3167
  const absoluteRoot = path7.resolve(projectPath);
2981
- const dbPath = path7.join(absoluteRoot, CTX_DIR3, DB_FILENAME3);
3168
+ const dbPath = path7.join(absoluteRoot, CTX_DIR4, DB_FILENAME3);
2982
3169
  if (!fs8.existsSync(dbPath)) {
2983
3170
  throw new KontextError(
2984
- `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)`,
2985
3172
  ErrorCode.NOT_INITIALIZED
2986
3173
  );
2987
3174
  }
2988
- const db = createDatabase(dbPath);
3175
+ const embedderConfig = getProjectEmbedderConfig(absoluteRoot);
3176
+ const db = createDatabase(dbPath, embedderConfig.dimensions);
2989
3177
  try {
2990
3178
  const provider = options.provider ?? null;
2991
3179
  if (!provider) {
2992
- const output = await fallbackSearch(db, query, limit);
3180
+ const output = await fallbackSearch(db, absoluteRoot, query, limit);
2993
3181
  output.warning = FALLBACK_NOTICE;
2994
3182
  if (options.format === "text") {
2995
3183
  output.text = formatTextOutput2(output);
2996
3184
  }
2997
3185
  return output;
2998
3186
  }
2999
- const executor = createSearchExecutor(db, query);
3187
+ const executor = createSearchExecutor(db, absoluteRoot, query);
3000
3188
  if (options.noExplain) {
3001
3189
  return await runNoExplain(provider, query, limit, options, executor);
3002
3190
  }
@@ -3048,9 +3236,9 @@ async function runWithSteering(provider, query, limit, options, executor) {
3048
3236
  // src/cli/commands/status.ts
3049
3237
  import fs9 from "fs";
3050
3238
  import path8 from "path";
3051
- var CTX_DIR4 = ".ctx";
3239
+ var CTX_DIR5 = ".ctx";
3052
3240
  var DB_FILENAME4 = "index.db";
3053
- var CONFIG_FILENAME2 = "config.json";
3241
+ var CONFIG_FILENAME3 = "config.json";
3054
3242
  function formatBytes2(bytes) {
3055
3243
  if (bytes < 1024) return `${bytes} B`;
3056
3244
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -3065,14 +3253,15 @@ function formatTimestamp(raw) {
3065
3253
  function capitalize(s) {
3066
3254
  return s.charAt(0).toUpperCase() + s.slice(1);
3067
3255
  }
3068
- function readConfig(ctxDir) {
3069
- const configPath = path8.join(ctxDir, CONFIG_FILENAME2);
3070
- if (!fs9.existsSync(configPath)) return null;
3256
+ function readConfig2(ctxDir) {
3257
+ const configPath2 = path8.join(ctxDir, CONFIG_FILENAME3);
3258
+ if (!fs9.existsSync(configPath2)) return null;
3071
3259
  try {
3072
- const raw = fs9.readFileSync(configPath, "utf-8");
3260
+ const raw = fs9.readFileSync(configPath2, "utf-8");
3073
3261
  const parsed = JSON.parse(raw);
3074
3262
  const embedder = parsed.embedder;
3075
3263
  return {
3264
+ provider: embedder?.provider ?? parsed.provider ?? "unknown",
3076
3265
  model: embedder?.model ?? parsed.model ?? "unknown",
3077
3266
  dimensions: embedder?.dimensions ?? parsed.dimensions ?? 0
3078
3267
  };
@@ -3093,7 +3282,7 @@ function formatStatus(projectPath, output) {
3093
3282
  `Kontext Status \u2014 ${projectPath}`,
3094
3283
  "",
3095
3284
  ` Initialized: Yes`,
3096
- ` Database: ${CTX_DIR4}/${DB_FILENAME4} (${formatBytes2(output.dbSizeBytes)})`
3285
+ ` Database: ${CTX_DIR5}/${DB_FILENAME4} (${formatBytes2(output.dbSizeBytes)})`
3097
3286
  ];
3098
3287
  if (output.lastIndexed) {
3099
3288
  lines.push(` Last indexed: ${formatTimestamp(output.lastIndexed)}`);
@@ -3116,7 +3305,7 @@ function formatStatus(projectPath, output) {
3116
3305
  if (output.config) {
3117
3306
  lines.push("");
3118
3307
  lines.push(
3119
- ` Embedder: local (${output.config.model}, ${output.config.dimensions} dims)`
3308
+ ` Embedder: ${output.config.provider} (${output.config.model}, ${output.config.dimensions} dims)`
3120
3309
  );
3121
3310
  }
3122
3311
  lines.push("");
@@ -3124,7 +3313,7 @@ function formatStatus(projectPath, output) {
3124
3313
  }
3125
3314
  async function runStatus(projectPath) {
3126
3315
  const absoluteRoot = path8.resolve(projectPath);
3127
- const ctxDir = path8.join(absoluteRoot, CTX_DIR4);
3316
+ const ctxDir = path8.join(absoluteRoot, CTX_DIR5);
3128
3317
  const dbPath = path8.join(ctxDir, DB_FILENAME4);
3129
3318
  if (!fs9.existsSync(dbPath)) {
3130
3319
  const output = {
@@ -3147,7 +3336,7 @@ async function runStatus(projectPath) {
3147
3336
  const vectorCount = db.getVectorCount();
3148
3337
  const languages = db.getLanguageBreakdown();
3149
3338
  const lastIndexed = db.getLastIndexed();
3150
- const config = readConfig(ctxDir);
3339
+ const config = readConfig2(ctxDir);
3151
3340
  const dbSizeBytes = fs9.statSync(dbPath).size;
3152
3341
  const output = {
3153
3342
  initialized: true,