kotadb 2.0.1-next.20260203164934 → 2.0.1-next.20260203174555

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/src/mcp/tools.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  runIndexingWorkflow,
14
14
  searchFiles,
15
15
  } from "@api/queries";
16
+ import { getDomainKeyFiles } from "@api/expertise-queries.js";
16
17
  import { getGlobalDatabase } from "@db/sqlite/index.js";
17
18
  import type { KotaDatabase } from "@db/sqlite/sqlite-client.js";
18
19
  import { buildSnippet } from "@indexer/extractors";
@@ -23,6 +24,8 @@ import { analyzeChangeImpact } from "./impact-analysis";
23
24
  import { invalidParams } from "./jsonrpc";
24
25
  import { validateImplementationSpec } from "./spec-validation";
25
26
  import { resolveRepositoryIdentifierWithError } from "./repository-resolver";
27
+ import { readFileSync, existsSync } from "node:fs";
28
+ import { parse as parseYaml } from "yaml";
26
29
 
27
30
  const logger = createLogger({ module: "mcp-tools" });
28
31
 
@@ -596,6 +599,109 @@ export const RECORD_INSIGHT_TOOL: ToolDefinition = {
596
599
  };
597
600
 
598
601
 
602
+ // ============================================================================
603
+ // Dynamic Expertise Tool Definitions
604
+ // ============================================================================
605
+
606
+ /**
607
+ * Tool: get_domain_key_files
608
+ */
609
+ export const GET_DOMAIN_KEY_FILES_TOOL: ToolDefinition = {
610
+ name: "get_domain_key_files",
611
+ description:
612
+ "Get the most-depended-on files for a domain. Key files are core infrastructure that many other files depend on.",
613
+ inputSchema: {
614
+ type: "object",
615
+ properties: {
616
+ domain: {
617
+ type: "string",
618
+ description: "Domain name (e.g., 'database', 'api', 'indexer', 'testing', 'claude-config', 'agent-authoring', 'automation', 'github', 'documentation')",
619
+ },
620
+ limit: {
621
+ type: "number",
622
+ description: "Optional: Maximum number of files to return (default: 10)",
623
+ },
624
+ repository: {
625
+ type: "string",
626
+ description: "Optional: Filter to a specific repository ID",
627
+ },
628
+ },
629
+ required: ["domain"],
630
+ },
631
+ };
632
+
633
+ /**
634
+ * Tool: validate_expertise
635
+ */
636
+ export const VALIDATE_EXPERTISE_TOOL: ToolDefinition = {
637
+ name: "validate_expertise",
638
+ description:
639
+ "Validate that key_files defined in expertise.yaml exist in the indexed codebase. Checks for stale or missing file references.",
640
+ inputSchema: {
641
+ type: "object",
642
+ properties: {
643
+ domain: {
644
+ type: "string",
645
+ description: "Domain name to validate (e.g., 'database', 'api', 'indexer')",
646
+ },
647
+ },
648
+ required: ["domain"],
649
+ },
650
+ };
651
+
652
+ /**
653
+ * Tool: sync_expertise
654
+ */
655
+ export const SYNC_EXPERTISE_TOOL: ToolDefinition = {
656
+ name: "sync_expertise",
657
+ description:
658
+ "Sync patterns from expertise.yaml files to the patterns table. Extracts pattern definitions and stores them for future reference.",
659
+ inputSchema: {
660
+ type: "object",
661
+ properties: {
662
+ domain: {
663
+ type: "string",
664
+ description: "Optional: Specific domain to sync. If not provided, syncs all domains.",
665
+ },
666
+ force: {
667
+ type: "boolean",
668
+ description: "Optional: Force sync even if patterns already exist (default: false)",
669
+ },
670
+ },
671
+ },
672
+ };
673
+
674
+ /**
675
+ * Tool: get_recent_patterns
676
+ */
677
+ export const GET_RECENT_PATTERNS_TOOL: ToolDefinition = {
678
+ name: "get_recent_patterns",
679
+ description:
680
+ "Get recently observed patterns from the patterns table. Useful for understanding codebase conventions.",
681
+ inputSchema: {
682
+ type: "object",
683
+ properties: {
684
+ domain: {
685
+ type: "string",
686
+ description: "Optional: Filter patterns by domain",
687
+ },
688
+ days: {
689
+ type: "number",
690
+ description: "Optional: Only return patterns from the last N days (default: 30)",
691
+ },
692
+ limit: {
693
+ type: "number",
694
+ description: "Optional: Maximum number of patterns to return (default: 20)",
695
+ },
696
+ repository: {
697
+ type: "string",
698
+ description: "Optional: Filter to a specific repository ID",
699
+ },
700
+ },
701
+ },
702
+ };
703
+
704
+
599
705
  /**
600
706
  * Get all available tool definitions
601
707
  */
@@ -617,6 +723,11 @@ export function getToolDefinitions(): ToolDefinition[] {
617
723
  RECORD_FAILURE_TOOL,
618
724
  SEARCH_PATTERNS_TOOL,
619
725
  RECORD_INSIGHT_TOOL,
726
+ // Dynamic Expertise tools
727
+ GET_DOMAIN_KEY_FILES_TOOL,
728
+ VALIDATE_EXPERTISE_TOOL,
729
+ SYNC_EXPERTISE_TOOL,
730
+ GET_RECENT_PATTERNS_TOOL,
620
731
  ];
621
732
  }
622
733
 
@@ -1876,6 +1987,436 @@ export async function executeRecordInsight(
1876
1987
  };
1877
1988
  }
1878
1989
 
1990
+
1991
+ // ============================================================================
1992
+ // Expertise Layer Tool Executors
1993
+ // ============================================================================
1994
+
1995
+ /**
1996
+ * Execute get_domain_key_files tool
1997
+ * Returns files with highest dependent counts for a domain
1998
+ */
1999
+ export async function executeGetDomainKeyFiles(
2000
+ params: unknown,
2001
+ _requestId: string | number,
2002
+ _userId: string,
2003
+ ): Promise<unknown> {
2004
+ if (typeof params !== "object" || params === null) {
2005
+ throw new Error("Parameters must be an object");
2006
+ }
2007
+
2008
+ const p = params as Record<string, unknown>;
2009
+
2010
+ if (p.domain === undefined || typeof p.domain !== "string") {
2011
+ throw new Error("Missing or invalid required parameter: domain");
2012
+ }
2013
+ if (p.limit !== undefined && typeof p.limit !== "number") {
2014
+ throw new Error("Parameter 'limit' must be a number");
2015
+ }
2016
+ if (p.repository !== undefined && typeof p.repository !== "string") {
2017
+ throw new Error("Parameter 'repository' must be a string");
2018
+ }
2019
+
2020
+ const domain = p.domain as string;
2021
+ const limit = Math.min(Math.max((p.limit as number) || 10, 1), 50);
2022
+
2023
+ // Resolve repository if provided
2024
+ let repositoryId: string | undefined;
2025
+ if (p.repository) {
2026
+ const repoResult = resolveRepositoryIdentifierWithError(p.repository as string);
2027
+ if (!("error" in repoResult)) {
2028
+ repositoryId = repoResult.id;
2029
+ }
2030
+ }
2031
+
2032
+ // Use getDomainKeyFiles from expertise-queries
2033
+ const keyFiles = getDomainKeyFiles(domain, limit, repositoryId);
2034
+
2035
+ // Transform to expected output format with purpose field
2036
+ const results = keyFiles.map((file) => {
2037
+ const pathParts = file.path.split("/");
2038
+ const fileName = pathParts.pop() || "";
2039
+ const directory = pathParts.pop() || "";
2040
+ const purpose = directory ? directory + "/" + fileName : fileName;
2041
+
2042
+ return {
2043
+ path: file.path,
2044
+ dependent_count: file.dependentCount,
2045
+ purpose,
2046
+ };
2047
+ });
2048
+
2049
+ logger.debug("get_domain_key_files completed", {
2050
+ domain,
2051
+ files_found: results.length,
2052
+ });
2053
+
2054
+ return {
2055
+ domain,
2056
+ key_files: results,
2057
+ };
2058
+ }
2059
+
2060
+ /**
2061
+ * Execute validate_expertise tool
2062
+ * Validates expertise.yaml patterns against indexed code
2063
+ */
2064
+ export async function executeValidateExpertise(
2065
+ params: unknown,
2066
+ _requestId: string | number,
2067
+ _userId: string,
2068
+ ): Promise<unknown> {
2069
+ if (typeof params !== "object" || params === null) {
2070
+ throw new Error("Parameters must be an object");
2071
+ }
2072
+
2073
+ const p = params as Record<string, unknown>;
2074
+
2075
+ if (p.domain === undefined || typeof p.domain !== "string") {
2076
+ throw new Error("Missing or invalid required parameter: domain");
2077
+ }
2078
+ if (p.expertise_path !== undefined && typeof p.expertise_path !== "string") {
2079
+ throw new Error("Parameter 'expertise_path' must be a string");
2080
+ }
2081
+ if (p.repository !== undefined && typeof p.repository !== "string") {
2082
+ throw new Error("Parameter 'repository' must be a string");
2083
+ }
2084
+
2085
+ const domain = p.domain as string;
2086
+ const defaultPath = ".claude/agents/experts/" + domain + "/expertise.yaml";
2087
+ const expertisePath = (p.expertise_path as string) || defaultPath;
2088
+
2089
+ // Check if expertise file exists
2090
+ if (!existsSync(expertisePath)) {
2091
+ return {
2092
+ domain,
2093
+ valid: false,
2094
+ error: "Expertise file not found: " + expertisePath,
2095
+ valid_patterns: [],
2096
+ stale_patterns: [],
2097
+ missing_key_files: [],
2098
+ summary: { total: 0, valid: 0, stale: 0 },
2099
+ };
2100
+ }
2101
+
2102
+ // Read and parse expertise.yaml
2103
+ let expertise: Record<string, unknown>;
2104
+ try {
2105
+ const content = readFileSync(expertisePath, "utf-8");
2106
+ expertise = parseYaml(content) as Record<string, unknown>;
2107
+ } catch (error) {
2108
+ return {
2109
+ domain,
2110
+ valid: false,
2111
+ error: "Failed to parse expertise.yaml: " + (error instanceof Error ? error.message : String(error)),
2112
+ valid_patterns: [],
2113
+ stale_patterns: [],
2114
+ missing_key_files: [],
2115
+ summary: { total: 0, valid: 0, stale: 0 },
2116
+ };
2117
+ }
2118
+
2119
+ const db = getGlobalDatabase();
2120
+
2121
+ // Resolve repository if provided
2122
+ let repositoryId: string | null = null;
2123
+ if (p.repository) {
2124
+ const repoResult = resolveRepositoryIdentifierWithError(p.repository as string);
2125
+ if (!("error" in repoResult)) {
2126
+ repositoryId = repoResult.id;
2127
+ }
2128
+ }
2129
+
2130
+ const validPatterns: Array<{ name: string; file_path?: string }> = [];
2131
+ const stalePatterns: Array<{ name: string; reason: string }> = [];
2132
+ const missingKeyFiles: string[] = [];
2133
+
2134
+ // Extract patterns from expertise.yaml
2135
+ const patterns = (expertise.patterns as Record<string, unknown>) || {};
2136
+ for (const [patternName, patternData] of Object.entries(patterns)) {
2137
+ const pattern = patternData as Record<string, unknown>;
2138
+ const filePath = pattern.file_path as string | undefined;
2139
+
2140
+ if (filePath) {
2141
+ // Check if file exists in indexed files
2142
+ let sql = "SELECT id FROM indexed_files WHERE path LIKE ?";
2143
+ const queryParams: string[] = ["%" + filePath];
2144
+
2145
+ if (repositoryId) {
2146
+ sql += " AND repository_id = ?";
2147
+ queryParams.push(repositoryId);
2148
+ }
2149
+ sql += " LIMIT 1";
2150
+
2151
+ const result = db.queryOne<{ id: string }>(sql, queryParams);
2152
+
2153
+ if (result) {
2154
+ validPatterns.push({ name: patternName, file_path: filePath });
2155
+ } else {
2156
+ stalePatterns.push({ name: patternName, reason: "File not found: " + filePath });
2157
+ }
2158
+ } else {
2159
+ // Pattern without file path - consider valid
2160
+ validPatterns.push({ name: patternName });
2161
+ }
2162
+ }
2163
+
2164
+ // Check key_files from core_implementation
2165
+ const coreImpl = (expertise.core_implementation as Record<string, unknown>) || {};
2166
+ const keyFiles = (coreImpl.key_files as Array<{ path?: string }>) || [];
2167
+
2168
+ for (const keyFile of keyFiles) {
2169
+ const filePath = keyFile.path;
2170
+ if (filePath) {
2171
+ let sql = "SELECT id FROM indexed_files WHERE path LIKE ?";
2172
+ const queryParams: string[] = ["%" + filePath];
2173
+
2174
+ if (repositoryId) {
2175
+ sql += " AND repository_id = ?";
2176
+ queryParams.push(repositoryId);
2177
+ }
2178
+ sql += " LIMIT 1";
2179
+
2180
+ const result = db.queryOne<{ id: string }>(sql, queryParams);
2181
+
2182
+ if (!result) {
2183
+ missingKeyFiles.push(filePath);
2184
+ }
2185
+ }
2186
+ }
2187
+
2188
+ const total = validPatterns.length + stalePatterns.length;
2189
+
2190
+ logger.debug("validate_expertise completed", {
2191
+ domain,
2192
+ valid_count: validPatterns.length,
2193
+ stale_count: stalePatterns.length,
2194
+ missing_key_files: missingKeyFiles.length,
2195
+ });
2196
+
2197
+ return {
2198
+ domain,
2199
+ valid: stalePatterns.length === 0 && missingKeyFiles.length === 0,
2200
+ valid_patterns: validPatterns,
2201
+ stale_patterns: stalePatterns,
2202
+ missing_key_files: missingKeyFiles,
2203
+ summary: {
2204
+ total,
2205
+ valid: validPatterns.length,
2206
+ stale: stalePatterns.length,
2207
+ },
2208
+ };
2209
+ }
2210
+
2211
+ /**
2212
+ * Execute sync_expertise tool
2213
+ * Extracts patterns from expertise.yaml and stores in patterns table
2214
+ */
2215
+ export async function executeSyncExpertise(
2216
+ params: unknown,
2217
+ _requestId: string | number,
2218
+ _userId: string,
2219
+ ): Promise<unknown> {
2220
+ if (typeof params !== "object" || params === null) {
2221
+ throw new Error("Parameters must be an object");
2222
+ }
2223
+
2224
+ const p = params as Record<string, unknown>;
2225
+
2226
+ if (p.domain === undefined || typeof p.domain !== "string") {
2227
+ throw new Error("Missing or invalid required parameter: domain");
2228
+ }
2229
+ if (p.expertise_path !== undefined && typeof p.expertise_path !== "string") {
2230
+ throw new Error("Parameter 'expertise_path' must be a string");
2231
+ }
2232
+ if (p.dry_run !== undefined && typeof p.dry_run !== "boolean") {
2233
+ throw new Error("Parameter 'dry_run' must be a boolean");
2234
+ }
2235
+ if (p.repository !== undefined && typeof p.repository !== "string") {
2236
+ throw new Error("Parameter 'repository' must be a string");
2237
+ }
2238
+
2239
+ const domain = p.domain as string;
2240
+ const defaultPath = ".claude/agents/experts/" + domain + "/expertise.yaml";
2241
+ const expertisePath = (p.expertise_path as string) || defaultPath;
2242
+ const dryRun = (p.dry_run as boolean) || false;
2243
+
2244
+ // Check if expertise file exists
2245
+ if (!existsSync(expertisePath)) {
2246
+ return {
2247
+ success: false,
2248
+ error: "Expertise file not found: " + expertisePath,
2249
+ patterns_synced: 0,
2250
+ patterns_skipped: 0,
2251
+ };
2252
+ }
2253
+
2254
+ // Read and parse expertise.yaml
2255
+ let expertise: Record<string, unknown>;
2256
+ try {
2257
+ const content = readFileSync(expertisePath, "utf-8");
2258
+ expertise = parseYaml(content) as Record<string, unknown>;
2259
+ } catch (error) {
2260
+ return {
2261
+ success: false,
2262
+ error: "Failed to parse expertise.yaml: " + (error instanceof Error ? error.message : String(error)),
2263
+ patterns_synced: 0,
2264
+ patterns_skipped: 0,
2265
+ };
2266
+ }
2267
+
2268
+ // Resolve repository if provided
2269
+ let repositoryId: string | null = null;
2270
+ if (p.repository) {
2271
+ const repoResult = resolveRepositoryIdentifierWithError(p.repository as string);
2272
+ if (!("error" in repoResult)) {
2273
+ repositoryId = repoResult.id;
2274
+ }
2275
+ }
2276
+
2277
+ const db = getGlobalDatabase();
2278
+ const { randomUUID } = await import("node:crypto");
2279
+
2280
+ let patternsSynced = 0;
2281
+ let patternsSkipped = 0;
2282
+ const syncedPatterns: Array<{ name: string; type: string }> = [];
2283
+
2284
+ // Extract patterns from expertise.yaml
2285
+ const patterns = (expertise.patterns as Record<string, unknown>) || {};
2286
+
2287
+ for (const [patternName, patternData] of Object.entries(patterns)) {
2288
+ const pattern = patternData as Record<string, unknown>;
2289
+ const patternType = domain + ":" + patternName;
2290
+ const filePath = (pattern.file_path as string) || null;
2291
+ const description = (pattern.description as string) || (pattern.structure as string) || patternName;
2292
+ const example = (pattern.example as string) || (pattern.notes as string) || null;
2293
+
2294
+ // Check if pattern already exists
2295
+ const existing = db.queryOne<{ id: string }>(
2296
+ "SELECT id FROM patterns WHERE pattern_type = ?",
2297
+ [patternType],
2298
+ );
2299
+
2300
+ if (existing) {
2301
+ patternsSkipped++;
2302
+ continue;
2303
+ }
2304
+
2305
+ if (!dryRun) {
2306
+ const id = randomUUID();
2307
+ db.run(
2308
+ "INSERT INTO patterns (id, repository_id, pattern_type, file_path, description, example, created_at) VALUES (?, ?, ?, ?, ?, ?, datetime('now'))",
2309
+ [id, repositoryId, patternType, filePath, description, example],
2310
+ );
2311
+ }
2312
+
2313
+ patternsSynced++;
2314
+ syncedPatterns.push({ name: patternName, type: patternType });
2315
+ }
2316
+
2317
+ logger.info("sync_expertise completed", {
2318
+ domain,
2319
+ patterns_synced: patternsSynced,
2320
+ patterns_skipped: patternsSkipped,
2321
+ dry_run: dryRun,
2322
+ });
2323
+
2324
+ return {
2325
+ success: true,
2326
+ dry_run: dryRun,
2327
+ patterns_synced: patternsSynced,
2328
+ patterns_skipped: patternsSkipped,
2329
+ synced_patterns: syncedPatterns,
2330
+ };
2331
+ }
2332
+
2333
+ /**
2334
+ * Execute get_recent_patterns tool
2335
+ * Returns recently observed patterns from the patterns table
2336
+ */
2337
+ export async function executeGetRecentPatterns(
2338
+ params: unknown,
2339
+ _requestId: string | number,
2340
+ _userId: string,
2341
+ ): Promise<unknown> {
2342
+ if (params !== undefined && (typeof params !== "object" || params === null)) {
2343
+ throw new Error("Parameters must be an object");
2344
+ }
2345
+
2346
+ const p = (params as Record<string, unknown>) || {};
2347
+
2348
+ if (p.domain !== undefined && typeof p.domain !== "string") {
2349
+ throw new Error("Parameter 'domain' must be a string");
2350
+ }
2351
+ if (p.days !== undefined && typeof p.days !== "number") {
2352
+ throw new Error("Parameter 'days' must be a number");
2353
+ }
2354
+ if (p.limit !== undefined && typeof p.limit !== "number") {
2355
+ throw new Error("Parameter 'limit' must be a number");
2356
+ }
2357
+ if (p.repository !== undefined && typeof p.repository !== "string") {
2358
+ throw new Error("Parameter 'repository' must be a string");
2359
+ }
2360
+
2361
+ const db = getGlobalDatabase();
2362
+ const domain = p.domain as string | undefined;
2363
+ const days = Math.min(Math.max((p.days as number) || 30, 1), 365);
2364
+ const limit = Math.min(Math.max((p.limit as number) || 20, 1), 100);
2365
+
2366
+ let sql = "SELECT id, repository_id, pattern_type, file_path, description, example, created_at FROM patterns WHERE created_at > datetime('now', '-' || ? || ' days')";
2367
+ const queryParams: (string | number)[] = [days];
2368
+
2369
+ // Filter by domain prefix if provided
2370
+ if (domain) {
2371
+ sql += " AND pattern_type LIKE ?";
2372
+ queryParams.push(domain + ":%");
2373
+ }
2374
+
2375
+ // Filter by repository if provided
2376
+ if (p.repository) {
2377
+ const repoResult = resolveRepositoryIdentifierWithError(p.repository as string);
2378
+ if (!("error" in repoResult)) {
2379
+ sql += " AND repository_id = ?";
2380
+ queryParams.push(repoResult.id);
2381
+ }
2382
+ }
2383
+
2384
+ sql += " ORDER BY created_at DESC LIMIT ?";
2385
+ queryParams.push(limit);
2386
+
2387
+ const rows = db.query<{
2388
+ id: string;
2389
+ repository_id: string | null;
2390
+ pattern_type: string;
2391
+ file_path: string | null;
2392
+ description: string;
2393
+ example: string | null;
2394
+ created_at: string;
2395
+ }>(sql, queryParams);
2396
+
2397
+ logger.debug("get_recent_patterns completed", {
2398
+ domain,
2399
+ days,
2400
+ patterns_found: rows.length,
2401
+ });
2402
+
2403
+ return {
2404
+ patterns: rows.map((row) => ({
2405
+ id: row.id,
2406
+ pattern_type: row.pattern_type,
2407
+ file_path: row.file_path,
2408
+ description: row.description,
2409
+ example: row.example,
2410
+ created_at: row.created_at,
2411
+ })),
2412
+ count: rows.length,
2413
+ filter: {
2414
+ domain: domain || null,
2415
+ days,
2416
+ },
2417
+ };
2418
+ }
2419
+
1879
2420
  /**
1880
2421
  * Main tool call dispatcher
1881
2422
  */
@@ -1917,6 +2458,15 @@ export async function handleToolCall(
1917
2458
  return await executeSearchPatterns(params, requestId, userId);
1918
2459
  case "record_insight":
1919
2460
  return await executeRecordInsight(params, requestId, userId);
2461
+ // Expertise Layer tools
2462
+ case "get_domain_key_files":
2463
+ return await executeGetDomainKeyFiles(params, requestId, userId);
2464
+ case "validate_expertise":
2465
+ return await executeValidateExpertise(params, requestId, userId);
2466
+ case "sync_expertise":
2467
+ return await executeSyncExpertise(params, requestId, userId);
2468
+ case "get_recent_patterns":
2469
+ return await executeGetRecentPatterns(params, requestId, userId);
1920
2470
  default:
1921
2471
  throw invalidParams(requestId, "Unknown tool: " + toolName);
1922
2472
  }