kotadb 2.1.0-next.20260203233957 → 2.1.0-next.20260204014250

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.1.0-next.20260203233957",
3
+ "version": "2.1.0-next.20260204014250",
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",
package/src/mcp/server.ts CHANGED
@@ -18,17 +18,14 @@ import {
18
18
  GENERATE_TASK_CONTEXT_TOOL,
19
19
  INDEX_REPOSITORY_TOOL,
20
20
  LIST_RECENT_FILES_TOOL,
21
- SEARCH_CODE_TOOL,
21
+ SEARCH_TOOL,
22
22
  SEARCH_DEPENDENCIES_TOOL,
23
23
  SYNC_EXPORT_TOOL,
24
24
  SYNC_IMPORT_TOOL,
25
25
  VALIDATE_IMPLEMENTATION_SPEC_TOOL,
26
26
  // Memory Layer tools
27
- SEARCH_DECISIONS_TOOL,
28
27
  RECORD_DECISION_TOOL,
29
- SEARCH_FAILURES_TOOL,
30
28
  RECORD_FAILURE_TOOL,
31
- SEARCH_PATTERNS_TOOL,
32
29
  RECORD_INSIGHT_TOOL,
33
30
  // Dynamic Expertise tools
34
31
  GET_DOMAIN_KEY_FILES_TOOL,
@@ -40,17 +37,14 @@ import {
40
37
  executeGenerateTaskContext,
41
38
  executeIndexRepository,
42
39
  executeListRecentFiles,
43
- executeSearchCode,
40
+ executeSearch,
44
41
  executeSearchDependencies,
45
42
  executeSyncExport,
46
43
  executeSyncImport,
47
44
  executeValidateImplementationSpec,
48
45
  // Memory Layer execute functions
49
- executeSearchDecisions,
50
46
  executeRecordDecision,
51
- executeSearchFailures,
52
47
  executeRecordFailure,
53
- executeSearchPatterns,
54
48
  executeRecordInsight,
55
49
  // Dynamic Expertise execute functions
56
50
  executeGetDomainKeyFiles,
@@ -143,8 +137,8 @@ export function createMcpServer(context: McpServerContext): Server {
143
137
 
144
138
  try {
145
139
  switch (name) {
146
- case "search_code":
147
- result = await executeSearchCode(
140
+ case "search":
141
+ result = await executeSearch(
148
142
  toolArgs,
149
143
  "", // requestId not used
150
144
  context.userId,
@@ -199,13 +193,6 @@ export function createMcpServer(context: McpServerContext): Server {
199
193
  );
200
194
  break;
201
195
  // Memory Layer tools
202
- case "search_decisions":
203
- result = await executeSearchDecisions(
204
- toolArgs,
205
- "", // requestId not used
206
- context.userId,
207
- );
208
- break;
209
196
  case "record_decision":
210
197
  result = await executeRecordDecision(
211
198
  toolArgs,
@@ -213,13 +200,6 @@ export function createMcpServer(context: McpServerContext): Server {
213
200
  context.userId,
214
201
  );
215
202
  break;
216
- case "search_failures":
217
- result = await executeSearchFailures(
218
- toolArgs,
219
- "", // requestId not used
220
- context.userId,
221
- );
222
- break;
223
203
  case "record_failure":
224
204
  result = await executeRecordFailure(
225
205
  toolArgs,
@@ -227,13 +207,6 @@ export function createMcpServer(context: McpServerContext): Server {
227
207
  context.userId,
228
208
  );
229
209
  break;
230
- case "search_patterns":
231
- result = await executeSearchPatterns(
232
- toolArgs,
233
- "", // requestId not used
234
- context.userId,
235
- );
236
- break;
237
210
  case "record_insight":
238
211
  result = await executeRecordInsight(
239
212
  toolArgs,
package/src/mcp/tools.ts CHANGED
@@ -99,35 +99,112 @@ export function getToolsByTier(toolset: ToolsetTier): ToolDefinition[] {
99
99
  export function isValidToolset(value: string): value is ToolsetTier {
100
100
  return value === "default" || value === "core" || value === "memory" || value === "full";
101
101
  }
102
+
103
+ // ============================================================================
104
+ // UNIFIED SEARCH TOOL - Replaces search_code, search_symbols, search_decisions, search_patterns, search_failures
105
+ // Issue: #143
106
+ // ============================================================================
107
+
102
108
  /**
103
- /**
104
- * Tool: search_code
109
+ * Tool: search (unified)
105
110
  */
106
- export const SEARCH_CODE_TOOL: ToolDefinition = {
111
+ export const SEARCH_TOOL: ToolDefinition = {
107
112
  tier: "core",
108
- name: "search_code",
113
+ name: "search",
109
114
  description:
110
- "Search indexed code files for a specific term. Returns matching files with context snippets.",
115
+ "Search indexed code, symbols, decisions, patterns, and failures. Supports multiple search scopes simultaneously with scope-specific filters and output formats.",
111
116
  inputSchema: {
112
117
  type: "object",
113
118
  properties: {
114
- term: {
119
+ query: {
115
120
  type: "string",
116
- description: "The search term to find in code files",
121
+ description: "Search query term or phrase",
117
122
  },
118
- repository: {
119
- type: "string",
120
- description: "Optional: Filter results to a specific repository ID",
123
+ scope: {
124
+ type: "array",
125
+ items: {
126
+ type: "string",
127
+ enum: ["code", "symbols", "decisions", "patterns", "failures"],
128
+ },
129
+ description: "Search scopes to query (default: ['code'])",
130
+ },
131
+ filters: {
132
+ type: "object",
133
+ description: "Scope-specific filters (invalid filters ignored)",
134
+ properties: {
135
+ // Code scope filters
136
+ glob: {
137
+ type: "string",
138
+ description: "File path glob pattern (code scope only)",
139
+ },
140
+ exclude: {
141
+ type: "array",
142
+ items: { type: "string" },
143
+ description: "Exclude patterns (code scope only)",
144
+ },
145
+ language: {
146
+ type: "string",
147
+ description: "Programming language filter (code scope only)",
148
+ },
149
+ // Symbol scope filters
150
+ symbol_kind: {
151
+ type: "array",
152
+ items: {
153
+ type: "string",
154
+ enum: [
155
+ "function",
156
+ "class",
157
+ "interface",
158
+ "type",
159
+ "variable",
160
+ "constant",
161
+ "method",
162
+ "property",
163
+ "module",
164
+ "namespace",
165
+ "enum",
166
+ "enum_member",
167
+ ],
168
+ },
169
+ description: "Symbol kinds to include (symbols scope only)",
170
+ },
171
+ exported_only: {
172
+ type: "boolean",
173
+ description: "Only exported symbols (symbols scope only)",
174
+ },
175
+ // Decision scope filters
176
+ decision_scope: {
177
+ type: "string",
178
+ enum: ["architecture", "pattern", "convention", "workaround"],
179
+ description: "Decision category (decisions scope only)",
180
+ },
181
+ // Pattern scope filters
182
+ pattern_type: {
183
+ type: "string",
184
+ description: "Pattern type filter (patterns scope only)",
185
+ },
186
+ // Common filters
187
+ repository: {
188
+ type: "string",
189
+ description: "Repository ID or full_name filter (all scopes)",
190
+ },
191
+ },
121
192
  },
122
193
  limit: {
123
194
  type: "number",
124
- description: "Optional: Maximum number of results (default: 20, max: 100)",
195
+ description: "Max results per scope (default: 20, max: 100)",
196
+ },
197
+ output: {
198
+ type: "string",
199
+ enum: ["full", "paths", "compact"],
200
+ description: "Output format (default: 'full')",
125
201
  },
126
202
  },
127
- required: ["term"],
203
+ required: ["query"],
128
204
  },
129
205
  };
130
206
 
207
+
131
208
  /**
132
209
  * Tool: index_repository
133
210
  */
@@ -446,39 +523,6 @@ export const GENERATE_TASK_CONTEXT_TOOL: ToolDefinition = {
446
523
  // Memory Layer Tool Definitions
447
524
  // ============================================================================
448
525
 
449
- /**
450
- * Tool: search_decisions
451
- */
452
- export const SEARCH_DECISIONS_TOOL: ToolDefinition = {
453
- tier: "memory",
454
- name: "search_decisions",
455
- description:
456
- "Search past architectural decisions using FTS5. Returns decisions with relevance scores.",
457
- inputSchema: {
458
- type: "object",
459
- properties: {
460
- query: {
461
- type: "string",
462
- description: "Search query for decisions",
463
- },
464
- scope: {
465
- type: "string",
466
- enum: ["architecture", "pattern", "convention", "workaround"],
467
- description: "Optional: Filter by decision scope",
468
- },
469
- repository: {
470
- type: "string",
471
- description: "Optional: Filter to a specific repository ID or full_name",
472
- },
473
- limit: {
474
- type: "number",
475
- description: "Optional: Max results (default: 20)",
476
- },
477
- },
478
- required: ["query"],
479
- },
480
- };
481
-
482
526
  /**
483
527
  * Tool: record_decision
484
528
  */
@@ -530,34 +574,6 @@ export const RECORD_DECISION_TOOL: ToolDefinition = {
530
574
  },
531
575
  };
532
576
 
533
- /**
534
- * Tool: search_failures
535
- */
536
- export const SEARCH_FAILURES_TOOL: ToolDefinition = {
537
- tier: "memory",
538
- name: "search_failures",
539
- description:
540
- "Search failed approaches to avoid repeating mistakes. Returns failures with relevance scores.",
541
- inputSchema: {
542
- type: "object",
543
- properties: {
544
- query: {
545
- type: "string",
546
- description: "Search query for failures",
547
- },
548
- repository: {
549
- type: "string",
550
- description: "Optional: Filter to a specific repository ID or full_name",
551
- },
552
- limit: {
553
- type: "number",
554
- description: "Optional: Max results (default: 20)",
555
- },
556
- },
557
- required: ["query"],
558
- },
559
- };
560
-
561
577
  /**
562
578
  * Tool: record_failure
563
579
  */
@@ -599,41 +615,6 @@ export const RECORD_FAILURE_TOOL: ToolDefinition = {
599
615
  },
600
616
  };
601
617
 
602
- /**
603
- * Tool: search_patterns
604
- */
605
- export const SEARCH_PATTERNS_TOOL: ToolDefinition = {
606
- tier: "memory",
607
- name: "search_patterns",
608
- description:
609
- "Find codebase patterns by type or file. Returns discovered patterns for consistency.",
610
- inputSchema: {
611
- type: "object",
612
- properties: {
613
- query: {
614
- type: "string",
615
- description: "Optional: Search query for pattern name/description",
616
- },
617
- pattern_type: {
618
- type: "string",
619
- description: "Optional: Filter by pattern type (e.g., error-handling, api-call)",
620
- },
621
- file: {
622
- type: "string",
623
- description: "Optional: Filter by file path",
624
- },
625
- repository: {
626
- type: "string",
627
- description: "Optional: Filter to a specific repository ID or full_name",
628
- },
629
- limit: {
630
- type: "number",
631
- description: "Optional: Max results (default: 20)",
632
- },
633
- },
634
- },
635
- };
636
-
637
618
  /**
638
619
  * Tool: record_insight
639
620
  */
@@ -784,7 +765,7 @@ export const GET_RECENT_PATTERNS_TOOL: ToolDefinition = {
784
765
  */
785
766
  export function getToolDefinitions(): ToolDefinition[] {
786
767
  return [
787
- SEARCH_CODE_TOOL,
768
+ SEARCH_TOOL,
788
769
  INDEX_REPOSITORY_TOOL,
789
770
  LIST_RECENT_FILES_TOOL,
790
771
  SEARCH_DEPENDENCIES_TOOL,
@@ -794,11 +775,8 @@ export function getToolDefinitions(): ToolDefinition[] {
794
775
  SYNC_IMPORT_TOOL,
795
776
  GENERATE_TASK_CONTEXT_TOOL,
796
777
  // Memory Layer tools
797
- SEARCH_DECISIONS_TOOL,
798
778
  RECORD_DECISION_TOOL,
799
- SEARCH_FAILURES_TOOL,
800
779
  RECORD_FAILURE_TOOL,
801
- SEARCH_PATTERNS_TOOL,
802
780
  RECORD_INSIGHT_TOOL,
803
781
  // Dynamic Expertise tools
804
782
  GET_DOMAIN_KEY_FILES_TOOL,
@@ -821,7 +799,361 @@ function isListRecentParams(params: unknown): params is { limit?: number; reposi
821
799
  return true;
822
800
  }
823
801
 
802
+ // ============================================================================
803
+ // UNIFIED SEARCH - Helper Functions and Types
804
+ // ============================================================================
805
+
806
+ interface NormalizedFilters {
807
+ // Common
808
+ repositoryId?: string;
809
+ // Code
810
+ glob?: string;
811
+ exclude?: string[];
812
+ language?: string;
813
+ // Symbols
814
+ symbol_kind?: string[];
815
+ exported_only?: boolean;
816
+ // Decisions
817
+ decision_scope?: string;
818
+ // Patterns
819
+ pattern_type?: string;
820
+ }
821
+
822
+ function normalizeFilters(filters: unknown): NormalizedFilters {
823
+ if (!filters || typeof filters !== "object") {
824
+ return {};
825
+ }
826
+
827
+ const f = filters as Record<string, unknown>;
828
+ const normalized: NormalizedFilters = {};
829
+
830
+ // Resolve repository (UUID or full_name)
831
+ if (f.repository && typeof f.repository === "string") {
832
+ const resolved = resolveRepositoryIdentifierWithError(f.repository);
833
+ if (!("error" in resolved)) {
834
+ normalized.repositoryId = resolved.id;
835
+ }
836
+ }
837
+
838
+ // Extract typed filters (silently ignore invalid)
839
+ if (f.glob && typeof f.glob === "string") {
840
+ normalized.glob = f.glob;
841
+ }
842
+
843
+ if (Array.isArray(f.exclude)) {
844
+ normalized.exclude = f.exclude.filter(e => typeof e === "string");
845
+ }
846
+
847
+ if (f.language && typeof f.language === "string") {
848
+ normalized.language = f.language;
849
+ }
850
+
851
+ if (Array.isArray(f.symbol_kind)) {
852
+ normalized.symbol_kind = f.symbol_kind.filter(k => typeof k === "string");
853
+ }
854
+
855
+ if (typeof f.exported_only === "boolean") {
856
+ normalized.exported_only = f.exported_only;
857
+ }
858
+
859
+ if (f.decision_scope && typeof f.decision_scope === "string") {
860
+ normalized.decision_scope = f.decision_scope;
861
+ }
862
+
863
+ if (f.pattern_type && typeof f.pattern_type === "string") {
864
+ normalized.pattern_type = f.pattern_type;
865
+ }
866
+
867
+ return normalized;
868
+ }
869
+
870
+ interface SymbolResult {
871
+ id: string;
872
+ name: string;
873
+ kind: string;
874
+ signature: string | null;
875
+ documentation: string | null;
876
+ location: {
877
+ file: string;
878
+ line_start: number;
879
+ line_end: number;
880
+ };
881
+ repository_id: string;
882
+ is_exported: boolean;
883
+ }
884
+
885
+ async function searchSymbols(
886
+ query: string,
887
+ filters: NormalizedFilters,
888
+ limit: number
889
+ ): Promise<SymbolResult[]> {
890
+ const db = getGlobalDatabase();
891
+
892
+ let sql = `
893
+ SELECT
894
+ s.id,
895
+ s.name,
896
+ s.kind,
897
+ s.signature,
898
+ s.documentation,
899
+ s.line_start,
900
+ s.line_end,
901
+ s.metadata,
902
+ f.path as file_path,
903
+ s.repository_id
904
+ FROM indexed_symbols s
905
+ JOIN indexed_files f ON s.file_id = f.id
906
+ WHERE s.name LIKE ?
907
+ `;
908
+
909
+ const params: (string | number)[] = [`%${query}%`];
910
+
911
+ // Apply symbol_kind filter
912
+ if (filters.symbol_kind && filters.symbol_kind.length > 0) {
913
+ const placeholders = filters.symbol_kind.map(() => "?").join(", ");
914
+ sql += ` AND s.kind IN (${placeholders})`;
915
+ params.push(...filters.symbol_kind);
916
+ }
917
+
918
+ // Apply exported_only filter
919
+ if (filters.exported_only) {
920
+ sql += ` AND json_extract(s.metadata, '$.is_exported') = 1`;
921
+ }
922
+
923
+ // Apply repository filter
924
+ if (filters.repositoryId) {
925
+ sql += ` AND s.repository_id = ?`;
926
+ params.push(filters.repositoryId);
927
+ }
928
+
929
+ sql += ` ORDER BY s.name LIMIT ?`;
930
+ params.push(limit);
931
+
932
+ const rows = db.query<{
933
+ id: string;
934
+ name: string;
935
+ kind: string;
936
+ signature: string | null;
937
+ documentation: string | null;
938
+ line_start: number;
939
+ line_end: number;
940
+ metadata: string;
941
+ file_path: string;
942
+ repository_id: string;
943
+ }>(sql, params);
944
+
945
+ return rows.map(row => ({
946
+ id: row.id,
947
+ name: row.name,
948
+ kind: row.kind,
949
+ signature: row.signature,
950
+ documentation: row.documentation,
951
+ location: {
952
+ file: row.file_path,
953
+ line_start: row.line_start,
954
+ line_end: row.line_end,
955
+ },
956
+ repository_id: row.repository_id,
957
+ is_exported: JSON.parse(row.metadata || '{}').is_exported || false,
958
+ }));
959
+ }
960
+
961
+ function formatSearchResults(
962
+ query: string,
963
+ scopes: string[],
964
+ scopeResults: Record<string, unknown[]>,
965
+ format: string
966
+ ): Record<string, unknown> {
967
+ const response: Record<string, unknown> = {
968
+ query,
969
+ scopes,
970
+ results: {} as Record<string, unknown>,
971
+ counts: { total: 0 } as Record<string, unknown>,
972
+ };
973
+
974
+ for (const scope of scopes) {
975
+ const items = scopeResults[scope] || [];
976
+
977
+ if (format === "paths") {
978
+ // Extract file paths only
979
+ (response.results as Record<string, unknown>)[scope] = items.map((item: any) => {
980
+ if (item.path) return item.path;
981
+ if (item.file_path) return item.file_path;
982
+ if (item.location?.file) return item.location.file;
983
+ return "unknown";
984
+ });
985
+ } else if (format === "compact") {
986
+ // Summary info only
987
+ (response.results as Record<string, unknown>)[scope] = items.map((item: any) => {
988
+ if (scope === "code") {
989
+ return { path: item.path, match_count: 1 };
990
+ } else if (scope === "symbols") {
991
+ return { name: item.name, kind: item.kind, file: item.location.file };
992
+ } else if (scope === "decisions") {
993
+ return { title: item.title, scope: item.scope };
994
+ } else if (scope === "patterns") {
995
+ return { pattern_type: item.pattern_type, file_path: item.file_path };
996
+ } else if (scope === "failures") {
997
+ return { title: item.title, problem: item.problem };
998
+ }
999
+ return item;
1000
+ });
1001
+ } else {
1002
+ // Full details
1003
+ (response.results as Record<string, unknown>)[scope] = items;
1004
+ }
1005
+
1006
+ (response.counts as Record<string, unknown>)[scope] = items.length;
1007
+ (response.counts as Record<string, unknown>).total = ((response.counts as Record<string, unknown>).total as number) + items.length;
1008
+ }
1009
+
1010
+ return response;
1011
+ }
1012
+
1013
+ // ============================================================================
1014
+ // UNIFIED SEARCH - Execute Function
1015
+ // ============================================================================
1016
+
824
1017
  /**
1018
+ * Execute search tool (unified search across multiple scopes)
1019
+ */
1020
+ export async function executeSearch(
1021
+ params: unknown,
1022
+ requestId: string | number,
1023
+ userId: string,
1024
+ ): Promise<unknown> {
1025
+ // Validate params structure
1026
+ if (typeof params !== "object" || params === null) {
1027
+ throw new Error("Parameters must be an object");
1028
+ }
1029
+
1030
+ const p = params as Record<string, unknown>;
1031
+
1032
+ // Check required parameter: query
1033
+ if (p.query === undefined) {
1034
+ throw new Error("Missing required parameter: query");
1035
+ }
1036
+ if (typeof p.query !== "string") {
1037
+ throw new Error("Parameter 'query' must be a string");
1038
+ }
1039
+
1040
+ // Validate optional parameters
1041
+ let scopes: string[] = ["code"]; // Default scope
1042
+ if (p.scope !== undefined) {
1043
+ if (!Array.isArray(p.scope)) {
1044
+ throw new Error("Parameter 'scope' must be an array");
1045
+ }
1046
+ const validScopes = ["code", "symbols", "decisions", "patterns", "failures"];
1047
+ for (const s of p.scope) {
1048
+ if (typeof s !== "string" || !validScopes.includes(s)) {
1049
+ throw new Error(`Invalid scope: ${s}. Must be one of: ${validScopes.join(", ")}`);
1050
+ }
1051
+ }
1052
+ scopes = p.scope as string[];
1053
+ }
1054
+
1055
+ if (p.limit !== undefined && typeof p.limit !== "number") {
1056
+ throw new Error("Parameter 'limit' must be a number");
1057
+ }
1058
+
1059
+ if (p.output !== undefined) {
1060
+ if (typeof p.output !== "string" || !["full", "paths", "compact"].includes(p.output)) {
1061
+ throw new Error("Parameter 'output' must be one of: full, paths, compact");
1062
+ }
1063
+ }
1064
+
1065
+ const limit = Math.min(Math.max((p.limit as number) || 20, 1), 100);
1066
+ const output = (p.output as string) || "full";
1067
+ const filters = normalizeFilters(p.filters);
1068
+
1069
+ // Route to scope handlers in parallel
1070
+ const results: Record<string, unknown[]> = {};
1071
+ const searchPromises: Promise<void>[] = [];
1072
+
1073
+ if (scopes.includes("code")) {
1074
+ searchPromises.push(
1075
+ (async () => {
1076
+ // Reuse existing searchFiles logic
1077
+ const codeResults = searchFiles(p.query as string, {
1078
+ repositoryId: filters.repositoryId,
1079
+ limit,
1080
+ });
1081
+ results.code = codeResults;
1082
+ })()
1083
+ );
1084
+ }
1085
+
1086
+ if (scopes.includes("symbols")) {
1087
+ searchPromises.push(
1088
+ (async () => {
1089
+ const symbolResults = await searchSymbols(p.query as string, filters, limit);
1090
+ results.symbols = symbolResults;
1091
+ })()
1092
+ );
1093
+ }
1094
+
1095
+ if (scopes.includes("decisions")) {
1096
+ searchPromises.push(
1097
+ (async () => {
1098
+ // Reuse existing executeSearchDecisions logic
1099
+ const decisionParams = {
1100
+ query: p.query,
1101
+ scope: filters.decision_scope,
1102
+ repository: filters.repositoryId,
1103
+ limit,
1104
+ };
1105
+ const decisionResults = await executeSearchDecisions(decisionParams, requestId, userId);
1106
+ results.decisions = (decisionResults as { results: unknown[] }).results;
1107
+ })()
1108
+ );
1109
+ }
1110
+
1111
+ if (scopes.includes("patterns")) {
1112
+ searchPromises.push(
1113
+ (async () => {
1114
+ // Reuse existing executeSearchPatterns logic
1115
+ const patternParams = {
1116
+ query: p.query,
1117
+ pattern_type: filters.pattern_type,
1118
+ repository: filters.repositoryId,
1119
+ limit,
1120
+ };
1121
+ const patternResults = await executeSearchPatterns(patternParams, requestId, userId);
1122
+ results.patterns = (patternResults as { results: unknown[] }).results;
1123
+ })()
1124
+ );
1125
+ }
1126
+
1127
+ if (scopes.includes("failures")) {
1128
+ searchPromises.push(
1129
+ (async () => {
1130
+ // Reuse existing executeSearchFailures logic
1131
+ const failureParams = {
1132
+ query: p.query,
1133
+ repository: filters.repositoryId,
1134
+ limit,
1135
+ };
1136
+ const failureResults = await executeSearchFailures(failureParams, requestId, userId);
1137
+ results.failures = (failureResults as { results: unknown[] }).results;
1138
+ })()
1139
+ );
1140
+ }
1141
+
1142
+ await Promise.all(searchPromises);
1143
+
1144
+ // Format output
1145
+ const response = formatSearchResults(p.query as string, scopes, results, output);
1146
+
1147
+ logger.info("Unified search completed", {
1148
+ query: p.query,
1149
+ scopes,
1150
+ total_results: (response.counts as Record<string, unknown>).total,
1151
+ user_id: userId,
1152
+ });
1153
+
1154
+ return response;
1155
+ }
1156
+
825
1157
  /**
826
1158
  * Execute search_code tool
827
1159
  *
@@ -2638,8 +2970,8 @@ export async function handleToolCall(
2638
2970
  userId: string,
2639
2971
  ): Promise<unknown> {
2640
2972
  switch (toolName) {
2641
- case "search_code":
2642
- return await executeSearchCode(params, requestId, userId);
2973
+ case "search":
2974
+ return await executeSearch(params, requestId, userId);
2643
2975
  case "index_repository":
2644
2976
  return await executeIndexRepository(params, requestId, userId);
2645
2977
  case "list_recent_files":
@@ -2657,16 +2989,10 @@ export async function handleToolCall(
2657
2989
  case "generate_task_context":
2658
2990
  return await executeGenerateTaskContext(params, requestId, userId);
2659
2991
  // Memory Layer tools
2660
- case "search_decisions":
2661
- return await executeSearchDecisions(params, requestId, userId);
2662
2992
  case "record_decision":
2663
2993
  return await executeRecordDecision(params, requestId, userId);
2664
- case "search_failures":
2665
- return await executeSearchFailures(params, requestId, userId);
2666
2994
  case "record_failure":
2667
2995
  return await executeRecordFailure(params, requestId, userId);
2668
- case "search_patterns":
2669
- return await executeSearchPatterns(params, requestId, userId);
2670
2996
  case "record_insight":
2671
2997
  return await executeRecordInsight(params, requestId, userId);
2672
2998
  // Expertise Layer tools