kontext-engine 0.1.3 → 0.1.4

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/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 fs5 from "fs";
8
- import path4 from "path";
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";
@@ -1336,33 +1337,230 @@ function getMetaVersion(db) {
1336
1337
  }
1337
1338
  }
1338
1339
 
1339
- // src/cli/commands/init.ts
1340
+ // src/cli/commands/config.ts
1341
+ import fs5 from "fs";
1342
+ import path4 from "path";
1340
1343
  var CTX_DIR = ".ctx";
1341
- var DB_FILENAME = "index.db";
1342
1344
  var CONFIG_FILENAME = "config.json";
1345
+ var DEFAULT_CONFIG = {
1346
+ embedder: {
1347
+ provider: "local",
1348
+ model: "Xenova/all-MiniLM-L6-v2",
1349
+ dimensions: 384
1350
+ },
1351
+ search: {
1352
+ defaultLimit: 10,
1353
+ strategies: ["vector", "fts", "ast", "path"],
1354
+ weights: { vector: 1, fts: 0.8, ast: 0.9, path: 0.7, dependency: 0.6 }
1355
+ },
1356
+ watch: {
1357
+ debounceMs: 500,
1358
+ ignored: []
1359
+ },
1360
+ llm: {
1361
+ provider: null,
1362
+ model: null
1363
+ }
1364
+ };
1365
+ var VALID_EMBEDDER_PROVIDERS = /* @__PURE__ */ new Set(["local", "voyage", "openai"]);
1366
+ var VALID_LLM_PROVIDERS = /* @__PURE__ */ new Set(["gemini", "openai", "anthropic"]);
1367
+ var VALIDATION_RULES = {
1368
+ "embedder.provider": {
1369
+ validate: (v) => typeof v === "string" && VALID_EMBEDDER_PROVIDERS.has(v),
1370
+ message: `Must be one of: ${[...VALID_EMBEDDER_PROVIDERS].join(", ")}`
1371
+ },
1372
+ "embedder.dimensions": {
1373
+ validate: (v) => typeof v === "number" && v > 0 && Number.isInteger(v),
1374
+ message: "Must be a positive integer"
1375
+ },
1376
+ "search.defaultLimit": {
1377
+ validate: (v) => typeof v === "number" && v > 0 && Number.isInteger(v),
1378
+ message: "Must be a positive integer"
1379
+ },
1380
+ "watch.debounceMs": {
1381
+ validate: (v) => typeof v === "number" && v >= 0 && Number.isInteger(v),
1382
+ message: "Must be a non-negative integer"
1383
+ },
1384
+ "llm.provider": {
1385
+ validate: (v) => v === null || typeof v === "string" && VALID_LLM_PROVIDERS.has(v),
1386
+ message: `Must be null or one of: ${[...VALID_LLM_PROVIDERS].join(", ")}`
1387
+ }
1388
+ };
1389
+ function resolveCtxDir(projectPath) {
1390
+ const absoluteRoot = path4.resolve(projectPath);
1391
+ const ctxDir = path4.join(absoluteRoot, CTX_DIR);
1392
+ if (!fs5.existsSync(ctxDir)) {
1393
+ throw new ConfigError(
1394
+ `Project not initialized. Run "ctx init" first. (${CTX_DIR}/ not found)`,
1395
+ ErrorCode.NOT_INITIALIZED
1396
+ );
1397
+ }
1398
+ return ctxDir;
1399
+ }
1400
+ function configPath(ctxDir) {
1401
+ return path4.join(ctxDir, CONFIG_FILENAME);
1402
+ }
1403
+ function readConfig(ctxDir) {
1404
+ const filePath = configPath(ctxDir);
1405
+ if (!fs5.existsSync(filePath)) {
1406
+ writeConfig(ctxDir, DEFAULT_CONFIG);
1407
+ return structuredClone(DEFAULT_CONFIG);
1408
+ }
1409
+ const raw = fs5.readFileSync(filePath, "utf-8");
1410
+ const parsed = JSON.parse(raw);
1411
+ return mergeWithDefaults(parsed);
1412
+ }
1413
+ function writeConfig(ctxDir, config) {
1414
+ fs5.writeFileSync(
1415
+ configPath(ctxDir),
1416
+ JSON.stringify(config, null, 2) + "\n"
1417
+ );
1418
+ }
1419
+ function mergeWithDefaults(partial) {
1420
+ return {
1421
+ embedder: { ...DEFAULT_CONFIG.embedder, ...partial.embedder },
1422
+ search: {
1423
+ ...DEFAULT_CONFIG.search,
1424
+ ...partial.search,
1425
+ weights: { ...DEFAULT_CONFIG.search.weights, ...partial.search?.weights }
1426
+ },
1427
+ watch: { ...DEFAULT_CONFIG.watch, ...partial.watch },
1428
+ llm: { ...DEFAULT_CONFIG.llm, ...partial.llm }
1429
+ };
1430
+ }
1431
+ function getNestedValue(obj, key) {
1432
+ const parts = key.split(".");
1433
+ let current = obj;
1434
+ for (const part of parts) {
1435
+ if (current === null || current === void 0 || typeof current !== "object") {
1436
+ return void 0;
1437
+ }
1438
+ current = current[part];
1439
+ }
1440
+ return current;
1441
+ }
1442
+ function setNestedValue(obj, key, value) {
1443
+ const parts = key.split(".");
1444
+ let current = obj;
1445
+ for (let i = 0; i < parts.length - 1; i++) {
1446
+ const part = parts[i];
1447
+ if (typeof current[part] !== "object" || current[part] === null) {
1448
+ current[part] = {};
1449
+ }
1450
+ current = current[part];
1451
+ }
1452
+ current[parts[parts.length - 1]] = value;
1453
+ }
1454
+ function parseValue(rawValue) {
1455
+ if (rawValue === "null") return null;
1456
+ if (rawValue === "true") return true;
1457
+ if (rawValue === "false") return false;
1458
+ const num = Number(rawValue);
1459
+ if (!Number.isNaN(num) && rawValue.trim() !== "") return num;
1460
+ if (rawValue.startsWith("[") || rawValue.startsWith("{")) {
1461
+ try {
1462
+ return JSON.parse(rawValue);
1463
+ } catch {
1464
+ }
1465
+ }
1466
+ return rawValue;
1467
+ }
1468
+ function runConfigShow(projectPath) {
1469
+ const ctxDir = resolveCtxDir(projectPath);
1470
+ const config = readConfig(ctxDir);
1471
+ return {
1472
+ config,
1473
+ text: JSON.stringify(config, null, 2)
1474
+ };
1475
+ }
1476
+ function runConfigGet(projectPath, key) {
1477
+ const ctxDir = resolveCtxDir(projectPath);
1478
+ const config = readConfig(ctxDir);
1479
+ return getNestedValue(config, key);
1480
+ }
1481
+ function runConfigSet(projectPath, key, rawValue) {
1482
+ const ctxDir = resolveCtxDir(projectPath);
1483
+ const config = readConfig(ctxDir);
1484
+ const value = parseValue(rawValue);
1485
+ const rule = VALIDATION_RULES[key];
1486
+ if (rule && !rule.validate(value)) {
1487
+ throw new ConfigError(`Invalid value for "${key}": ${rule.message}`, ErrorCode.CONFIG_INVALID);
1488
+ }
1489
+ setNestedValue(config, key, value);
1490
+ writeConfig(ctxDir, config);
1491
+ }
1492
+ function runConfigReset(projectPath) {
1493
+ const ctxDir = resolveCtxDir(projectPath);
1494
+ writeConfig(ctxDir, structuredClone(DEFAULT_CONFIG));
1495
+ }
1496
+ function registerConfigCommand(program2) {
1497
+ const cmd = program2.command("config").description("Show or modify configuration");
1498
+ function configErrorHandler(err) {
1499
+ const verbose = program2.opts()["verbose"] === true;
1500
+ const logger = createLogger({ level: verbose ? LogLevel.DEBUG : LogLevel.INFO });
1501
+ process.exitCode = handleCommandError(err, logger, verbose);
1502
+ }
1503
+ cmd.command("show").description("Show current configuration").action(() => {
1504
+ try {
1505
+ const output = runConfigShow(process.cwd());
1506
+ console.log(output.text);
1507
+ } catch (err) {
1508
+ configErrorHandler(err);
1509
+ }
1510
+ });
1511
+ cmd.command("get <key>").description("Get a configuration value (dot notation)").action((key) => {
1512
+ try {
1513
+ const value = runConfigGet(process.cwd(), key);
1514
+ console.log(
1515
+ typeof value === "object" ? JSON.stringify(value, null, 2) : String(value)
1516
+ );
1517
+ } catch (err) {
1518
+ configErrorHandler(err);
1519
+ }
1520
+ });
1521
+ cmd.command("set <key> <value>").description("Set a configuration value (dot notation)").action((key, value) => {
1522
+ try {
1523
+ runConfigSet(process.cwd(), key, value);
1524
+ console.log(`Set ${key} = ${value}`);
1525
+ } catch (err) {
1526
+ configErrorHandler(err);
1527
+ }
1528
+ });
1529
+ cmd.command("reset").description("Reset configuration to defaults").action(() => {
1530
+ try {
1531
+ runConfigReset(process.cwd());
1532
+ console.log("Configuration reset to defaults.");
1533
+ } catch (err) {
1534
+ configErrorHandler(err);
1535
+ }
1536
+ });
1537
+ }
1538
+
1539
+ // src/cli/commands/init.ts
1540
+ var CTX_DIR2 = ".ctx";
1541
+ var DB_FILENAME = "index.db";
1542
+ var CONFIG_FILENAME2 = "config.json";
1343
1543
  var GITIGNORE_ENTRY = ".ctx/";
1344
1544
  function ensureGitignore(projectRoot) {
1345
- const gitignorePath = path4.join(projectRoot, ".gitignore");
1346
- if (fs5.existsSync(gitignorePath)) {
1347
- const content = fs5.readFileSync(gitignorePath, "utf-8");
1545
+ const gitignorePath = path5.join(projectRoot, ".gitignore");
1546
+ if (fs6.existsSync(gitignorePath)) {
1547
+ const content = fs6.readFileSync(gitignorePath, "utf-8");
1348
1548
  if (content.includes(GITIGNORE_ENTRY)) return;
1349
1549
  const suffix = content.endsWith("\n") ? "" : "\n";
1350
- fs5.writeFileSync(gitignorePath, `${content}${suffix}${GITIGNORE_ENTRY}
1550
+ fs6.writeFileSync(gitignorePath, `${content}${suffix}${GITIGNORE_ENTRY}
1351
1551
  `);
1352
1552
  } else {
1353
- fs5.writeFileSync(gitignorePath, `${GITIGNORE_ENTRY}
1553
+ fs6.writeFileSync(gitignorePath, `${GITIGNORE_ENTRY}
1354
1554
  `);
1355
1555
  }
1356
1556
  }
1357
1557
  function ensureConfig(ctxDir) {
1358
- const configPath2 = path4.join(ctxDir, CONFIG_FILENAME);
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");
1558
+ const configPath2 = path5.join(ctxDir, CONFIG_FILENAME2);
1559
+ if (fs6.existsSync(configPath2)) return;
1560
+ fs6.writeFileSync(
1561
+ configPath2,
1562
+ JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n"
1563
+ );
1366
1564
  }
1367
1565
  function formatDuration(ms) {
1368
1566
  if (ms < 1e3) return `${Math.round(ms)}ms`;
@@ -1379,14 +1577,14 @@ function formatLanguageSummary(counts) {
1379
1577
  }
1380
1578
  async function runInit(projectPath, options = {}) {
1381
1579
  const log = options.log ?? console.log;
1382
- const absoluteRoot = path4.resolve(projectPath);
1580
+ const absoluteRoot = path5.resolve(projectPath);
1383
1581
  const start = performance.now();
1384
1582
  log(`Indexing ${absoluteRoot}...`);
1385
- const ctxDir = path4.join(absoluteRoot, CTX_DIR);
1386
- if (!fs5.existsSync(ctxDir)) fs5.mkdirSync(ctxDir, { recursive: true });
1583
+ const ctxDir = path5.join(absoluteRoot, CTX_DIR2);
1584
+ if (!fs6.existsSync(ctxDir)) fs6.mkdirSync(ctxDir, { recursive: true });
1387
1585
  ensureGitignore(absoluteRoot);
1388
1586
  ensureConfig(ctxDir);
1389
- const dbPath = path4.join(ctxDir, DB_FILENAME);
1587
+ const dbPath = path5.join(ctxDir, DB_FILENAME);
1390
1588
  const db = createDatabase(dbPath);
1391
1589
  try {
1392
1590
  const discovered = await discoverFiles({
@@ -1491,13 +1689,13 @@ async function runInit(projectPath, options = {}) {
1491
1689
  vectorsCreated = vectors.length;
1492
1690
  }
1493
1691
  const durationMs = performance.now() - start;
1494
- const dbSize = fs5.existsSync(dbPath) ? fs5.statSync(dbPath).size : 0;
1692
+ const dbSize = fs6.existsSync(dbPath) ? fs6.statSync(dbPath).size : 0;
1495
1693
  log("");
1496
1694
  log(`\u2713 Indexed in ${formatDuration(durationMs)}`);
1497
1695
  log(
1498
1696
  ` ${discovered.length} files \u2192 ${allChunksWithMeta.length} chunks` + (vectorsCreated > 0 ? ` \u2192 ${vectorsCreated} vectors` : "")
1499
1697
  );
1500
- log(` Database: ${CTX_DIR}/${DB_FILENAME} (${formatBytes(dbSize)})`);
1698
+ log(` Database: ${CTX_DIR2}/${DB_FILENAME} (${formatBytes(dbSize)})`);
1501
1699
  return {
1502
1700
  filesDiscovered: discovered.length,
1503
1701
  filesAdded: changes.added.length,
@@ -1535,8 +1733,8 @@ function registerInitCommand(program2) {
1535
1733
  }
1536
1734
 
1537
1735
  // src/cli/commands/query.ts
1538
- import fs6 from "fs";
1539
- import path5 from "path";
1736
+ import fs7 from "fs";
1737
+ import path6 from "path";
1540
1738
 
1541
1739
  // src/search/vector.ts
1542
1740
  function distanceToScore(distance) {
@@ -1577,7 +1775,15 @@ async function vectorSearch(db, embedder, query, limit, filters) {
1577
1775
 
1578
1776
  // src/search/fts.ts
1579
1777
  function sanitizeFtsQuery(query) {
1580
- return query.replace(/[?()":^~{}!+\-\\]/g, " ").replace(/(?<!\w)\*/g, " ").replace(/\s+/g, " ").trim();
1778
+ const tokenized = query.replace(/[^A-Za-z0-9_*]+/g, " ").trim();
1779
+ if (tokenized.length === 0) return "";
1780
+ const sanitizedTerms = tokenized.split(/\s+/).map((term) => {
1781
+ const hasTrailingWildcard = /\*+$/.test(term);
1782
+ const base = term.replace(/\*/g, "");
1783
+ if (base.length === 0) return "";
1784
+ return hasTrailingWildcard ? `${base}*` : base;
1785
+ }).filter((term) => term.length > 0);
1786
+ return sanitizedTerms.join(" ");
1581
1787
  }
1582
1788
  function bm25ToScore(rank) {
1583
1789
  return 1 / (1 + Math.abs(rank));
@@ -1722,27 +1928,56 @@ function pathKeywordSearch(db, query, limit) {
1722
1928
  }
1723
1929
  if (scoredPaths.length === 0) return [];
1724
1930
  scoredPaths.sort((a, b) => b.score - a.score);
1725
- const results = [];
1931
+ const matchedFiles = [];
1726
1932
  for (const { filePath, score } of scoredPaths) {
1727
- if (results.length >= limit) break;
1728
1933
  const file = db.getFile(filePath);
1729
1934
  if (!file) continue;
1730
1935
  const chunks = db.getChunksByFile(file.id);
1731
- for (const chunk of chunks) {
1936
+ if (chunks.length === 0) continue;
1937
+ matchedFiles.push({
1938
+ filePath: file.path,
1939
+ language: file.language,
1940
+ score,
1941
+ chunks
1942
+ });
1943
+ }
1944
+ if (matchedFiles.length === 0) return [];
1945
+ const results = [];
1946
+ const pushChunk = (filePath, language, score, chunk) => {
1947
+ results.push({
1948
+ chunkId: chunk.id,
1949
+ filePath,
1950
+ lineStart: chunk.lineStart,
1951
+ lineEnd: chunk.lineEnd,
1952
+ name: chunk.name,
1953
+ type: chunk.type,
1954
+ exported: chunk.exports,
1955
+ text: chunk.text,
1956
+ score,
1957
+ language
1958
+ });
1959
+ };
1960
+ for (const matched of matchedFiles) {
1961
+ if (results.length >= limit) break;
1962
+ pushChunk(
1963
+ matched.filePath,
1964
+ matched.language,
1965
+ matched.score,
1966
+ matched.chunks[0]
1967
+ );
1968
+ }
1969
+ let offset = 1;
1970
+ while (results.length < limit) {
1971
+ let addedInRound = false;
1972
+ for (const matched of matchedFiles) {
1732
1973
  if (results.length >= limit) break;
1733
- results.push({
1734
- chunkId: chunk.id,
1735
- filePath: file.path,
1736
- lineStart: chunk.lineStart,
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
- });
1974
+ const chunk = matched.chunks[offset];
1975
+ if (!chunk) continue;
1976
+ pushChunk(matched.filePath, matched.language, matched.score, chunk);
1977
+ addedInRound = true;
1745
1978
  }
1979
+ if (!addedInRound) break;
1980
+ offset++;
1746
1981
  }
1747
1982
  return results;
1748
1983
  }
@@ -1941,25 +2176,128 @@ function renormalize(results) {
1941
2176
  }));
1942
2177
  }
1943
2178
 
1944
- // src/cli/commands/query.ts
1945
- var CTX_DIR2 = ".ctx";
1946
- var DB_FILENAME2 = "index.db";
1947
- var SNIPPET_MAX_LENGTH = 200;
1948
- var STRATEGY_WEIGHTS = {
1949
- vector: 1,
1950
- fts: 0.8,
1951
- ast: 0.9,
1952
- path: 0.7,
1953
- dependency: 0.6
1954
- };
1955
- function truncateSnippet(text) {
1956
- const oneLine = text.replace(/\n/g, " ").replace(/\s+/g, " ").trim();
1957
- if (oneLine.length <= SNIPPET_MAX_LENGTH) return oneLine;
1958
- return oneLine.slice(0, SNIPPET_MAX_LENGTH) + "...";
1959
- }
1960
- function toOutputResult(r) {
1961
- return {
1962
- file: r.filePath,
2179
+ // src/steering/classify.ts
2180
+ var SYMBOL_CAMEL_RE = /^[a-z][a-zA-Z0-9]*$/;
2181
+ var SYMBOL_PASCAL_RE = /^[A-Z][a-zA-Z0-9]*$/;
2182
+ var SYMBOL_SNAKE_RE = /^[a-z]+(?:_[a-z]+)+$/;
2183
+ var SYMBOL_UPPER_RE = /^[A-Z]+(?:_[A-Z]+)*$/;
2184
+ 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;
2185
+ var QUESTION_WORDS = /* @__PURE__ */ new Set([
2186
+ "how",
2187
+ "what",
2188
+ "where",
2189
+ "why",
2190
+ "when",
2191
+ "which",
2192
+ "show",
2193
+ "explain",
2194
+ "find",
2195
+ "list"
2196
+ ]);
2197
+ var STOP_WORDS = /* @__PURE__ */ new Set([
2198
+ "the",
2199
+ "a",
2200
+ "an",
2201
+ "is",
2202
+ "are",
2203
+ "was",
2204
+ "were",
2205
+ "do",
2206
+ "does",
2207
+ "did",
2208
+ "to",
2209
+ "for",
2210
+ "of",
2211
+ "in",
2212
+ "on",
2213
+ "with",
2214
+ "by",
2215
+ "and",
2216
+ "or"
2217
+ ]);
2218
+ function defaultMultipliers() {
2219
+ return {
2220
+ vector: 1,
2221
+ fts: 1,
2222
+ ast: 1,
2223
+ path: 1,
2224
+ dependency: 1
2225
+ };
2226
+ }
2227
+ function isSymbolQuery(query) {
2228
+ return SYMBOL_CAMEL_RE.test(query) || SYMBOL_PASCAL_RE.test(query) || SYMBOL_SNAKE_RE.test(query) || SYMBOL_UPPER_RE.test(query);
2229
+ }
2230
+ function isPathQuery(query) {
2231
+ return query.includes("/") || PATH_EXTENSION_RE.test(query);
2232
+ }
2233
+ function isNaturalLanguageQuery(query) {
2234
+ const lower = query.toLowerCase();
2235
+ const words = lower.split(/\s+/).filter((w) => w.length > 0);
2236
+ const hasQuestionWord = words.some((w) => QUESTION_WORDS.has(w));
2237
+ const hasStopWord = words.some((w) => STOP_WORDS.has(w));
2238
+ return hasQuestionWord || words.length >= 4 && hasStopWord;
2239
+ }
2240
+ function classifyQuery(query) {
2241
+ const trimmed = query.trim();
2242
+ const multipliers = defaultMultipliers();
2243
+ if (isPathQuery(trimmed)) {
2244
+ multipliers.path = 2;
2245
+ multipliers.ast = 0.5;
2246
+ return { kind: "path", multipliers };
2247
+ }
2248
+ if (isSymbolQuery(trimmed)) {
2249
+ multipliers.ast = 1.5;
2250
+ multipliers.vector = 0.5;
2251
+ return { kind: "symbol", multipliers };
2252
+ }
2253
+ if (isNaturalLanguageQuery(trimmed)) {
2254
+ multipliers.vector = 1.5;
2255
+ multipliers.path = 1.2;
2256
+ multipliers.ast = 0.7;
2257
+ return { kind: "natural_language", multipliers };
2258
+ }
2259
+ return { kind: "keyword", multipliers };
2260
+ }
2261
+
2262
+ // src/cli/commands/query.ts
2263
+ var CTX_DIR3 = ".ctx";
2264
+ var DB_FILENAME2 = "index.db";
2265
+ var SNIPPET_MAX_LENGTH = 200;
2266
+ var STRATEGY_WEIGHTS = {
2267
+ vector: 1,
2268
+ fts: 0.8,
2269
+ ast: 0.9,
2270
+ path: 0.7,
2271
+ dependency: 0.6
2272
+ };
2273
+ function getEffectiveStrategyWeights(query) {
2274
+ const { multipliers } = classifyQuery(query);
2275
+ return {
2276
+ vector: STRATEGY_WEIGHTS.vector * multipliers.vector,
2277
+ fts: STRATEGY_WEIGHTS.fts * multipliers.fts,
2278
+ ast: STRATEGY_WEIGHTS.ast * multipliers.ast,
2279
+ path: STRATEGY_WEIGHTS.path * multipliers.path,
2280
+ dependency: STRATEGY_WEIGHTS.dependency * multipliers.dependency
2281
+ };
2282
+ }
2283
+ function normalizeLimit(limit) {
2284
+ if (!Number.isFinite(limit)) return 0;
2285
+ return Math.max(0, Math.trunc(limit));
2286
+ }
2287
+ function resolveQueryStrategies(query, strategies, source) {
2288
+ if (source !== "default") return strategies;
2289
+ if (classifyQuery(query).kind !== "natural_language") return strategies;
2290
+ if (strategies.includes("vector")) return strategies;
2291
+ return [...strategies, "vector"];
2292
+ }
2293
+ function truncateSnippet(text) {
2294
+ const oneLine = text.replace(/\n/g, " ").replace(/\s+/g, " ").trim();
2295
+ if (oneLine.length <= SNIPPET_MAX_LENGTH) return oneLine;
2296
+ return oneLine.slice(0, SNIPPET_MAX_LENGTH) + "...";
2297
+ }
2298
+ function toOutputResult(r) {
2299
+ return {
2300
+ file: r.filePath,
1963
2301
  lines: [r.lineStart, r.lineEnd],
1964
2302
  name: r.name,
1965
2303
  type: r.type,
@@ -1992,20 +2330,21 @@ function isPathLike(query) {
1992
2330
  return query.includes("/") || query.includes("*") || query.includes(".");
1993
2331
  }
1994
2332
  async function runQuery(projectPath, query, options) {
1995
- const absoluteRoot = path5.resolve(projectPath);
1996
- const dbPath = path5.join(absoluteRoot, CTX_DIR2, DB_FILENAME2);
1997
- if (!fs6.existsSync(dbPath)) {
2333
+ const limit = normalizeLimit(options.limit);
2334
+ const absoluteRoot = path6.resolve(projectPath);
2335
+ const dbPath = path6.join(absoluteRoot, CTX_DIR3, DB_FILENAME2);
2336
+ if (!fs7.existsSync(dbPath)) {
1998
2337
  throw new KontextError(
1999
- `Project not initialized. Run "ctx init" first. (${CTX_DIR2}/${DB_FILENAME2} not found)`,
2338
+ `Project not initialized. Run "ctx init" first. (${CTX_DIR3}/${DB_FILENAME2} not found)`,
2000
2339
  ErrorCode.NOT_INITIALIZED
2001
2340
  );
2002
2341
  }
2003
2342
  const start = performance.now();
2004
2343
  const db = createDatabase(dbPath);
2005
2344
  try {
2006
- const strategyResults = await runStrategies(db, query, options);
2345
+ const strategyResults = await runStrategies(db, query, { ...options, limit });
2007
2346
  const pathBoostTerms = extractPathBoostTerms(query);
2008
- const fused = fusionMergeWithPathBoost(strategyResults, options.limit, pathBoostTerms);
2347
+ const fused = fusionMergeWithPathBoost(strategyResults, limit, pathBoostTerms);
2009
2348
  const outputResults = fused.map(toOutputResult);
2010
2349
  const searchTimeMs = Math.round(performance.now() - start);
2011
2350
  const text = options.format === "text" ? formatTextOutput(query, outputResults) : void 0;
@@ -2027,8 +2366,9 @@ async function runStrategies(db, query, options) {
2027
2366
  const results = [];
2028
2367
  const filters = options.language ? { language: options.language } : void 0;
2029
2368
  const limit = options.limit * 3;
2369
+ const effectiveWeights = getEffectiveStrategyWeights(query);
2030
2370
  for (const strategy of options.strategies) {
2031
- const weight = STRATEGY_WEIGHTS[strategy];
2371
+ const weight = effectiveWeights[strategy];
2032
2372
  const searchResults = await executeStrategy(
2033
2373
  db,
2034
2374
  strategy,
@@ -2084,18 +2424,25 @@ async function loadEmbedder() {
2084
2424
  return embedderInstance;
2085
2425
  }
2086
2426
  function registerQueryCommand(program2) {
2087
- program2.command("query <query>").description("Multi-strategy code search").option("-l, --limit <n>", "Max results", "10").option(
2427
+ program2.command("query <query>").alias("find").description("Multi-strategy code search").option("-l, --limit <n>", "Max results", "10").option(
2088
2428
  "-s, --strategy <list>",
2089
2429
  "Comma-separated strategies: vector,fts,ast,path",
2090
2430
  "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) => {
2431
+ ).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
2432
  const projectPath = process.cwd();
2093
2433
  const verbose = program2.opts()["verbose"] === true;
2094
2434
  const logger = createLogger({ level: verbose ? LogLevel.DEBUG : LogLevel.INFO });
2095
- const strategies = (opts["strategy"] ?? "fts,ast,path").split(",").map((s) => s.trim());
2435
+ const strategySource = command.getOptionValueSource("strategy");
2436
+ const parsedStrategies = (opts["strategy"] ?? "fts,ast,path").split(",").map((s) => s.trim());
2437
+ const strategies = resolveQueryStrategies(
2438
+ query,
2439
+ parsedStrategies,
2440
+ strategySource === "default" ? "default" : strategySource ? "cli" : "unknown"
2441
+ );
2442
+ const limit = normalizeLimit(parseInt(opts["limit"] ?? "10", 10));
2096
2443
  try {
2097
2444
  const output = await runQuery(projectPath, query, {
2098
- limit: parseInt(opts["limit"] ?? "10", 10),
2445
+ limit,
2099
2446
  strategies,
2100
2447
  language: opts["language"],
2101
2448
  format: opts["format"] ?? "json"
@@ -2117,8 +2464,8 @@ function registerQueryCommand(program2) {
2117
2464
  }
2118
2465
 
2119
2466
  // src/cli/commands/ask.ts
2120
- import fs7 from "fs";
2121
- import path6 from "path";
2467
+ import fs8 from "fs";
2468
+ import path7 from "path";
2122
2469
 
2123
2470
  // src/steering/prompts.ts
2124
2471
  var PLAN_SYSTEM_PROMPT = `You are a code-search strategy planner for a TypeScript/JavaScript codebase.
@@ -2348,7 +2695,7 @@ function createAnthropicProvider(apiKey) {
2348
2695
  }
2349
2696
  };
2350
2697
  }
2351
- var STOP_WORDS = /* @__PURE__ */ new Set([
2698
+ var STOP_WORDS2 = /* @__PURE__ */ new Set([
2352
2699
  // Interrogatives & conjunctions
2353
2700
  "how",
2354
2701
  "does",
@@ -2459,6 +2806,80 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
2459
2806
  ]);
2460
2807
  var CODE_IDENT_RE = /^(?:[a-z]+(?:[A-Z][a-z]*)+|[A-Z][a-zA-Z]+|[a-z]+(?:_[a-z]+)+|[A-Z]+(?:_[A-Z]+)+)$/;
2461
2808
  var DOTTED_IDENT_RE = /[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)+/g;
2809
+ var COMMON_STEMS = {
2810
+ authentication: "auth",
2811
+ authorization: "auth",
2812
+ configuration: "config",
2813
+ initialization: "init",
2814
+ initialize: "init",
2815
+ initializing: "init",
2816
+ implementation: "impl",
2817
+ implements: "impl",
2818
+ implementing: "impl",
2819
+ dependency: "dep",
2820
+ dependencies: "dep",
2821
+ middleware: "middleware",
2822
+ validation: "valid",
2823
+ validator: "valid",
2824
+ serialize: "serial",
2825
+ serialization: "serial",
2826
+ deserialize: "deserial",
2827
+ database: "db",
2828
+ logging: "log",
2829
+ logger: "log",
2830
+ testing: "test",
2831
+ handler: "handle",
2832
+ handling: "handle",
2833
+ callback: "callback",
2834
+ subscriber: "subscribe",
2835
+ subscription: "subscribe",
2836
+ rendering: "render",
2837
+ renderer: "render",
2838
+ transformer: "transform",
2839
+ transformation: "transform",
2840
+ connection: "connect",
2841
+ connector: "connect",
2842
+ migration: "migrate",
2843
+ scheduling: "schedule",
2844
+ scheduler: "schedule",
2845
+ parsing: "parse",
2846
+ parser: "parse",
2847
+ routing: "route",
2848
+ router: "route",
2849
+ indexing: "index",
2850
+ indexer: "index"
2851
+ };
2852
+ var STEM_SUFFIXES = [
2853
+ "tion",
2854
+ "sion",
2855
+ "ment",
2856
+ "ness",
2857
+ "ing",
2858
+ "er",
2859
+ "or",
2860
+ "able",
2861
+ "ible",
2862
+ "ity",
2863
+ "ous",
2864
+ "ive",
2865
+ "ful",
2866
+ "less",
2867
+ "ly"
2868
+ ];
2869
+ function getStemVariant(term) {
2870
+ const lower = term.toLowerCase();
2871
+ const mapped = COMMON_STEMS[lower];
2872
+ if (mapped && mapped !== lower) return mapped;
2873
+ if (!/^[a-z][a-z0-9_]*$/.test(lower)) return null;
2874
+ for (const suffix of STEM_SUFFIXES) {
2875
+ if (!lower.endsWith(suffix)) continue;
2876
+ const stem = lower.slice(0, -suffix.length);
2877
+ if (stem.length >= 4 && stem !== lower) {
2878
+ return stem;
2879
+ }
2880
+ }
2881
+ return null;
2882
+ }
2462
2883
  function extractSearchTerms(query) {
2463
2884
  const terms = [];
2464
2885
  const seen = /* @__PURE__ */ new Set();
@@ -2469,16 +2890,23 @@ function extractSearchTerms(query) {
2469
2890
  terms.push(term);
2470
2891
  }
2471
2892
  };
2893
+ const addTermAndVariants = (term) => {
2894
+ addUnique(term);
2895
+ const variant = getStemVariant(term);
2896
+ if (variant && variant !== term.toLowerCase()) {
2897
+ addUnique(variant);
2898
+ }
2899
+ };
2472
2900
  const dottedMatches = query.match(DOTTED_IDENT_RE) ?? [];
2473
- for (const m of dottedMatches) addUnique(m);
2901
+ for (const m of dottedMatches) addTermAndVariants(m);
2474
2902
  const pathTokens = query.split(/\s+/).filter((t) => t.includes("/"));
2475
- for (const p of pathTokens) addUnique(p.replace(/[?!,;]+$/g, ""));
2903
+ for (const p of pathTokens) addTermAndVariants(p.replace(/[?!,;]+$/g, ""));
2476
2904
  const words = query.replace(/[^a-zA-Z0-9_.\s/-]/g, " ").split(/\s+/).filter((w) => w.length >= 2);
2477
2905
  for (const w of words) {
2478
2906
  const lower = w.toLowerCase();
2479
2907
  if (seen.has(lower)) continue;
2480
- if (STOP_WORDS.has(lower) && !CODE_IDENT_RE.test(w)) continue;
2481
- addUnique(w);
2908
+ if (STOP_WORDS2.has(lower) && !CODE_IDENT_RE.test(w)) continue;
2909
+ addTermAndVariants(w);
2482
2910
  }
2483
2911
  if (terms.length === 0) {
2484
2912
  const allWords = query.replace(/[^a-zA-Z0-9_\s]/g, " ").split(/\s+/).filter((w) => w.length >= 2);
@@ -2495,17 +2923,42 @@ var VALID_STRATEGIES = /* @__PURE__ */ new Set([
2495
2923
  "dependency"
2496
2924
  ]);
2497
2925
  function buildFallbackPlan(query) {
2498
- const keywords = extractSearchTerms(query);
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
- ];
2926
+ const strategies = buildFallbackStrategies(query);
2504
2927
  return {
2505
2928
  interpretation: `Searching for: ${query}`,
2506
2929
  strategies
2507
2930
  };
2508
2931
  }
2932
+ function buildFallbackStrategies(query) {
2933
+ const keywords = extractSearchTerms(query);
2934
+ const { multipliers } = classifyQuery(query);
2935
+ return [
2936
+ {
2937
+ strategy: "vector",
2938
+ query,
2939
+ weight: 1 * multipliers.vector,
2940
+ reason: "Semantic search over natural language intent"
2941
+ },
2942
+ {
2943
+ strategy: "fts",
2944
+ query: keywords,
2945
+ weight: 0.8 * multipliers.fts,
2946
+ reason: "Full-text keyword search"
2947
+ },
2948
+ {
2949
+ strategy: "ast",
2950
+ query: keywords,
2951
+ weight: 0.9 * multipliers.ast,
2952
+ reason: "Structural symbol search"
2953
+ },
2954
+ {
2955
+ strategy: "path",
2956
+ query: keywords,
2957
+ weight: 0.7 * multipliers.path,
2958
+ reason: "Path keyword search"
2959
+ }
2960
+ ];
2961
+ }
2509
2962
  function parseSearchPlan(raw, query) {
2510
2963
  const jsonMatch = raw.match(/\{[\s\S]*\}/);
2511
2964
  if (!jsonMatch) return buildFallbackPlan(query);
@@ -2585,10 +3038,14 @@ async function steer(provider, query, limit, searchExecutor) {
2585
3038
  }
2586
3039
 
2587
3040
  // src/cli/commands/ask.ts
2588
- var CTX_DIR3 = ".ctx";
3041
+ var CTX_DIR4 = ".ctx";
2589
3042
  var DB_FILENAME3 = "index.db";
2590
3043
  var SNIPPET_MAX_LENGTH2 = 200;
2591
3044
  var FALLBACK_NOTICE = "No LLM provider configured. Set CTX_GEMINI_KEY, CTX_OPENAI_KEY, or CTX_ANTHROPIC_KEY. Running basic search instead.";
3045
+ function normalizeLimit2(limit) {
3046
+ if (!Number.isFinite(limit)) return 0;
3047
+ return Math.max(0, Math.trunc(limit));
3048
+ }
2592
3049
  var PROVIDER_ENV_MAP = {
2593
3050
  gemini: "CTX_GEMINI_KEY",
2594
3051
  openai: "CTX_OPENAI_KEY",
@@ -2734,12 +3191,7 @@ async function loadEmbedder2() {
2734
3191
  }
2735
3192
  async function fallbackSearch(db, query, limit) {
2736
3193
  const executor = createSearchExecutor(db, query);
2737
- const keywords = extractSearchTerms(query);
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
- ];
3194
+ const fallbackStrategies = buildFallbackStrategies(query);
2743
3195
  const results = await executor(fallbackStrategies, limit);
2744
3196
  return {
2745
3197
  query,
@@ -2756,11 +3208,12 @@ async function fallbackSearch(db, query, limit) {
2756
3208
  };
2757
3209
  }
2758
3210
  async function runAsk(projectPath, query, options) {
2759
- const absoluteRoot = path6.resolve(projectPath);
2760
- const dbPath = path6.join(absoluteRoot, CTX_DIR3, DB_FILENAME3);
2761
- if (!fs7.existsSync(dbPath)) {
3211
+ const limit = normalizeLimit2(options.limit);
3212
+ const absoluteRoot = path7.resolve(projectPath);
3213
+ const dbPath = path7.join(absoluteRoot, CTX_DIR4, DB_FILENAME3);
3214
+ if (!fs8.existsSync(dbPath)) {
2762
3215
  throw new KontextError(
2763
- `Project not initialized. Run "ctx init" first. (${CTX_DIR3}/${DB_FILENAME3} not found)`,
3216
+ `Project not initialized. Run "ctx init" first. (${CTX_DIR4}/${DB_FILENAME3} not found)`,
2764
3217
  ErrorCode.NOT_INITIALIZED
2765
3218
  );
2766
3219
  }
@@ -2768,7 +3221,7 @@ async function runAsk(projectPath, query, options) {
2768
3221
  try {
2769
3222
  const provider = options.provider ?? null;
2770
3223
  if (!provider) {
2771
- const output = await fallbackSearch(db, query, options.limit);
3224
+ const output = await fallbackSearch(db, query, limit);
2772
3225
  output.warning = FALLBACK_NOTICE;
2773
3226
  if (options.format === "text") {
2774
3227
  output.text = formatTextOutput2(output);
@@ -2777,16 +3230,16 @@ async function runAsk(projectPath, query, options) {
2777
3230
  }
2778
3231
  const executor = createSearchExecutor(db, query);
2779
3232
  if (options.noExplain) {
2780
- return await runNoExplain(provider, query, options, executor);
3233
+ return await runNoExplain(provider, query, limit, options, executor);
2781
3234
  }
2782
- return await runWithSteering(provider, query, options, executor);
3235
+ return await runWithSteering(provider, query, limit, options, executor);
2783
3236
  } finally {
2784
3237
  db.close();
2785
3238
  }
2786
3239
  }
2787
- async function runNoExplain(provider, query, options, executor) {
3240
+ async function runNoExplain(provider, query, limit, options, executor) {
2788
3241
  const plan = await planSearch(provider, query);
2789
- const results = await executor(plan.strategies, options.limit);
3242
+ const results = await executor(plan.strategies, limit);
2790
3243
  const output = {
2791
3244
  query,
2792
3245
  interpretation: plan.interpretation,
@@ -2804,8 +3257,8 @@ async function runNoExplain(provider, query, options, executor) {
2804
3257
  }
2805
3258
  return output;
2806
3259
  }
2807
- async function runWithSteering(provider, query, options, executor) {
2808
- const result = await steer(provider, query, options.limit, executor);
3260
+ async function runWithSteering(provider, query, limit, options, executor) {
3261
+ const result = await steer(provider, query, limit, executor);
2809
3262
  const output = {
2810
3263
  query,
2811
3264
  interpretation: result.interpretation,
@@ -2830,9 +3283,10 @@ function registerAskCommand(program2) {
2830
3283
  const logger = createLogger({ level: verbose ? LogLevel.DEBUG : LogLevel.INFO });
2831
3284
  const providerName = opts["provider"];
2832
3285
  const provider = detectProvider(providerName);
3286
+ const limit = normalizeLimit2(parseInt(String(opts["limit"] ?? "10"), 10));
2833
3287
  try {
2834
3288
  const output = await runAsk(projectPath, query, {
2835
- limit: parseInt(String(opts["limit"] ?? "10"), 10),
3289
+ limit,
2836
3290
  format: opts["format"] ?? "text",
2837
3291
  provider: provider ?? void 0,
2838
3292
  noExplain: opts["explain"] === false
@@ -2856,13 +3310,6 @@ function registerAskCommand(program2) {
2856
3310
  });
2857
3311
  }
2858
3312
 
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
3313
  // src/cli/commands/update.ts
2867
3314
  function registerUpdateCommand(program2) {
2868
3315
  program2.command("update").description("Incremental re-index of changed files").action(() => {
@@ -2871,12 +3318,12 @@ function registerUpdateCommand(program2) {
2871
3318
  }
2872
3319
 
2873
3320
  // src/cli/commands/watch.ts
2874
- import fs8 from "fs";
2875
- import path8 from "path";
3321
+ import fs9 from "fs";
3322
+ import path9 from "path";
2876
3323
 
2877
3324
  // src/watcher/watcher.ts
2878
3325
  import { watch } from "chokidar";
2879
- import path7 from "path";
3326
+ import path8 from "path";
2880
3327
  var DEFAULT_DEBOUNCE_MS = 500;
2881
3328
  var ALWAYS_IGNORED_DIRS = /* @__PURE__ */ new Set([
2882
3329
  "node_modules",
@@ -2888,15 +3335,15 @@ var ALWAYS_IGNORED_DIRS = /* @__PURE__ */ new Set([
2888
3335
  ]);
2889
3336
  var WATCHED_EXTENSIONS = new Set(Object.keys(LANGUAGE_MAP));
2890
3337
  function isWatchedFile(filePath) {
2891
- const ext = path7.extname(filePath).toLowerCase();
3338
+ const ext = path8.extname(filePath).toLowerCase();
2892
3339
  return WATCHED_EXTENSIONS.has(ext);
2893
3340
  }
2894
3341
  function createWatcher(options, events) {
2895
3342
  const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
2896
- const projectPath = path7.resolve(options.projectPath);
3343
+ const projectPath = path8.resolve(options.projectPath);
2897
3344
  const extraIgnored = new Set(options.ignored ?? []);
2898
3345
  function isIgnored(filePath) {
2899
- const segments = filePath.split(path7.sep);
3346
+ const segments = filePath.split(path8.sep);
2900
3347
  for (const seg of segments) {
2901
3348
  if (ALWAYS_IGNORED_DIRS.has(seg)) return true;
2902
3349
  if (extraIgnored.has(seg)) return true;
@@ -2954,13 +3401,13 @@ function createWatcher(options, events) {
2954
3401
  }
2955
3402
 
2956
3403
  // src/cli/commands/watch.ts
2957
- var CTX_DIR4 = ".ctx";
3404
+ var CTX_DIR5 = ".ctx";
2958
3405
  var DB_FILENAME4 = "index.db";
2959
3406
  function timestamp() {
2960
3407
  return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false });
2961
3408
  }
2962
3409
  function detectLanguage(filePath) {
2963
- const ext = path8.extname(filePath).toLowerCase();
3410
+ const ext = path9.extname(filePath).toLowerCase();
2964
3411
  return LANGUAGE_MAP[ext] ?? null;
2965
3412
  }
2966
3413
  function formatDuration2(ms) {
@@ -2969,7 +3416,7 @@ function formatDuration2(ms) {
2969
3416
  }
2970
3417
  async function hashFile(absolutePath) {
2971
3418
  const { createHash: createHash3 } = await import("crypto");
2972
- const content = fs8.readFileSync(absolutePath);
3419
+ const content = fs9.readFileSync(absolutePath);
2973
3420
  return createHash3("sha256").update(content).digest("hex");
2974
3421
  }
2975
3422
  async function reindexChanges(db, changes, projectPath, options) {
@@ -2979,7 +3426,7 @@ async function reindexChanges(db, changes, projectPath, options) {
2979
3426
  let chunksUpdated = 0;
2980
3427
  const allChunksWithMeta = [];
2981
3428
  for (const change of changes) {
2982
- const absolutePath = path8.join(projectPath, change.path);
3429
+ const absolutePath = path9.join(projectPath, change.path);
2983
3430
  const language = detectLanguage(change.path);
2984
3431
  if (change.type === "unlink") {
2985
3432
  log(`[${timestamp()}] Deleted: ${change.path}`);
@@ -2991,7 +3438,7 @@ async function reindexChanges(db, changes, projectPath, options) {
2991
3438
  continue;
2992
3439
  }
2993
3440
  if (!language) continue;
2994
- if (!fs8.existsSync(absolutePath)) continue;
3441
+ if (!fs9.existsSync(absolutePath)) continue;
2995
3442
  const label = change.type === "add" ? "Added" : "Changed";
2996
3443
  log(`[${timestamp()}] ${label}: ${change.path}`);
2997
3444
  const existingFile = db.getFile(change.path);
@@ -3007,7 +3454,7 @@ async function reindexChanges(db, changes, projectPath, options) {
3007
3454
  }
3008
3455
  const chunks = chunkFile(nodes, change.path);
3009
3456
  const hash = await hashFile(absolutePath);
3010
- const size = fs8.statSync(absolutePath).size;
3457
+ const size = fs9.statSync(absolutePath).size;
3011
3458
  const fileId = db.upsertFile({
3012
3459
  path: change.path,
3013
3460
  language,
@@ -3060,15 +3507,15 @@ async function loadEmbedder3() {
3060
3507
  return embedderInstance3;
3061
3508
  }
3062
3509
  async function runWatch(projectPath, options = {}) {
3063
- const absoluteRoot = path8.resolve(projectPath);
3064
- const dbPath = path8.join(absoluteRoot, CTX_DIR4, DB_FILENAME4);
3510
+ const absoluteRoot = path9.resolve(projectPath);
3511
+ const dbPath = path9.join(absoluteRoot, CTX_DIR5, DB_FILENAME4);
3065
3512
  const log = options.log ?? console.log;
3066
3513
  if (options.init) {
3067
3514
  await runInit(absoluteRoot, { log, skipEmbedding: options.skipEmbedding });
3068
3515
  }
3069
- if (!fs8.existsSync(dbPath)) {
3516
+ if (!fs9.existsSync(dbPath)) {
3070
3517
  throw new KontextError(
3071
- `Project not initialized. Run "ctx init" first or use --init flag. (${CTX_DIR4}/${DB_FILENAME4} not found)`,
3518
+ `Project not initialized. Run "ctx init" first or use --init flag. (${CTX_DIR5}/${DB_FILENAME4} not found)`,
3072
3519
  ErrorCode.NOT_INITIALIZED
3073
3520
  );
3074
3521
  }
@@ -3149,11 +3596,11 @@ function registerWatchCommand(program2) {
3149
3596
  }
3150
3597
 
3151
3598
  // src/cli/commands/status.ts
3152
- import fs9 from "fs";
3153
- import path9 from "path";
3154
- var CTX_DIR5 = ".ctx";
3599
+ import fs10 from "fs";
3600
+ import path10 from "path";
3601
+ var CTX_DIR6 = ".ctx";
3155
3602
  var DB_FILENAME5 = "index.db";
3156
- var CONFIG_FILENAME2 = "config.json";
3603
+ var CONFIG_FILENAME3 = "config.json";
3157
3604
  function formatBytes2(bytes) {
3158
3605
  if (bytes < 1024) return `${bytes} B`;
3159
3606
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -3168,15 +3615,16 @@ function formatTimestamp(raw) {
3168
3615
  function capitalize(s) {
3169
3616
  return s.charAt(0).toUpperCase() + s.slice(1);
3170
3617
  }
3171
- function readConfig(ctxDir) {
3172
- const configPath2 = path9.join(ctxDir, CONFIG_FILENAME2);
3173
- if (!fs9.existsSync(configPath2)) return null;
3618
+ function readConfig2(ctxDir) {
3619
+ const configPath2 = path10.join(ctxDir, CONFIG_FILENAME3);
3620
+ if (!fs10.existsSync(configPath2)) return null;
3174
3621
  try {
3175
- const raw = fs9.readFileSync(configPath2, "utf-8");
3622
+ const raw = fs10.readFileSync(configPath2, "utf-8");
3176
3623
  const parsed = JSON.parse(raw);
3624
+ const embedder = parsed.embedder;
3177
3625
  return {
3178
- model: parsed.model ?? "unknown",
3179
- dimensions: parsed.dimensions ?? 0
3626
+ model: embedder?.model ?? parsed.model ?? "unknown",
3627
+ dimensions: embedder?.dimensions ?? parsed.dimensions ?? 0
3180
3628
  };
3181
3629
  } catch {
3182
3630
  return null;
@@ -3195,7 +3643,7 @@ function formatStatus(projectPath, output) {
3195
3643
  `Kontext Status \u2014 ${projectPath}`,
3196
3644
  "",
3197
3645
  ` Initialized: Yes`,
3198
- ` Database: ${CTX_DIR5}/${DB_FILENAME5} (${formatBytes2(output.dbSizeBytes)})`
3646
+ ` Database: ${CTX_DIR6}/${DB_FILENAME5} (${formatBytes2(output.dbSizeBytes)})`
3199
3647
  ];
3200
3648
  if (output.lastIndexed) {
3201
3649
  lines.push(` Last indexed: ${formatTimestamp(output.lastIndexed)}`);
@@ -3225,10 +3673,10 @@ function formatStatus(projectPath, output) {
3225
3673
  return lines.join("\n");
3226
3674
  }
3227
3675
  async function runStatus(projectPath) {
3228
- const absoluteRoot = path9.resolve(projectPath);
3229
- const ctxDir = path9.join(absoluteRoot, CTX_DIR5);
3230
- const dbPath = path9.join(ctxDir, DB_FILENAME5);
3231
- if (!fs9.existsSync(dbPath)) {
3676
+ const absoluteRoot = path10.resolve(projectPath);
3677
+ const ctxDir = path10.join(absoluteRoot, CTX_DIR6);
3678
+ const dbPath = path10.join(ctxDir, DB_FILENAME5);
3679
+ if (!fs10.existsSync(dbPath)) {
3232
3680
  const output = {
3233
3681
  initialized: false,
3234
3682
  fileCount: 0,
@@ -3249,8 +3697,8 @@ async function runStatus(projectPath) {
3249
3697
  const vectorCount = db.getVectorCount();
3250
3698
  const languages = db.getLanguageBreakdown();
3251
3699
  const lastIndexed = db.getLastIndexed();
3252
- const config = readConfig(ctxDir);
3253
- const dbSizeBytes = fs9.statSync(dbPath).size;
3700
+ const config = readConfig2(ctxDir);
3701
+ const dbSizeBytes = fs10.statSync(dbPath).size;
3254
3702
  const output = {
3255
3703
  initialized: true,
3256
3704
  fileCount,
@@ -3303,205 +3751,6 @@ function registerChunkCommand(program2) {
3303
3751
  });
3304
3752
  }
3305
3753
 
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
3754
  // src/cli/commands/auth.ts
3506
3755
  function registerAuthCommand(program2) {
3507
3756
  program2.command("auth").description("Set API keys for LLM and embedding providers").action(() => {
@@ -3510,12 +3759,13 @@ function registerAuthCommand(program2) {
3510
3759
  }
3511
3760
 
3512
3761
  // src/cli/index.ts
3762
+ var require3 = createRequire2(import.meta.url);
3763
+ var packageJson = require3("../../package.json");
3513
3764
  var program = new Command();
3514
- program.name("ctx").description("Kontext \u2014 Context engine for AI coding agents").version("0.1.0").option("--verbose", "Enable verbose/debug output");
3765
+ 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
3766
  registerInitCommand(program);
3516
3767
  registerQueryCommand(program);
3517
3768
  registerAskCommand(program);
3518
- registerFindCommand(program);
3519
3769
  registerUpdateCommand(program);
3520
3770
  registerWatchCommand(program);
3521
3771
  registerStatusCommand(program);