kotadb 2.1.0 → 2.2.0-next.20260204175832
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 +6 -3
- package/src/cli/args.ts +105 -0
- package/src/cli.ts +72 -4
- package/src/db/migrations/005_workflow_contexts.sql +41 -0
- package/src/indexer/ast-parser.ts +43 -1
- package/src/mcp/server.ts +26 -56
- package/src/mcp/tools.ts +526 -120
package/src/mcp/tools.ts
CHANGED
|
@@ -34,8 +34,14 @@ const logger = createLogger({ module: "mcp-tools" });
|
|
|
34
34
|
/**
|
|
35
35
|
* MCP Tool Definition
|
|
36
36
|
*/
|
|
37
|
+
/**
|
|
38
|
+
* Tool tier for categorizing tools by feature set
|
|
39
|
+
*/
|
|
40
|
+
export type ToolTier = "core" | "sync" | "memory" | "expertise";
|
|
41
|
+
|
|
37
42
|
export interface ToolDefinition {
|
|
38
43
|
name: string;
|
|
44
|
+
tier: ToolTier;
|
|
39
45
|
description: string;
|
|
40
46
|
inputSchema: {
|
|
41
47
|
type: "object";
|
|
@@ -45,38 +51,165 @@ export interface ToolDefinition {
|
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
/**
|
|
54
|
+
* Toolset tier for CLI selection (maps to tool tiers)
|
|
55
|
+
*/
|
|
56
|
+
export type ToolsetTier = "default" | "core" | "memory" | "full";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Filter tools by the requested toolset tier
|
|
60
|
+
*
|
|
61
|
+
* Tier mapping:
|
|
62
|
+
* - core: 6 tools (core tier only)
|
|
63
|
+
* - default: 8 tools (core + sync tiers)
|
|
64
|
+
* - memory: 14 tools (core + sync + memory tiers)
|
|
65
|
+
* - full: all tools
|
|
66
|
+
*
|
|
67
|
+
* @param tier - The toolset tier to filter by
|
|
68
|
+
* @param tools - Optional array of tools (defaults to all tool definitions)
|
|
69
|
+
*/
|
|
70
|
+
export function filterToolsByTier(tier: ToolsetTier, tools?: ToolDefinition[]): ToolDefinition[] {
|
|
71
|
+
const allTools = tools ?? getToolDefinitions();
|
|
72
|
+
switch (tier) {
|
|
73
|
+
case "core":
|
|
74
|
+
return allTools.filter((t) => t.tier === "core");
|
|
75
|
+
case "default":
|
|
76
|
+
return allTools.filter((t) => t.tier === "core" || t.tier === "sync");
|
|
77
|
+
case "memory":
|
|
78
|
+
return allTools.filter((t) => t.tier === "core" || t.tier === "sync" || t.tier === "memory");
|
|
79
|
+
case "full":
|
|
80
|
+
return allTools;
|
|
81
|
+
default:
|
|
82
|
+
// Default to "default" tier if unknown
|
|
83
|
+
return allTools.filter((t) => t.tier === "core" || t.tier === "sync");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
48
86
|
|
|
49
87
|
/**
|
|
50
|
-
*
|
|
88
|
+
* Alias for filterToolsByTier - get tool definitions filtered by toolset
|
|
89
|
+
*
|
|
90
|
+
* @param toolset - The toolset tier to filter by
|
|
51
91
|
*/
|
|
52
|
-
export
|
|
53
|
-
|
|
92
|
+
export function getToolsByTier(toolset: ToolsetTier): ToolDefinition[] {
|
|
93
|
+
return filterToolsByTier(toolset);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validate if a string is a valid toolset tier
|
|
98
|
+
*/
|
|
99
|
+
export function isValidToolset(value: string): value is ToolsetTier {
|
|
100
|
+
return value === "default" || value === "core" || value === "memory" || value === "full";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// UNIFIED SEARCH TOOL - Replaces search_code, search_symbols, search_decisions, search_patterns, search_failures
|
|
105
|
+
// Issue: #143
|
|
106
|
+
// ============================================================================
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Tool: search (unified)
|
|
110
|
+
*/
|
|
111
|
+
export const SEARCH_TOOL: ToolDefinition = {
|
|
112
|
+
tier: "core",
|
|
113
|
+
name: "search",
|
|
54
114
|
description:
|
|
55
|
-
"Search indexed code
|
|
115
|
+
"Search indexed code, symbols, decisions, patterns, and failures. Supports multiple search scopes simultaneously with scope-specific filters and output formats.",
|
|
56
116
|
inputSchema: {
|
|
57
117
|
type: "object",
|
|
58
118
|
properties: {
|
|
59
|
-
|
|
119
|
+
query: {
|
|
60
120
|
type: "string",
|
|
61
|
-
description: "
|
|
121
|
+
description: "Search query term or phrase",
|
|
62
122
|
},
|
|
63
|
-
|
|
64
|
-
type: "
|
|
65
|
-
|
|
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
|
+
},
|
|
66
192
|
},
|
|
67
193
|
limit: {
|
|
68
194
|
type: "number",
|
|
69
|
-
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')",
|
|
70
201
|
},
|
|
71
202
|
},
|
|
72
|
-
required: ["
|
|
203
|
+
required: ["query"],
|
|
73
204
|
},
|
|
74
205
|
};
|
|
75
206
|
|
|
207
|
+
|
|
76
208
|
/**
|
|
77
209
|
* Tool: index_repository
|
|
78
210
|
*/
|
|
79
211
|
export const INDEX_REPOSITORY_TOOL: ToolDefinition = {
|
|
212
|
+
tier: "core",
|
|
80
213
|
name: "index_repository",
|
|
81
214
|
description:
|
|
82
215
|
"Index a git repository by cloning/updating it and extracting code files. Performs synchronous indexing and returns immediately with status 'completed' and full indexing stats.",
|
|
@@ -104,6 +237,7 @@ export const INDEX_REPOSITORY_TOOL: ToolDefinition = {
|
|
|
104
237
|
* Tool: list_recent_files
|
|
105
238
|
*/
|
|
106
239
|
export const LIST_RECENT_FILES_TOOL: ToolDefinition = {
|
|
240
|
+
tier: "core",
|
|
107
241
|
name: "list_recent_files",
|
|
108
242
|
description:
|
|
109
243
|
"List recently indexed files, ordered by indexing timestamp. Useful for seeing what code is available.",
|
|
@@ -126,6 +260,7 @@ export const LIST_RECENT_FILES_TOOL: ToolDefinition = {
|
|
|
126
260
|
* Tool: search_dependencies
|
|
127
261
|
*/
|
|
128
262
|
export const SEARCH_DEPENDENCIES_TOOL: ToolDefinition = {
|
|
263
|
+
tier: "core",
|
|
129
264
|
name: "search_dependencies",
|
|
130
265
|
description:
|
|
131
266
|
"Search the dependency graph to find files that depend on (dependents) or are depended on by (dependencies) a target file. Useful for impact analysis before refactoring, test scope discovery, and circular dependency detection.",
|
|
@@ -174,6 +309,7 @@ export const SEARCH_DEPENDENCIES_TOOL: ToolDefinition = {
|
|
|
174
309
|
* Tool: analyze_change_impact
|
|
175
310
|
*/
|
|
176
311
|
export const ANALYZE_CHANGE_IMPACT_TOOL: ToolDefinition = {
|
|
312
|
+
tier: "core",
|
|
177
313
|
name: "analyze_change_impact",
|
|
178
314
|
description:
|
|
179
315
|
"Analyze the impact of proposed code changes by examining dependency graphs, test scope, and potential conflicts. Returns comprehensive analysis including affected files, test recommendations, architectural warnings, and risk assessment. Useful for planning implementations and avoiding breaking changes.",
|
|
@@ -221,6 +357,7 @@ export const ANALYZE_CHANGE_IMPACT_TOOL: ToolDefinition = {
|
|
|
221
357
|
* Tool: validate_implementation_spec
|
|
222
358
|
*/
|
|
223
359
|
export const VALIDATE_IMPLEMENTATION_SPEC_TOOL: ToolDefinition = {
|
|
360
|
+
tier: "expertise",
|
|
224
361
|
name: "validate_implementation_spec",
|
|
225
362
|
description:
|
|
226
363
|
"Validate an implementation specification against KotaDB conventions and repository state. Checks for file conflicts, naming conventions, path alias usage, test coverage, and dependency compatibility. Returns validation errors, warnings, and approval conditions checklist.",
|
|
@@ -303,6 +440,7 @@ export const VALIDATE_IMPLEMENTATION_SPEC_TOOL: ToolDefinition = {
|
|
|
303
440
|
* Tool: kota_sync_export
|
|
304
441
|
*/
|
|
305
442
|
export const SYNC_EXPORT_TOOL: ToolDefinition = {
|
|
443
|
+
tier: "sync",
|
|
306
444
|
name: "kota_sync_export",
|
|
307
445
|
description:
|
|
308
446
|
"Export local SQLite database to JSONL files for git sync. Uses hash-based change detection to skip unchanged tables. Exports to .kotadb/export/ by default.",
|
|
@@ -325,6 +463,7 @@ export const SYNC_EXPORT_TOOL: ToolDefinition = {
|
|
|
325
463
|
* Tool: kota_sync_import
|
|
326
464
|
*/
|
|
327
465
|
export const SYNC_IMPORT_TOOL: ToolDefinition = {
|
|
466
|
+
tier: "sync",
|
|
328
467
|
name: "kota_sync_import",
|
|
329
468
|
description:
|
|
330
469
|
"Import JSONL files into local SQLite database. Applies deletion manifest first, then imports all tables transactionally. Typically run after git pull to sync remote changes.",
|
|
@@ -347,6 +486,7 @@ export const SYNC_IMPORT_TOOL: ToolDefinition = {
|
|
|
347
486
|
* Target: <100ms response time
|
|
348
487
|
*/
|
|
349
488
|
export const GENERATE_TASK_CONTEXT_TOOL: ToolDefinition = {
|
|
489
|
+
tier: "core",
|
|
350
490
|
name: "generate_task_context",
|
|
351
491
|
description:
|
|
352
492
|
"Generate structured context for a set of files including dependency counts, impacted files, test files, and recent changes. Designed for hook-based context injection with <100ms performance target.",
|
|
@@ -383,42 +523,11 @@ export const GENERATE_TASK_CONTEXT_TOOL: ToolDefinition = {
|
|
|
383
523
|
// Memory Layer Tool Definitions
|
|
384
524
|
// ============================================================================
|
|
385
525
|
|
|
386
|
-
/**
|
|
387
|
-
* Tool: search_decisions
|
|
388
|
-
*/
|
|
389
|
-
export const SEARCH_DECISIONS_TOOL: ToolDefinition = {
|
|
390
|
-
name: "search_decisions",
|
|
391
|
-
description:
|
|
392
|
-
"Search past architectural decisions using FTS5. Returns decisions with relevance scores.",
|
|
393
|
-
inputSchema: {
|
|
394
|
-
type: "object",
|
|
395
|
-
properties: {
|
|
396
|
-
query: {
|
|
397
|
-
type: "string",
|
|
398
|
-
description: "Search query for decisions",
|
|
399
|
-
},
|
|
400
|
-
scope: {
|
|
401
|
-
type: "string",
|
|
402
|
-
enum: ["architecture", "pattern", "convention", "workaround"],
|
|
403
|
-
description: "Optional: Filter by decision scope",
|
|
404
|
-
},
|
|
405
|
-
repository: {
|
|
406
|
-
type: "string",
|
|
407
|
-
description: "Optional: Filter to a specific repository ID or full_name",
|
|
408
|
-
},
|
|
409
|
-
limit: {
|
|
410
|
-
type: "number",
|
|
411
|
-
description: "Optional: Max results (default: 20)",
|
|
412
|
-
},
|
|
413
|
-
},
|
|
414
|
-
required: ["query"],
|
|
415
|
-
},
|
|
416
|
-
};
|
|
417
|
-
|
|
418
526
|
/**
|
|
419
527
|
* Tool: record_decision
|
|
420
528
|
*/
|
|
421
529
|
export const RECORD_DECISION_TOOL: ToolDefinition = {
|
|
530
|
+
tier: "memory",
|
|
422
531
|
name: "record_decision",
|
|
423
532
|
description:
|
|
424
533
|
"Record a new architectural decision for future reference. Decisions are searchable via search_decisions.",
|
|
@@ -465,37 +574,11 @@ export const RECORD_DECISION_TOOL: ToolDefinition = {
|
|
|
465
574
|
},
|
|
466
575
|
};
|
|
467
576
|
|
|
468
|
-
/**
|
|
469
|
-
* Tool: search_failures
|
|
470
|
-
*/
|
|
471
|
-
export const SEARCH_FAILURES_TOOL: ToolDefinition = {
|
|
472
|
-
name: "search_failures",
|
|
473
|
-
description:
|
|
474
|
-
"Search failed approaches to avoid repeating mistakes. Returns failures with relevance scores.",
|
|
475
|
-
inputSchema: {
|
|
476
|
-
type: "object",
|
|
477
|
-
properties: {
|
|
478
|
-
query: {
|
|
479
|
-
type: "string",
|
|
480
|
-
description: "Search query for failures",
|
|
481
|
-
},
|
|
482
|
-
repository: {
|
|
483
|
-
type: "string",
|
|
484
|
-
description: "Optional: Filter to a specific repository ID or full_name",
|
|
485
|
-
},
|
|
486
|
-
limit: {
|
|
487
|
-
type: "number",
|
|
488
|
-
description: "Optional: Max results (default: 20)",
|
|
489
|
-
},
|
|
490
|
-
},
|
|
491
|
-
required: ["query"],
|
|
492
|
-
},
|
|
493
|
-
};
|
|
494
|
-
|
|
495
577
|
/**
|
|
496
578
|
* Tool: record_failure
|
|
497
579
|
*/
|
|
498
580
|
export const RECORD_FAILURE_TOOL: ToolDefinition = {
|
|
581
|
+
tier: "memory",
|
|
499
582
|
name: "record_failure",
|
|
500
583
|
description:
|
|
501
584
|
"Record a failed approach for future reference. Helps agents avoid repeating mistakes.",
|
|
@@ -532,44 +615,11 @@ export const RECORD_FAILURE_TOOL: ToolDefinition = {
|
|
|
532
615
|
},
|
|
533
616
|
};
|
|
534
617
|
|
|
535
|
-
/**
|
|
536
|
-
* Tool: search_patterns
|
|
537
|
-
*/
|
|
538
|
-
export const SEARCH_PATTERNS_TOOL: ToolDefinition = {
|
|
539
|
-
name: "search_patterns",
|
|
540
|
-
description:
|
|
541
|
-
"Find codebase patterns by type or file. Returns discovered patterns for consistency.",
|
|
542
|
-
inputSchema: {
|
|
543
|
-
type: "object",
|
|
544
|
-
properties: {
|
|
545
|
-
query: {
|
|
546
|
-
type: "string",
|
|
547
|
-
description: "Optional: Search query for pattern name/description",
|
|
548
|
-
},
|
|
549
|
-
pattern_type: {
|
|
550
|
-
type: "string",
|
|
551
|
-
description: "Optional: Filter by pattern type (e.g., error-handling, api-call)",
|
|
552
|
-
},
|
|
553
|
-
file: {
|
|
554
|
-
type: "string",
|
|
555
|
-
description: "Optional: Filter by file path",
|
|
556
|
-
},
|
|
557
|
-
repository: {
|
|
558
|
-
type: "string",
|
|
559
|
-
description: "Optional: Filter to a specific repository ID or full_name",
|
|
560
|
-
},
|
|
561
|
-
limit: {
|
|
562
|
-
type: "number",
|
|
563
|
-
description: "Optional: Max results (default: 20)",
|
|
564
|
-
},
|
|
565
|
-
},
|
|
566
|
-
},
|
|
567
|
-
};
|
|
568
|
-
|
|
569
618
|
/**
|
|
570
619
|
* Tool: record_insight
|
|
571
620
|
*/
|
|
572
621
|
export const RECORD_INSIGHT_TOOL: ToolDefinition = {
|
|
622
|
+
tier: "memory",
|
|
573
623
|
name: "record_insight",
|
|
574
624
|
description:
|
|
575
625
|
"Store a session insight for future agents. Insights are discoveries, failures, or workarounds.",
|
|
@@ -611,6 +661,7 @@ export const RECORD_INSIGHT_TOOL: ToolDefinition = {
|
|
|
611
661
|
* Tool: get_domain_key_files
|
|
612
662
|
*/
|
|
613
663
|
export const GET_DOMAIN_KEY_FILES_TOOL: ToolDefinition = {
|
|
664
|
+
tier: "expertise",
|
|
614
665
|
name: "get_domain_key_files",
|
|
615
666
|
description:
|
|
616
667
|
"Get the most-depended-on files for a domain. Key files are core infrastructure that many other files depend on.",
|
|
@@ -638,6 +689,7 @@ export const GET_DOMAIN_KEY_FILES_TOOL: ToolDefinition = {
|
|
|
638
689
|
* Tool: validate_expertise
|
|
639
690
|
*/
|
|
640
691
|
export const VALIDATE_EXPERTISE_TOOL: ToolDefinition = {
|
|
692
|
+
tier: "expertise",
|
|
641
693
|
name: "validate_expertise",
|
|
642
694
|
description:
|
|
643
695
|
"Validate that key_files defined in expertise.yaml exist in the indexed codebase. Checks for stale or missing file references.",
|
|
@@ -657,6 +709,7 @@ export const VALIDATE_EXPERTISE_TOOL: ToolDefinition = {
|
|
|
657
709
|
* Tool: sync_expertise
|
|
658
710
|
*/
|
|
659
711
|
export const SYNC_EXPERTISE_TOOL: ToolDefinition = {
|
|
712
|
+
tier: "expertise",
|
|
660
713
|
name: "sync_expertise",
|
|
661
714
|
description:
|
|
662
715
|
"Sync patterns from expertise.yaml files to the patterns table. Extracts pattern definitions and stores them for future reference.",
|
|
@@ -679,6 +732,7 @@ export const SYNC_EXPERTISE_TOOL: ToolDefinition = {
|
|
|
679
732
|
* Tool: get_recent_patterns
|
|
680
733
|
*/
|
|
681
734
|
export const GET_RECENT_PATTERNS_TOOL: ToolDefinition = {
|
|
735
|
+
tier: "expertise",
|
|
682
736
|
name: "get_recent_patterns",
|
|
683
737
|
description:
|
|
684
738
|
"Get recently observed patterns from the patterns table. Useful for understanding codebase conventions.",
|
|
@@ -711,7 +765,7 @@ export const GET_RECENT_PATTERNS_TOOL: ToolDefinition = {
|
|
|
711
765
|
*/
|
|
712
766
|
export function getToolDefinitions(): ToolDefinition[] {
|
|
713
767
|
return [
|
|
714
|
-
|
|
768
|
+
SEARCH_TOOL,
|
|
715
769
|
INDEX_REPOSITORY_TOOL,
|
|
716
770
|
LIST_RECENT_FILES_TOOL,
|
|
717
771
|
SEARCH_DEPENDENCIES_TOOL,
|
|
@@ -721,11 +775,8 @@ export function getToolDefinitions(): ToolDefinition[] {
|
|
|
721
775
|
SYNC_IMPORT_TOOL,
|
|
722
776
|
GENERATE_TASK_CONTEXT_TOOL,
|
|
723
777
|
// Memory Layer tools
|
|
724
|
-
SEARCH_DECISIONS_TOOL,
|
|
725
778
|
RECORD_DECISION_TOOL,
|
|
726
|
-
SEARCH_FAILURES_TOOL,
|
|
727
779
|
RECORD_FAILURE_TOOL,
|
|
728
|
-
SEARCH_PATTERNS_TOOL,
|
|
729
780
|
RECORD_INSIGHT_TOOL,
|
|
730
781
|
// Dynamic Expertise tools
|
|
731
782
|
GET_DOMAIN_KEY_FILES_TOOL,
|
|
@@ -734,7 +785,6 @@ export function getToolDefinitions(): ToolDefinition[] {
|
|
|
734
785
|
GET_RECENT_PATTERNS_TOOL,
|
|
735
786
|
];
|
|
736
787
|
}
|
|
737
|
-
|
|
738
788
|
/**
|
|
739
789
|
|
|
740
790
|
/**
|
|
@@ -749,7 +799,361 @@ function isListRecentParams(params: unknown): params is { limit?: number; reposi
|
|
|
749
799
|
return true;
|
|
750
800
|
}
|
|
751
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
|
+
|
|
752
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
|
+
|
|
753
1157
|
/**
|
|
754
1158
|
* Execute search_code tool
|
|
755
1159
|
*
|
|
@@ -943,8 +1347,18 @@ export async function executeListRecentFiles(
|
|
|
943
1347
|
? (params.repository as string | undefined)
|
|
944
1348
|
: undefined;
|
|
945
1349
|
|
|
1350
|
+
// Resolve repository ID (supports UUID or full_name)
|
|
1351
|
+
let repositoryId = repository;
|
|
1352
|
+
if (repositoryId) {
|
|
1353
|
+
const repoResult = resolveRepositoryIdentifierWithError(repositoryId);
|
|
1354
|
+
if ("error" in repoResult) {
|
|
1355
|
+
return { results: [], message: repoResult.error };
|
|
1356
|
+
}
|
|
1357
|
+
repositoryId = repoResult.id;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
946
1360
|
// Use SQLite via listRecentFiles with optional repository filter
|
|
947
|
-
const files = listRecentFiles(limit,
|
|
1361
|
+
const files = listRecentFiles(limit, repositoryId);
|
|
948
1362
|
|
|
949
1363
|
return {
|
|
950
1364
|
results: files.map((file) => ({
|
|
@@ -956,8 +1370,6 @@ export async function executeListRecentFiles(
|
|
|
956
1370
|
};
|
|
957
1371
|
}
|
|
958
1372
|
|
|
959
|
-
/**
|
|
960
|
-
|
|
961
1373
|
/**
|
|
962
1374
|
* Execute search_dependencies tool
|
|
963
1375
|
*/
|
|
@@ -2558,8 +2970,8 @@ export async function handleToolCall(
|
|
|
2558
2970
|
userId: string,
|
|
2559
2971
|
): Promise<unknown> {
|
|
2560
2972
|
switch (toolName) {
|
|
2561
|
-
case "
|
|
2562
|
-
return await
|
|
2973
|
+
case "search":
|
|
2974
|
+
return await executeSearch(params, requestId, userId);
|
|
2563
2975
|
case "index_repository":
|
|
2564
2976
|
return await executeIndexRepository(params, requestId, userId);
|
|
2565
2977
|
case "list_recent_files":
|
|
@@ -2577,16 +2989,10 @@ export async function handleToolCall(
|
|
|
2577
2989
|
case "generate_task_context":
|
|
2578
2990
|
return await executeGenerateTaskContext(params, requestId, userId);
|
|
2579
2991
|
// Memory Layer tools
|
|
2580
|
-
case "search_decisions":
|
|
2581
|
-
return await executeSearchDecisions(params, requestId, userId);
|
|
2582
2992
|
case "record_decision":
|
|
2583
2993
|
return await executeRecordDecision(params, requestId, userId);
|
|
2584
|
-
case "search_failures":
|
|
2585
|
-
return await executeSearchFailures(params, requestId, userId);
|
|
2586
2994
|
case "record_failure":
|
|
2587
2995
|
return await executeRecordFailure(params, requestId, userId);
|
|
2588
|
-
case "search_patterns":
|
|
2589
|
-
return await executeSearchPatterns(params, requestId, userId);
|
|
2590
2996
|
case "record_insight":
|
|
2591
2997
|
return await executeRecordInsight(params, requestId, userId);
|
|
2592
2998
|
// Expertise Layer tools
|