kotadb 2.2.0-next.20260204230500 → 2.2.0-next.20260204235102

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kotadb",
3
- "version": "2.2.0-next.20260204230500",
3
+ "version": "2.2.0-next.20260204235102",
4
4
  "description": "Local-only code intelligence tool for CLI agents. SQLite-backed repository indexing and code search via MCP.",
5
5
  "type": "module",
6
6
  "module": "src/index.ts",
@@ -1409,3 +1409,75 @@ export async function createDefaultOrganization(
1409
1409
  ): Promise<string> {
1410
1410
  throw new Error('createDefaultOrganization() is not available in local-only mode - organizations are a cloud-only feature');
1411
1411
  }
1412
+
1413
+ /**
1414
+ * Get index statistics for startup context display.
1415
+ * Queries counts of indexed files, symbols, references, and memory entries.
1416
+ *
1417
+ * @param db - Database instance (for testability)
1418
+ * @returns Statistics object with counts by type
1419
+ */
1420
+ function getIndexStatisticsInternal(db: KotaDatabase): {
1421
+ files: number;
1422
+ symbols: number;
1423
+ references: number;
1424
+ decisions: number;
1425
+ patterns: number;
1426
+ failures: number;
1427
+ repositories: number;
1428
+ } {
1429
+ const stats = {
1430
+ files: 0,
1431
+ symbols: 0,
1432
+ references: 0,
1433
+ decisions: 0,
1434
+ patterns: 0,
1435
+ failures: 0,
1436
+ repositories: 0,
1437
+ };
1438
+
1439
+ // Helper function to safely query count with fallback for missing tables
1440
+ const safeCount = (tableName: string): number => {
1441
+ try {
1442
+ const result = db.queryOne<{ count: number }>(
1443
+ `SELECT COUNT(*) as count FROM ${tableName}`
1444
+ );
1445
+ return result?.count || 0;
1446
+ } catch (error) {
1447
+ // Table doesn't exist yet (e.g., memory layer tables)
1448
+ return 0;
1449
+ }
1450
+ };
1451
+
1452
+ // Count indexed files
1453
+ stats.files = safeCount('indexed_files');
1454
+
1455
+ // Count indexed symbols
1456
+ stats.symbols = safeCount('indexed_symbols');
1457
+
1458
+ // Count references
1459
+ stats.references = safeCount('indexed_references');
1460
+
1461
+ // Count decisions (may not exist in all installations)
1462
+ stats.decisions = safeCount('kota_decisions');
1463
+
1464
+ // Count patterns (may not exist in all installations)
1465
+ stats.patterns = safeCount('kota_patterns');
1466
+
1467
+ // Count failures (may not exist in all installations)
1468
+ stats.failures = safeCount('kota_failures');
1469
+
1470
+ // Count repositories
1471
+ stats.repositories = safeCount('repositories');
1472
+
1473
+ return stats;
1474
+ }
1475
+
1476
+ /**
1477
+ * Get index statistics for startup context display (public API).
1478
+ *
1479
+ * @returns Statistics object with counts by type
1480
+ */
1481
+ export function getIndexStatistics(): ReturnType<typeof getIndexStatisticsInternal> {
1482
+ return getIndexStatisticsInternal(getGlobalDatabase());
1483
+ }
package/src/mcp/server.ts CHANGED
@@ -16,6 +16,7 @@ import { Sentry } from "../instrument.js";
16
16
  import {
17
17
  ANALYZE_CHANGE_IMPACT_TOOL,
18
18
  GENERATE_TASK_CONTEXT_TOOL,
19
+ GET_INDEX_STATISTICS_TOOL,
19
20
  INDEX_REPOSITORY_TOOL,
20
21
  LIST_RECENT_FILES_TOOL,
21
22
  SEARCH_TOOL,
@@ -35,6 +36,7 @@ import {
35
36
  // Execute functions
36
37
  executeAnalyzeChangeImpact,
37
38
  executeGenerateTaskContext,
39
+ executeGetIndexStatistics,
38
40
  executeIndexRepository,
39
41
  executeListRecentFiles,
40
42
  executeSearch,
package/src/mcp/tools.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import {
9
+ getIndexStatistics,
9
10
  listRecentFiles,
10
11
  queryDependencies,
11
12
  queryDependents,
@@ -353,6 +354,21 @@ export const ANALYZE_CHANGE_IMPACT_TOOL: ToolDefinition = {
353
354
  },
354
355
  };
355
356
 
357
+ /**
358
+ * Tool: get_index_statistics
359
+ */
360
+ export const GET_INDEX_STATISTICS_TOOL: ToolDefinition = {
361
+ tier: "core",
362
+ name: "get_index_statistics",
363
+ description:
364
+ "Get statistics about indexed data (files, symbols, references, decisions, patterns, failures). Useful for understanding what data is available for search.",
365
+ inputSchema: {
366
+ type: "object",
367
+ properties: {},
368
+ required: [],
369
+ },
370
+ };
371
+
356
372
  /**
357
373
  * Tool: validate_implementation_spec
358
374
  */
@@ -770,6 +786,7 @@ export function getToolDefinitions(): ToolDefinition[] {
770
786
  LIST_RECENT_FILES_TOOL,
771
787
  SEARCH_DEPENDENCIES_TOOL,
772
788
  ANALYZE_CHANGE_IMPACT_TOOL,
789
+ GET_INDEX_STATISTICS_TOOL,
773
790
  VALIDATE_IMPLEMENTATION_SPEC_TOOL,
774
791
  SYNC_EXPORT_TOOL,
775
792
  SYNC_IMPORT_TOOL,
@@ -958,11 +975,121 @@ async function searchSymbols(
958
975
  }));
959
976
  }
960
977
 
978
+ /**
979
+ * Generate contextual tips based on search query and parameters.
980
+ * Uses static pattern matching (no NLP) to detect suboptimal usage patterns.
981
+ *
982
+ * Tip frequency: MODERATE - show tips frequently including "nice to know" suggestions.
983
+ *
984
+ * @param query - Search query string
985
+ * @param scopes - Search scopes used
986
+ * @param filters - Normalized filters applied
987
+ * @param scopeResults - Results by scope
988
+ * @returns Array of tip strings (empty if search is optimal)
989
+ */
990
+ function generateSearchTips(
991
+ query: string,
992
+ scopes: string[],
993
+ filters: NormalizedFilters,
994
+ scopeResults: Record<string, unknown[]>
995
+ ): string[] {
996
+ const tips: string[] = [];
997
+ const queryLower = query.toLowerCase();
998
+
999
+ // Pattern 1: Query contains structural keywords but not using symbols scope
1000
+ const structuralKeywords = ['function', 'class', 'interface', 'type', 'method', 'component'];
1001
+ const hasStructuralKeyword = structuralKeywords.some(kw => queryLower.includes(kw));
1002
+
1003
+ if (hasStructuralKeyword && !scopes.includes('symbols')) {
1004
+ const matchedKeyword = structuralKeywords.find(kw => queryLower.includes(kw)) || 'function';
1005
+ tips.push(
1006
+ `You searched for "${query}" in code. Try scope: ['symbols'] with filters: {symbol_kind: ['${matchedKeyword}']} for precise structural discovery.`
1007
+ );
1008
+ }
1009
+
1010
+ // Pattern 2: Query looks like a file path but using code search
1011
+ const looksLikeFilePath = /^[\w\-./]+\.(ts|tsx|js|jsx|py|rs|go|java)$/i.test(query);
1012
+ if (looksLikeFilePath && scopes.includes('code')) {
1013
+ tips.push(
1014
+ `Query "${query}" looks like a file path. Consider using search_dependencies tool to find files that depend on this file or its dependencies.`
1015
+ );
1016
+ }
1017
+
1018
+ // Pattern 3: Symbol search without exported_only filter
1019
+ if (scopes.includes('symbols') && filters.exported_only === undefined) {
1020
+ const symbolCount = scopeResults['symbols']?.length || 0;
1021
+ if (symbolCount > 10) {
1022
+ tips.push(
1023
+ `Found ${symbolCount} symbols. Add filters: {exported_only: true} to narrow to public API only.`
1024
+ );
1025
+ }
1026
+ }
1027
+
1028
+ // Pattern 4: No repository filter with large result set
1029
+ if (!filters.repositoryId) {
1030
+ const totalResults = Object.values(scopeResults).reduce((sum, arr) => sum + arr.length, 0);
1031
+ if (totalResults > 20) {
1032
+ tips.push(
1033
+ `Found ${totalResults} results across all repositories. Add filters: {repository: "owner/repo"} to narrow to a specific repository.`
1034
+ );
1035
+ }
1036
+ }
1037
+
1038
+ // Pattern 5: Code search without glob/language filters
1039
+ if (scopes.includes('code') && !filters.glob && !filters.language) {
1040
+ const codeCount = scopeResults['code']?.length || 0;
1041
+ if (codeCount > 15) {
1042
+ tips.push(
1043
+ `Found ${codeCount} code results. Try filters: {glob: "**/*.ts"} or {language: "typescript"} to narrow file types.`
1044
+ );
1045
+ }
1046
+ }
1047
+
1048
+ // Pattern 6: Suggest decisions scope for "why" questions
1049
+ if (/\b(why|reason|decision|chose|choice)\b/i.test(query) && !scopes.includes('decisions')) {
1050
+ tips.push(
1051
+ `Query contains "why/reason/decision". Try scope: ['decisions'] to search architectural decisions and rationale.`
1052
+ );
1053
+ }
1054
+
1055
+ // Pattern 7: Suggest patterns scope for "how" questions
1056
+ if (/\b(how|pattern|best practice|convention)\b/i.test(query) && !scopes.includes('patterns')) {
1057
+ tips.push(
1058
+ `Query asks "how to". Try scope: ['patterns'] to search coding patterns and conventions from this codebase.`
1059
+ );
1060
+ }
1061
+
1062
+ // Pattern 8: Suggest failures scope for error-related queries
1063
+ if (/\b(error|bug|fail|issue|problem|fix)\b/i.test(query) && !scopes.includes('failures')) {
1064
+ tips.push(
1065
+ `Query mentions errors/issues. Try scope: ['failures'] to learn from past mistakes and avoid repeated failures.`
1066
+ );
1067
+ }
1068
+
1069
+ // Pattern 9: Single scope when multi-scope could be useful
1070
+ if (scopes.length === 1 && scopes[0] === 'code') {
1071
+ tips.push(
1072
+ `Tip: You can search multiple scopes simultaneously. Try scope: ['code', 'symbols'] for broader discovery.`
1073
+ );
1074
+ }
1075
+
1076
+ // Pattern 10: Suggest compact format for large result sets
1077
+ const totalResults = Object.values(scopeResults).reduce((sum, arr) => sum + arr.length, 0);
1078
+ if (totalResults > 30 && !tips.some(t => t.includes('output: "compact"'))) {
1079
+ tips.push(
1080
+ `Returning ${totalResults} full results. Use output: "compact" for summary view or output: "paths" for file paths only.`
1081
+ );
1082
+ }
1083
+
1084
+ return tips;
1085
+ }
1086
+
961
1087
  function formatSearchResults(
962
1088
  query: string,
963
1089
  scopes: string[],
964
1090
  scopeResults: Record<string, unknown[]>,
965
- format: string
1091
+ format: string,
1092
+ filters: NormalizedFilters
966
1093
  ): Record<string, unknown> {
967
1094
  const response: Record<string, unknown> = {
968
1095
  query,
@@ -1007,6 +1134,13 @@ function formatSearchResults(
1007
1134
  (response.counts as Record<string, unknown>).total = ((response.counts as Record<string, unknown>).total as number) + items.length;
1008
1135
  }
1009
1136
 
1137
+
1138
+ // Generate and add tips if applicable
1139
+ const tips = generateSearchTips(query, scopes, filters, scopeResults);
1140
+ if (tips.length > 0) {
1141
+ response.tips = tips;
1142
+ }
1143
+
1010
1144
  return response;
1011
1145
  }
1012
1146
 
@@ -1142,7 +1276,7 @@ export async function executeSearch(
1142
1276
  await Promise.all(searchPromises);
1143
1277
 
1144
1278
  // Format output
1145
- const response = formatSearchResults(p.query as string, scopes, results, output);
1279
+ const response = formatSearchResults(p.query as string, scopes, results, output, filters);
1146
1280
 
1147
1281
  logger.info("Unified search completed", {
1148
1282
  query: p.query,
@@ -1634,6 +1768,26 @@ export async function executeAnalyzeChangeImpact(
1634
1768
  return result;
1635
1769
  }
1636
1770
 
1771
+ /**
1772
+ * Execute get_index_statistics tool
1773
+ */
1774
+ export async function executeGetIndexStatistics(
1775
+ params: unknown,
1776
+ requestId: string | number,
1777
+ userId: string,
1778
+ ): Promise<unknown> {
1779
+ // No parameters to validate
1780
+
1781
+ logger.info("Getting index statistics", { request_id: String(requestId), user_id: userId });
1782
+
1783
+ const stats = getIndexStatistics();
1784
+
1785
+ return {
1786
+ ...stats,
1787
+ summary: `${stats.symbols.toLocaleString()} symbols, ${stats.files.toLocaleString()} files, ${stats.repositories} repositories indexed`,
1788
+ };
1789
+ }
1790
+
1637
1791
  /**
1638
1792
 
1639
1793
  /**