kotadb 2.1.0 → 2.2.0-next.20260204160632

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",
3
+ "version": "2.2.0-next.20260204160632",
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",
@@ -54,8 +54,8 @@
54
54
  "@asteasolutions/zod-to-openapi": "^8.4.0",
55
55
  "@modelcontextprotocol/sdk": "^1.25.0",
56
56
  "@sentry/node": "^10.25.0",
57
- "@typescript-eslint/parser": "^8.0.0",
58
- "@typescript-eslint/types": "^8.54.0",
57
+ "@typescript-eslint/parser": "8.54.0",
58
+ "@typescript-eslint/types": "8.54.0",
59
59
  "bcryptjs": "^2.4.3",
60
60
  "chokidar": "^5.0.0",
61
61
  "cors": "^2.8.6",
@@ -75,5 +75,8 @@
75
75
  "lint-staged": "^16.2.4",
76
76
  "supertest": "^7.1.4",
77
77
  "typescript": "^5.9.3"
78
+ },
79
+ "overrides": {
80
+ "@typescript-eslint/types": "8.54.0"
78
81
  }
79
82
  }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * CLI argument parsing utilities
3
+ *
4
+ * Extracted for testability. Used by main CLI entry point.
5
+ *
6
+ * @module cli/args
7
+ */
8
+
9
+ /**
10
+ * Valid toolset tiers for MCP tool selection
11
+ * - default: 8 tools (core + sync)
12
+ * - core: 6 tools
13
+ * - memory: 14 tools (core + sync + memory)
14
+ * - full: 20 tools (all)
15
+ */
16
+ export type ToolsetTier = "default" | "core" | "memory" | "full";
17
+
18
+ const VALID_TOOLSET_TIERS: ToolsetTier[] = ["default", "core", "memory", "full"];
19
+
20
+ export interface CliOptions {
21
+ port: number;
22
+ help: boolean;
23
+ version: boolean;
24
+ stdio: boolean;
25
+ toolset: ToolsetTier;
26
+ }
27
+
28
+ /**
29
+ * Type guard for valid toolset tier
30
+ */
31
+ export function isValidToolsetTier(value: string): value is ToolsetTier {
32
+ return VALID_TOOLSET_TIERS.includes(value as ToolsetTier);
33
+ }
34
+
35
+ /**
36
+ * Parse CLI arguments into options object
37
+ */
38
+ export function parseArgs(args: string[]): CliOptions {
39
+ const options: CliOptions = {
40
+ port: Number(process.env.PORT ?? 3000),
41
+ help: false,
42
+ version: false,
43
+ stdio: false,
44
+ toolset: "default",
45
+ };
46
+
47
+ for (let i = 0; i < args.length; i++) {
48
+ const arg = args[i];
49
+ if (arg === undefined) continue;
50
+
51
+ if (arg === "--help" || arg === "-h") {
52
+ options.help = true;
53
+ } else if (arg === "--version" || arg === "-v") {
54
+ options.version = true;
55
+ } else if (arg === "--stdio") {
56
+ options.stdio = true;
57
+ } else if (arg === "--port") {
58
+ const portStr = args[++i];
59
+ if (!portStr || Number.isNaN(Number(portStr))) {
60
+ process.stderr.write("Error: --port requires a valid number\n");
61
+ process.exit(1);
62
+ }
63
+ options.port = Number(portStr);
64
+ } else if (arg.startsWith("--port=")) {
65
+ const portStr = arg.split("=")[1];
66
+ if (portStr === undefined || Number.isNaN(Number(portStr))) {
67
+ process.stderr.write("Error: --port requires a valid number\n");
68
+ process.exit(1);
69
+ }
70
+ options.port = Number(portStr);
71
+ } else if (arg === "--toolset") {
72
+ const tierStr = args[++i];
73
+ if (!tierStr) {
74
+ process.stderr.write("Error: --toolset requires a tier value\n");
75
+ process.stderr.write("Valid tiers: default, core, memory, full\n");
76
+ process.exit(1);
77
+ }
78
+ if (!isValidToolsetTier(tierStr)) {
79
+ process.stderr.write(`Error: Invalid toolset tier '${tierStr}'\n`);
80
+ process.stderr.write("Valid tiers: default, core, memory, full\n");
81
+ process.exit(1);
82
+ }
83
+ options.toolset = tierStr;
84
+ } else if (arg.startsWith("--toolset=")) {
85
+ const tierStr = arg.split("=")[1];
86
+ if (tierStr === undefined || tierStr === "") {
87
+ process.stderr.write("Error: --toolset requires a tier value\n");
88
+ process.stderr.write("Valid tiers: default, core, memory, full\n");
89
+ process.exit(1);
90
+ }
91
+ if (!isValidToolsetTier(tierStr)) {
92
+ process.stderr.write(`Error: Invalid toolset tier '${tierStr}'\n`);
93
+ process.stderr.write("Valid tiers: default, core, memory, full\n");
94
+ process.exit(1);
95
+ }
96
+ options.toolset = tierStr;
97
+ } else if (arg.startsWith("-") && arg !== "-") {
98
+ process.stderr.write(`Unknown option: ${arg}\n`);
99
+ process.stderr.write("Use --help for usage information\n");
100
+ process.exit(1);
101
+ }
102
+ }
103
+
104
+ return options;
105
+ }
package/src/cli.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  * kotadb Start the MCP server (default port 3000)
10
10
  * kotadb --stdio Start in stdio mode (for Claude Code integration)
11
11
  * kotadb --port 4000 Start on custom port
12
+ * kotadb --toolset full Select tool tier (default, core, memory, full)
12
13
  * kotadb --version Show version
13
14
  * kotadb --help Show help
14
15
  * kotadb deps Query dependency information for a file
@@ -25,11 +26,23 @@ import { fileURLToPath } from "node:url";
25
26
 
26
27
  const __dirname = dirname(fileURLToPath(import.meta.url));
27
28
 
29
+ /**
30
+ * Valid toolset tiers for MCP tool selection
31
+ * - default: 8 tools (core + sync)
32
+ * - core: 6 tools
33
+ * - memory: 14 tools (core + sync + memory)
34
+ * - full: 20 tools (all)
35
+ */
36
+ export type ToolsetTier = "default" | "core" | "memory" | "full";
37
+
38
+ const VALID_TOOLSET_TIERS: ToolsetTier[] = ["default", "core", "memory", "full"];
39
+
28
40
  interface CliOptions {
29
41
  port: number;
30
42
  help: boolean;
31
43
  version: boolean;
32
44
  stdio: boolean;
45
+ toolset: ToolsetTier;
33
46
  }
34
47
 
35
48
  function getVersion(): string {
@@ -70,6 +83,12 @@ COMMANDS:
70
83
  OPTIONS:
71
84
  --stdio Use stdio transport (for Claude Code integration)
72
85
  --port <number> Port to listen on (default: 3000, env: PORT)
86
+ --toolset <tier> Select tool tier (default: default)
87
+ Tiers:
88
+ default 8 tools (core + sync)
89
+ core 6 tools (search, index, deps, impact)
90
+ memory 14 tools (core + sync + memory layer)
91
+ full 20 tools (all available tools)
73
92
  --version, -v Show version number
74
93
  --help, -h Show this help message
75
94
 
@@ -81,6 +100,8 @@ ENVIRONMENT VARIABLES:
81
100
 
82
101
  EXAMPLES:
83
102
  kotadb --stdio Start in stdio mode (for Claude Code)
103
+ kotadb --stdio --toolset full Start with all tools enabled
104
+ kotadb --stdio --toolset core Start with minimal core tools
84
105
  kotadb Start HTTP server on port 3000
85
106
  kotadb --port 4000 Start HTTP server on port 4000
86
107
  kotadb deps --file src/db/client.ts Query deps for a file (text)
@@ -99,6 +120,17 @@ MCP CONFIGURATION (stdio mode - RECOMMENDED):
99
120
  }
100
121
  }
101
122
 
123
+ With toolset selection:
124
+
125
+ {
126
+ "mcpServers": {
127
+ "kotadb": {
128
+ "command": "bunx",
129
+ "args": ["kotadb@next", "--stdio", "--toolset", "full"]
130
+ }
131
+ }
132
+ }
133
+
102
134
  MCP CONFIGURATION (HTTP mode - legacy):
103
135
  Add to your .mcp.json or Claude Code settings:
104
136
 
@@ -126,12 +158,17 @@ function printVersion(): void {
126
158
  process.stdout.write(`kotadb v${version}\n`);
127
159
  }
128
160
 
161
+ function isValidToolsetTier(value: string): value is ToolsetTier {
162
+ return VALID_TOOLSET_TIERS.includes(value as ToolsetTier);
163
+ }
164
+
129
165
  function parseArgs(args: string[]): CliOptions {
130
166
  const options: CliOptions = {
131
167
  port: Number(process.env.PORT ?? 3000),
132
168
  help: false,
133
169
  version: false,
134
170
  stdio: false,
171
+ toolset: "default",
135
172
  };
136
173
 
137
174
  for (let i = 0; i < args.length; i++) {
@@ -158,6 +195,32 @@ function parseArgs(args: string[]): CliOptions {
158
195
  process.exit(1);
159
196
  }
160
197
  options.port = Number(portStr);
198
+ } else if (arg === "--toolset") {
199
+ const tierStr = args[++i];
200
+ if (!tierStr) {
201
+ process.stderr.write("Error: --toolset requires a tier value\n");
202
+ process.stderr.write("Valid tiers: default, core, memory, full\n");
203
+ process.exit(1);
204
+ }
205
+ if (!isValidToolsetTier(tierStr)) {
206
+ process.stderr.write(`Error: Invalid toolset tier '${tierStr}'\n`);
207
+ process.stderr.write("Valid tiers: default, core, memory, full\n");
208
+ process.exit(1);
209
+ }
210
+ options.toolset = tierStr;
211
+ } else if (arg.startsWith("--toolset=")) {
212
+ const tierStr = arg.split("=")[1];
213
+ if (tierStr === undefined || tierStr === "") {
214
+ process.stderr.write("Error: --toolset requires a tier value\n");
215
+ process.stderr.write("Valid tiers: default, core, memory, full\n");
216
+ process.exit(1);
217
+ }
218
+ if (!isValidToolsetTier(tierStr)) {
219
+ process.stderr.write(`Error: Invalid toolset tier '${tierStr}'\n`);
220
+ process.stderr.write("Valid tiers: default, core, memory, full\n");
221
+ process.exit(1);
222
+ }
223
+ options.toolset = tierStr;
161
224
  } else if (arg.startsWith("-") && arg !== "-") {
162
225
  process.stderr.write(`Unknown option: ${arg}\n`);
163
226
  process.stderr.write("Use --help for usage information\n");
@@ -168,7 +231,7 @@ function parseArgs(args: string[]): CliOptions {
168
231
  return options;
169
232
  }
170
233
 
171
- async function runStdioMode(): Promise<void> {
234
+ async function runStdioMode(toolset: ToolsetTier): Promise<void> {
172
235
  // Redirect logger to stderr in stdio mode
173
236
  // This is CRITICAL - stdout is reserved for JSON-RPC protocol
174
237
  const logger = createLogger({
@@ -178,11 +241,13 @@ async function runStdioMode(): Promise<void> {
178
241
 
179
242
  logger.info("KotaDB MCP server starting in stdio mode", {
180
243
  version: getVersion(),
244
+ toolset,
181
245
  });
182
246
 
183
- // Create MCP server with local-only context
247
+ // Create MCP server with local-only context and toolset
184
248
  const context: McpServerContext = {
185
249
  userId: "local", // Local-only mode uses fixed user ID
250
+ toolset,
186
251
  };
187
252
  const server = createMcpServer(context);
188
253
 
@@ -192,7 +257,7 @@ async function runStdioMode(): Promise<void> {
192
257
  // Connect server to transport
193
258
  await server.connect(transport);
194
259
 
195
- logger.info("KotaDB MCP server connected via stdio");
260
+ logger.info("KotaDB MCP server connected via stdio", { toolset });
196
261
 
197
262
  // Server lifecycle is managed by the transport
198
263
  // Process will stay alive until stdin closes (when Claude Code terminates it)
@@ -234,7 +299,7 @@ async function main(): Promise<void> {
234
299
 
235
300
  // Handle stdio mode
236
301
  if (options.stdio) {
237
- await runStdioMode();
302
+ await runStdioMode(options.toolset);
238
303
  return; // runStdioMode() keeps process alive
239
304
  }
240
305
 
@@ -247,6 +312,7 @@ async function main(): Promise<void> {
247
312
  mode: envConfig.mode,
248
313
  port: options.port,
249
314
  localDbPath: envConfig.localDbPath,
315
+ toolset: options.toolset,
250
316
  });
251
317
 
252
318
  const app = createExpressApp();
@@ -256,6 +322,7 @@ async function main(): Promise<void> {
256
322
  port: options.port,
257
323
  mcp_endpoint: `http://localhost:${options.port}/mcp`,
258
324
  health_endpoint: `http://localhost:${options.port}/health`,
325
+ toolset: options.toolset,
259
326
  });
260
327
 
261
328
  // Print user-friendly startup message
@@ -265,6 +332,7 @@ async function main(): Promise<void> {
265
332
  process.stdout.write(` MCP Endpoint: http://localhost:${options.port}/mcp\n`);
266
333
  process.stdout.write(` Health Check: http://localhost:${options.port}/health\n`);
267
334
  process.stdout.write(` Database: ${envConfig.localDbPath}\n`);
335
+ process.stdout.write(` Toolset: ${options.toolset}\n`);
268
336
  process.stdout.write(`\n`);
269
337
  process.stdout.write(`Press Ctrl+C to stop\n`);
270
338
  process.stdout.write(`\n`);
@@ -42,6 +42,24 @@ export interface ParseError {
42
42
  column?: number;
43
43
  }
44
44
 
45
+ /**
46
+ * Type guard to validate that an AST node is a valid Program.
47
+ * Provides runtime type validation to prevent version compatibility issues.
48
+ *
49
+ * @param ast - Unknown AST node to validate
50
+ * @returns true if ast is a valid Program node
51
+ */
52
+ function isValidProgram(ast: unknown): ast is TSESTree.Program {
53
+ return (
54
+ ast !== null &&
55
+ typeof ast === "object" &&
56
+ "type" in ast &&
57
+ ast.type === "Program" &&
58
+ "body" in ast &&
59
+ Array.isArray(ast.body)
60
+ );
61
+ }
62
+
45
63
  /**
46
64
  * Result of parsing a file, including partial AST recovery information.
47
65
  */
@@ -103,7 +121,7 @@ function createParseError(error: unknown): ParseError {
103
121
 
104
122
  /**
105
123
  * Parse a file with error-tolerant options enabled.
106
- * Uses allowInvalidAST to attempt partial recovery.
124
+ * Uses allowInvalidAST to attempt partial recovery and validates result.
107
125
  */
108
126
  function parseWithRecoveryOptions(filePath: string, content: string): TSESTree.Program | null {
109
127
  try {
@@ -119,6 +137,18 @@ function parseWithRecoveryOptions(filePath: string, content: string): TSESTree.P
119
137
  allowInvalidAST: true,
120
138
  errorOnUnknownASTType: false,
121
139
  });
140
+
141
+ // Validate the returned AST is actually a Program node
142
+ if (!isValidProgram(ast)) {
143
+ const astType = typeof ast === "object" && ast !== null && "type" in ast ? (ast as any).type : typeof ast;
144
+ logger.warn(`Parser returned invalid Program type for ${filePath}`, {
145
+ file_path: filePath,
146
+ ast_type: astType,
147
+ recovery: "failed_validation",
148
+ });
149
+ return null;
150
+ }
151
+
122
152
  return ast;
123
153
  } catch {
124
154
  // Even with recovery options, parsing can still fail
@@ -159,6 +189,18 @@ export function parseFileWithRecovery(filePath: string, content: string): ParseR
159
189
  tokens: true,
160
190
  filePath,
161
191
  });
192
+
193
+ // Validate the returned AST is actually a Program node
194
+ if (!isValidProgram(ast)) {
195
+ const astType = typeof ast === "object" && ast !== null && "type" in ast ? (ast as any).type : typeof ast;
196
+ logger.warn(`Parser returned invalid Program type for ${filePath}`, {
197
+ file_path: filePath,
198
+ ast_type: astType,
199
+ recovery: "failed_validation",
200
+ });
201
+ throw new Error(`Invalid AST type returned: expected Program, got ${astType}`);
202
+ }
203
+
162
204
  return {
163
205
  ast,
164
206
  errors: [],
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,27 +37,35 @@ 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,
57
51
  executeValidateExpertise,
58
52
  executeSyncExpertise,
59
53
  executeGetRecentPatterns,
54
+ // Tool filtering
55
+ filterToolsByTier,
60
56
  } from "./tools";
61
57
 
62
58
  const logger = createLogger({ module: "mcp-server" });
63
59
 
60
+ /**
61
+ * Valid toolset tiers for MCP tool selection
62
+ * - default: 8 tools (core + sync)
63
+ * - core: 6 tools
64
+ * - memory: 14 tools (core + sync + memory)
65
+ * - full: 20 tools (all)
66
+ */
67
+ export type ToolsetTier = "default" | "core" | "memory" | "full";
68
+
64
69
  /**
65
70
  * MCP Server context passed to tool handlers via closure
66
71
  *
@@ -68,6 +73,7 @@ const logger = createLogger({ module: "mcp-server" });
68
73
  */
69
74
  export interface McpServerContext {
70
75
  userId: string;
76
+ toolset?: ToolsetTier;
71
77
  }
72
78
 
73
79
  /**
@@ -107,35 +113,20 @@ export function createMcpServer(context: McpServerContext): Server {
107
113
  },
108
114
  );
109
115
 
110
- // Register tools/list handler - local-only tools
116
+ // Register tools/list handler - filter by toolset tier
111
117
  server.setRequestHandler(ListToolsRequestSchema, async () => {
118
+ const tier = context.toolset || "default";
119
+ const filteredTools = filterToolsByTier(tier);
120
+
121
+ logger.debug("Listing MCP tools", {
122
+ tier,
123
+ tool_count: filteredTools.length
124
+ });
125
+
112
126
  return {
113
- tools: [
114
- SEARCH_CODE_TOOL,
115
- INDEX_REPOSITORY_TOOL,
116
- LIST_RECENT_FILES_TOOL,
117
- SEARCH_DEPENDENCIES_TOOL,
118
- ANALYZE_CHANGE_IMPACT_TOOL,
119
- VALIDATE_IMPLEMENTATION_SPEC_TOOL,
120
- SYNC_EXPORT_TOOL,
121
- SYNC_IMPORT_TOOL,
122
- GENERATE_TASK_CONTEXT_TOOL,
123
- // Memory Layer tools
124
- SEARCH_DECISIONS_TOOL,
125
- RECORD_DECISION_TOOL,
126
- SEARCH_FAILURES_TOOL,
127
- RECORD_FAILURE_TOOL,
128
- SEARCH_PATTERNS_TOOL,
129
- RECORD_INSIGHT_TOOL,
130
- // Dynamic Expertise tools
131
- GET_DOMAIN_KEY_FILES_TOOL,
132
- VALIDATE_EXPERTISE_TOOL,
133
- SYNC_EXPERTISE_TOOL,
134
- GET_RECENT_PATTERNS_TOOL,
135
- ],
127
+ tools: filteredTools,
136
128
  };
137
129
  });
138
-
139
130
  // Register tools/call handler
140
131
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
141
132
  const { name, arguments: toolArgs } = request.params;
@@ -146,8 +137,8 @@ export function createMcpServer(context: McpServerContext): Server {
146
137
 
147
138
  try {
148
139
  switch (name) {
149
- case "search_code":
150
- result = await executeSearchCode(
140
+ case "search":
141
+ result = await executeSearch(
151
142
  toolArgs,
152
143
  "", // requestId not used
153
144
  context.userId,
@@ -202,13 +193,6 @@ export function createMcpServer(context: McpServerContext): Server {
202
193
  );
203
194
  break;
204
195
  // Memory Layer tools
205
- case "search_decisions":
206
- result = await executeSearchDecisions(
207
- toolArgs,
208
- "", // requestId not used
209
- context.userId,
210
- );
211
- break;
212
196
  case "record_decision":
213
197
  result = await executeRecordDecision(
214
198
  toolArgs,
@@ -216,13 +200,6 @@ export function createMcpServer(context: McpServerContext): Server {
216
200
  context.userId,
217
201
  );
218
202
  break;
219
- case "search_failures":
220
- result = await executeSearchFailures(
221
- toolArgs,
222
- "", // requestId not used
223
- context.userId,
224
- );
225
- break;
226
203
  case "record_failure":
227
204
  result = await executeRecordFailure(
228
205
  toolArgs,
@@ -230,13 +207,6 @@ export function createMcpServer(context: McpServerContext): Server {
230
207
  context.userId,
231
208
  );
232
209
  break;
233
- case "search_patterns":
234
- result = await executeSearchPatterns(
235
- toolArgs,
236
- "", // requestId not used
237
- context.userId,
238
- );
239
- break;
240
210
  case "record_insight":
241
211
  result = await executeRecordInsight(
242
212
  toolArgs,
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
- * Tool: search_code
88
+ * Alias for filterToolsByTier - get tool definitions filtered by toolset
89
+ *
90
+ * @param toolset - The toolset tier to filter by
51
91
  */
52
- export const SEARCH_CODE_TOOL: ToolDefinition = {
53
- name: "search_code",
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 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.",
56
116
  inputSchema: {
57
117
  type: "object",
58
118
  properties: {
59
- term: {
119
+ query: {
60
120
  type: "string",
61
- description: "The search term to find in code files",
121
+ description: "Search query term or phrase",
62
122
  },
63
- repository: {
64
- type: "string",
65
- 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
+ },
66
192
  },
67
193
  limit: {
68
194
  type: "number",
69
- 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')",
70
201
  },
71
202
  },
72
- required: ["term"],
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
- SEARCH_CODE_TOOL,
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, repository);
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 "search_code":
2562
- return await executeSearchCode(params, requestId, userId);
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