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/package.json +1 -1
- package/src/api/expertise-queries.ts +274 -0
- package/src/cli/expertise.ts +495 -0
- package/src/cli.ts +14 -0
- package/src/mcp/server.ts +48 -0
- package/src/mcp/tools.ts +550 -0
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
|
}
|