kotadb 2.1.0-next.20260203231739 → 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 +1 -1
- package/src/mcp/server.ts +4 -31
- package/src/mcp/tools.ts +446 -120
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kotadb",
|
|
3
|
-
"version": "2.1.0-next.
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
147
|
-
result = await
|
|
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
|
|
111
|
+
export const SEARCH_TOOL: ToolDefinition = {
|
|
107
112
|
tier: "core",
|
|
108
|
-
name: "
|
|
113
|
+
name: "search",
|
|
109
114
|
description:
|
|
110
|
-
"Search indexed code
|
|
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
|
-
|
|
119
|
+
query: {
|
|
115
120
|
type: "string",
|
|
116
|
-
description: "
|
|
121
|
+
description: "Search query term or phrase",
|
|
117
122
|
},
|
|
118
|
-
|
|
119
|
-
type: "
|
|
120
|
-
|
|
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: "
|
|
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: ["
|
|
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
|
-
|
|
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 "
|
|
2642
|
-
return await
|
|
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
|