codebase-context 1.2.2 → 1.5.1
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/LICENSE +21 -21
- package/README.md +292 -87
- package/dist/analyzers/angular/index.d.ts +1 -1
- package/dist/analyzers/angular/index.d.ts.map +1 -1
- package/dist/analyzers/angular/index.js +298 -309
- package/dist/analyzers/angular/index.js.map +1 -1
- package/dist/analyzers/generic/index.d.ts +1 -2
- package/dist/analyzers/generic/index.d.ts.map +1 -1
- package/dist/analyzers/generic/index.js +93 -60
- package/dist/analyzers/generic/index.js.map +1 -1
- package/dist/constants/codebase-context.d.ts +8 -0
- package/dist/constants/codebase-context.d.ts.map +1 -0
- package/dist/constants/codebase-context.js +10 -0
- package/dist/constants/codebase-context.js.map +1 -0
- package/dist/constants/git-patterns.d.ts +12 -0
- package/dist/constants/git-patterns.d.ts.map +1 -0
- package/dist/constants/git-patterns.js +11 -0
- package/dist/constants/git-patterns.js.map +1 -0
- package/dist/core/analyzer-registry.d.ts.map +1 -1
- package/dist/core/analyzer-registry.js +8 -8
- package/dist/core/analyzer-registry.js.map +1 -1
- package/dist/core/indexer.d.ts +11 -1
- package/dist/core/indexer.d.ts.map +1 -1
- package/dist/core/indexer.js +359 -157
- package/dist/core/indexer.js.map +1 -1
- package/dist/core/manifest.d.ts +39 -0
- package/dist/core/manifest.d.ts.map +1 -0
- package/dist/core/manifest.js +86 -0
- package/dist/core/manifest.js.map +1 -0
- package/dist/core/search-quality.d.ts +10 -0
- package/dist/core/search-quality.d.ts.map +1 -0
- package/dist/core/search-quality.js +64 -0
- package/dist/core/search-quality.js.map +1 -0
- package/dist/core/search.d.ts +17 -1
- package/dist/core/search.d.ts.map +1 -1
- package/dist/core/search.js +303 -104
- package/dist/core/search.js.map +1 -1
- package/dist/embeddings/openai.d.ts.map +1 -1
- package/dist/embeddings/openai.js +2 -2
- package/dist/embeddings/openai.js.map +1 -1
- package/dist/embeddings/transformers.d.ts +1 -1
- package/dist/embeddings/transformers.d.ts.map +1 -1
- package/dist/embeddings/transformers.js +19 -15
- package/dist/embeddings/transformers.js.map +1 -1
- package/dist/embeddings/types.d.ts +1 -1
- package/dist/embeddings/types.d.ts.map +1 -1
- package/dist/embeddings/types.js +3 -3
- package/dist/embeddings/types.js.map +1 -1
- package/dist/errors/index.d.ts +8 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +11 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +7 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1125 -362
- package/dist/index.js.map +1 -1
- package/dist/lib.d.ts +18 -18
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +23 -23
- package/dist/lib.js.map +1 -1
- package/dist/memory/git-memory.d.ts +9 -0
- package/dist/memory/git-memory.d.ts.map +1 -0
- package/dist/memory/git-memory.js +51 -0
- package/dist/memory/git-memory.js.map +1 -0
- package/dist/memory/store.d.ts +38 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +136 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/patterns/semantics.d.ts +4 -0
- package/dist/patterns/semantics.d.ts.map +1 -0
- package/dist/patterns/semantics.js +24 -0
- package/dist/patterns/semantics.js.map +1 -0
- package/dist/preflight/evidence-lock.d.ts +50 -0
- package/dist/preflight/evidence-lock.d.ts.map +1 -0
- package/dist/preflight/evidence-lock.js +130 -0
- package/dist/preflight/evidence-lock.js.map +1 -0
- package/dist/preflight/query-scope.d.ts +3 -0
- package/dist/preflight/query-scope.d.ts.map +1 -0
- package/dist/preflight/query-scope.js +40 -0
- package/dist/preflight/query-scope.js.map +1 -0
- package/dist/resources/uri.d.ts +5 -0
- package/dist/resources/uri.d.ts.map +1 -0
- package/dist/resources/uri.js +15 -0
- package/dist/resources/uri.js.map +1 -0
- package/dist/storage/lancedb.d.ts +1 -0
- package/dist/storage/lancedb.d.ts.map +1 -1
- package/dist/storage/lancedb.js +51 -34
- package/dist/storage/lancedb.js.map +1 -1
- package/dist/storage/types.d.ts +5 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/storage/types.js +2 -1
- package/dist/storage/types.js.map +1 -1
- package/dist/types/index.d.ts +47 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/utils/chunking.d.ts.map +1 -1
- package/dist/utils/chunking.js +10 -9
- package/dist/utils/chunking.js.map +1 -1
- package/dist/utils/dependency-detection.d.ts +18 -0
- package/dist/utils/dependency-detection.d.ts.map +1 -0
- package/dist/utils/dependency-detection.js +102 -0
- package/dist/utils/dependency-detection.js.map +1 -0
- package/dist/utils/git-dates.d.ts +1 -0
- package/dist/utils/git-dates.d.ts.map +1 -1
- package/dist/utils/git-dates.js +23 -3
- package/dist/utils/git-dates.js.map +1 -1
- package/dist/utils/language-detection.d.ts.map +1 -1
- package/dist/utils/language-detection.js +69 -17
- package/dist/utils/language-detection.js.map +1 -1
- package/dist/utils/usage-tracker.d.ts +2 -2
- package/dist/utils/usage-tracker.d.ts.map +1 -1
- package/dist/utils/usage-tracker.js +67 -38
- package/dist/utils/usage-tracker.js.map +1 -1
- package/dist/utils/workspace-detection.d.ts +32 -0
- package/dist/utils/workspace-detection.d.ts.map +1 -0
- package/dist/utils/workspace-detection.js +107 -0
- package/dist/utils/workspace-detection.js.map +1 -0
- package/package.json +122 -97
- package/dist/core/file-watcher.d.ts +0 -63
- package/dist/core/file-watcher.d.ts.map +0 -1
- package/dist/core/file-watcher.js +0 -210
- package/dist/core/file-watcher.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -36
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -111
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/pattern-detector.d.ts +0 -41
- package/dist/utils/pattern-detector.d.ts.map +0 -1
- package/dist/utils/pattern-detector.js +0 -101
- package/dist/utils/pattern-detector.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
3
|
/**
|
|
3
4
|
* MCP Server for Codebase Context
|
|
4
5
|
* Provides codebase indexing and semantic search capabilities
|
|
5
6
|
*/
|
|
6
|
-
import { promises as fs } from
|
|
7
|
-
import path from
|
|
8
|
-
import { glob } from
|
|
9
|
-
import { Server } from
|
|
10
|
-
import { StdioServerTransport } from
|
|
11
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema
|
|
12
|
-
import { CodebaseIndexer } from
|
|
13
|
-
import { CodebaseSearcher } from
|
|
14
|
-
import { analyzerRegistry } from
|
|
15
|
-
import { AngularAnalyzer } from
|
|
16
|
-
import { GenericAnalyzer } from
|
|
17
|
-
import { InternalFileGraph } from
|
|
7
|
+
import { promises as fs } from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { glob } from 'glob';
|
|
10
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
11
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
13
|
+
import { CodebaseIndexer } from './core/indexer.js';
|
|
14
|
+
import { CodebaseSearcher } from './core/search.js';
|
|
15
|
+
import { analyzerRegistry } from './core/analyzer-registry.js';
|
|
16
|
+
import { AngularAnalyzer } from './analyzers/angular/index.js';
|
|
17
|
+
import { GenericAnalyzer } from './analyzers/generic/index.js';
|
|
18
|
+
import { InternalFileGraph } from './utils/usage-tracker.js';
|
|
19
|
+
import { getFileCommitDates } from './utils/git-dates.js';
|
|
20
|
+
import { IndexCorruptedError } from './errors/index.js';
|
|
21
|
+
import { CODEBASE_CONTEXT_DIRNAME, MEMORY_FILENAME, INTELLIGENCE_FILENAME, KEYWORD_INDEX_FILENAME, VECTOR_DB_DIRNAME } from './constants/codebase-context.js';
|
|
22
|
+
import { appendMemoryFile, readMemoriesFile, filterMemories, applyUnfilteredLimit, withConfidence } from './memory/store.js';
|
|
23
|
+
import { parseGitLogLineToMemory } from './memory/git-memory.js';
|
|
24
|
+
import { buildEvidenceLock } from './preflight/evidence-lock.js';
|
|
25
|
+
import { shouldIncludePatternConflictCategory } from './preflight/query-scope.js';
|
|
26
|
+
import { isComplementaryPatternCategory, isComplementaryPatternConflict, shouldSkipLegacyTestingFrameworkCategory } from './patterns/semantics.js';
|
|
27
|
+
import { CONTEXT_RESOURCE_URI, isContextResourceUri } from './resources/uri.js';
|
|
28
|
+
import { assessSearchQuality } from './core/search-quality.js';
|
|
18
29
|
analyzerRegistry.register(new AngularAnalyzer());
|
|
19
30
|
analyzerRegistry.register(new GenericAnalyzer());
|
|
20
31
|
// Resolve root path with validation
|
|
@@ -32,167 +43,316 @@ function resolveRootPath() {
|
|
|
32
43
|
return rootPath;
|
|
33
44
|
}
|
|
34
45
|
const ROOT_PATH = resolveRootPath();
|
|
46
|
+
// File paths (new structure)
|
|
47
|
+
const PATHS = {
|
|
48
|
+
baseDir: path.join(ROOT_PATH, CODEBASE_CONTEXT_DIRNAME),
|
|
49
|
+
memory: path.join(ROOT_PATH, CODEBASE_CONTEXT_DIRNAME, MEMORY_FILENAME),
|
|
50
|
+
intelligence: path.join(ROOT_PATH, CODEBASE_CONTEXT_DIRNAME, INTELLIGENCE_FILENAME),
|
|
51
|
+
keywordIndex: path.join(ROOT_PATH, CODEBASE_CONTEXT_DIRNAME, KEYWORD_INDEX_FILENAME),
|
|
52
|
+
vectorDb: path.join(ROOT_PATH, CODEBASE_CONTEXT_DIRNAME, VECTOR_DB_DIRNAME)
|
|
53
|
+
};
|
|
54
|
+
// Legacy paths for migration
|
|
55
|
+
const LEGACY_PATHS = {
|
|
56
|
+
// Pre-v1.5
|
|
57
|
+
intelligence: path.join(ROOT_PATH, '.codebase-intelligence.json'),
|
|
58
|
+
keywordIndex: path.join(ROOT_PATH, '.codebase-index.json'),
|
|
59
|
+
vectorDb: path.join(ROOT_PATH, '.codebase-index')
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Check if file/directory exists
|
|
63
|
+
*/
|
|
64
|
+
async function fileExists(filePath) {
|
|
65
|
+
try {
|
|
66
|
+
await fs.access(filePath);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Migrate legacy file structure to .codebase-context/ folder.
|
|
75
|
+
* Idempotent, fail-safe. Rollback compatibility is not required.
|
|
76
|
+
*/
|
|
77
|
+
async function migrateToNewStructure() {
|
|
78
|
+
let migrated = false;
|
|
79
|
+
try {
|
|
80
|
+
await fs.mkdir(PATHS.baseDir, { recursive: true });
|
|
81
|
+
// intelligence.json
|
|
82
|
+
if (!(await fileExists(PATHS.intelligence))) {
|
|
83
|
+
if (await fileExists(LEGACY_PATHS.intelligence)) {
|
|
84
|
+
await fs.copyFile(LEGACY_PATHS.intelligence, PATHS.intelligence);
|
|
85
|
+
migrated = true;
|
|
86
|
+
if (process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
87
|
+
console.error('[DEBUG] Migrated intelligence.json');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// index.json (keyword index)
|
|
92
|
+
if (!(await fileExists(PATHS.keywordIndex))) {
|
|
93
|
+
if (await fileExists(LEGACY_PATHS.keywordIndex)) {
|
|
94
|
+
await fs.copyFile(LEGACY_PATHS.keywordIndex, PATHS.keywordIndex);
|
|
95
|
+
migrated = true;
|
|
96
|
+
if (process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
97
|
+
console.error('[DEBUG] Migrated index.json');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Vector DB directory
|
|
102
|
+
if (!(await fileExists(PATHS.vectorDb))) {
|
|
103
|
+
if (await fileExists(LEGACY_PATHS.vectorDb)) {
|
|
104
|
+
await fs.rename(LEGACY_PATHS.vectorDb, PATHS.vectorDb);
|
|
105
|
+
migrated = true;
|
|
106
|
+
if (process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
107
|
+
console.error('[DEBUG] Migrated vector database');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return migrated;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
115
|
+
console.error('[DEBUG] Migration error:', error);
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Read version from package.json so it never drifts
|
|
121
|
+
const PKG_VERSION = JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url), 'utf-8')).version;
|
|
35
122
|
const indexState = {
|
|
36
|
-
status:
|
|
123
|
+
status: 'idle'
|
|
37
124
|
};
|
|
38
125
|
const server = new Server({
|
|
39
|
-
name:
|
|
40
|
-
version:
|
|
126
|
+
name: 'codebase-context',
|
|
127
|
+
version: PKG_VERSION
|
|
41
128
|
}, {
|
|
42
129
|
capabilities: {
|
|
43
130
|
tools: {},
|
|
44
|
-
resources: {}
|
|
45
|
-
}
|
|
131
|
+
resources: {}
|
|
132
|
+
}
|
|
46
133
|
});
|
|
47
134
|
const TOOLS = [
|
|
48
135
|
{
|
|
49
|
-
name:
|
|
50
|
-
description:
|
|
51
|
-
|
|
52
|
-
"
|
|
136
|
+
name: 'search_codebase',
|
|
137
|
+
description: 'Search the indexed codebase using natural language queries. Returns code summaries with file locations. ' +
|
|
138
|
+
'Supports framework-specific queries and architectural layer filtering. ' +
|
|
139
|
+
'When intent is "edit", "refactor", or "migrate", returns a preflight card with risk level, ' +
|
|
140
|
+
'patterns to use/avoid, impact candidates, related memories, and an evidence lock score — all in one call. ' +
|
|
141
|
+
'Use the returned filePath with other tools to read complete file contents.',
|
|
53
142
|
inputSchema: {
|
|
54
|
-
type:
|
|
143
|
+
type: 'object',
|
|
55
144
|
properties: {
|
|
56
145
|
query: {
|
|
57
|
-
type:
|
|
58
|
-
description:
|
|
146
|
+
type: 'string',
|
|
147
|
+
description: 'Natural language search query'
|
|
148
|
+
},
|
|
149
|
+
intent: {
|
|
150
|
+
type: 'string',
|
|
151
|
+
enum: ['explore', 'edit', 'refactor', 'migrate'],
|
|
152
|
+
description: 'Search intent. Use "explore" (default) for read-only browsing. ' +
|
|
153
|
+
'Use "edit", "refactor", or "migrate" to get a preflight card with risk assessment, ' +
|
|
154
|
+
'patterns to prefer/avoid, affected files, relevant team memories, and ready-to-edit evidence checks.'
|
|
59
155
|
},
|
|
60
156
|
limit: {
|
|
61
|
-
type:
|
|
62
|
-
description:
|
|
63
|
-
default: 5
|
|
157
|
+
type: 'number',
|
|
158
|
+
description: 'Maximum number of results to return (default: 5)',
|
|
159
|
+
default: 5
|
|
64
160
|
},
|
|
65
161
|
filters: {
|
|
66
|
-
type:
|
|
67
|
-
description:
|
|
162
|
+
type: 'object',
|
|
163
|
+
description: 'Optional filters',
|
|
68
164
|
properties: {
|
|
69
165
|
framework: {
|
|
70
|
-
type:
|
|
71
|
-
description:
|
|
166
|
+
type: 'string',
|
|
167
|
+
description: 'Filter by framework (angular, react, vue)'
|
|
72
168
|
},
|
|
73
169
|
language: {
|
|
74
|
-
type:
|
|
75
|
-
description:
|
|
170
|
+
type: 'string',
|
|
171
|
+
description: 'Filter by programming language'
|
|
76
172
|
},
|
|
77
173
|
componentType: {
|
|
78
|
-
type:
|
|
79
|
-
description:
|
|
174
|
+
type: 'string',
|
|
175
|
+
description: 'Filter by component type (component, service, directive, etc.)'
|
|
80
176
|
},
|
|
81
177
|
layer: {
|
|
82
|
-
type:
|
|
83
|
-
description:
|
|
178
|
+
type: 'string',
|
|
179
|
+
description: 'Filter by architectural layer (presentation, business, data, state, core, shared)'
|
|
84
180
|
},
|
|
85
181
|
tags: {
|
|
86
|
-
type:
|
|
87
|
-
items: { type:
|
|
88
|
-
description:
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
182
|
+
type: 'array',
|
|
183
|
+
items: { type: 'string' },
|
|
184
|
+
description: 'Filter by tags'
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
92
188
|
},
|
|
93
|
-
required: [
|
|
94
|
-
}
|
|
189
|
+
required: ['query']
|
|
190
|
+
}
|
|
95
191
|
},
|
|
96
192
|
{
|
|
97
|
-
name:
|
|
98
|
-
description:
|
|
99
|
-
|
|
193
|
+
name: 'get_codebase_metadata',
|
|
194
|
+
description: 'Get codebase metadata including framework information, dependencies, architecture patterns, ' +
|
|
195
|
+
'and project statistics.',
|
|
100
196
|
inputSchema: {
|
|
101
|
-
type:
|
|
102
|
-
properties: {}
|
|
103
|
-
}
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: {}
|
|
199
|
+
}
|
|
104
200
|
},
|
|
105
201
|
{
|
|
106
|
-
name:
|
|
107
|
-
description:
|
|
108
|
-
|
|
202
|
+
name: 'get_indexing_status',
|
|
203
|
+
description: 'Get current indexing status: state, statistics, and progress. ' +
|
|
204
|
+
'Use refresh_index to manually trigger re-indexing when needed.',
|
|
109
205
|
inputSchema: {
|
|
110
|
-
type:
|
|
111
|
-
properties: {}
|
|
112
|
-
}
|
|
206
|
+
type: 'object',
|
|
207
|
+
properties: {}
|
|
208
|
+
}
|
|
113
209
|
},
|
|
114
210
|
{
|
|
115
|
-
name:
|
|
116
|
-
description:
|
|
117
|
-
|
|
211
|
+
name: 'refresh_index',
|
|
212
|
+
description: 'Re-index the codebase. Supports full re-index or incremental mode. ' +
|
|
213
|
+
'Use incrementalOnly=true to only process files changed since last index.',
|
|
118
214
|
inputSchema: {
|
|
119
|
-
type:
|
|
215
|
+
type: 'object',
|
|
120
216
|
properties: {
|
|
121
217
|
reason: {
|
|
122
|
-
type:
|
|
123
|
-
description:
|
|
218
|
+
type: 'string',
|
|
219
|
+
description: 'Reason for refreshing the index (for logging)'
|
|
124
220
|
},
|
|
125
221
|
incrementalOnly: {
|
|
126
|
-
type:
|
|
127
|
-
description:
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
222
|
+
type: 'boolean',
|
|
223
|
+
description: 'If true, only re-index files changed since last full index (faster). Default: false (full re-index)'
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
131
227
|
},
|
|
132
228
|
{
|
|
133
|
-
name:
|
|
134
|
-
description:
|
|
229
|
+
name: 'get_style_guide',
|
|
230
|
+
description: 'Query style guide rules and architectural patterns from project documentation.',
|
|
135
231
|
inputSchema: {
|
|
136
|
-
type:
|
|
232
|
+
type: 'object',
|
|
137
233
|
properties: {
|
|
138
234
|
query: {
|
|
139
|
-
type:
|
|
140
|
-
description: 'Query for specific style guide rules (e.g., "component naming", "service patterns")'
|
|
235
|
+
type: 'string',
|
|
236
|
+
description: 'Query for specific style guide rules (e.g., "component naming", "service patterns")'
|
|
141
237
|
},
|
|
142
238
|
category: {
|
|
143
|
-
type:
|
|
144
|
-
description:
|
|
145
|
-
}
|
|
239
|
+
type: 'string',
|
|
240
|
+
description: 'Filter by category (naming, structure, patterns, testing)'
|
|
241
|
+
}
|
|
146
242
|
},
|
|
147
|
-
required: [
|
|
148
|
-
}
|
|
243
|
+
required: ['query']
|
|
244
|
+
}
|
|
149
245
|
},
|
|
150
246
|
{
|
|
151
|
-
name:
|
|
152
|
-
description:
|
|
153
|
-
|
|
247
|
+
name: 'get_team_patterns',
|
|
248
|
+
description: 'Get actionable team pattern recommendations based on codebase analysis. ' +
|
|
249
|
+
'Returns consensus patterns for DI, state management, testing, library wrappers, etc.',
|
|
154
250
|
inputSchema: {
|
|
155
|
-
type:
|
|
251
|
+
type: 'object',
|
|
156
252
|
properties: {
|
|
157
253
|
category: {
|
|
158
|
-
type:
|
|
159
|
-
description:
|
|
160
|
-
enum: [
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
254
|
+
type: 'string',
|
|
255
|
+
description: 'Pattern category to retrieve',
|
|
256
|
+
enum: ['all', 'di', 'state', 'testing', 'libraries']
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
164
260
|
},
|
|
165
261
|
{
|
|
166
|
-
name:
|
|
167
|
-
description:
|
|
262
|
+
name: 'get_component_usage',
|
|
263
|
+
description: 'Find WHERE a library or component is used in the codebase. ' +
|
|
168
264
|
"This is 'Find Usages' - returns all files that import a given package/module. " +
|
|
169
265
|
"Example: get_component_usage('@mycompany/utils') → shows all 34 files using it.",
|
|
170
266
|
inputSchema: {
|
|
171
|
-
type:
|
|
267
|
+
type: 'object',
|
|
172
268
|
properties: {
|
|
173
269
|
name: {
|
|
174
|
-
type:
|
|
175
|
-
description: "Import source to find usages for (e.g., 'primeng/table', '@mycompany/ui/button', 'lodash')"
|
|
176
|
-
}
|
|
270
|
+
type: 'string',
|
|
271
|
+
description: "Import source to find usages for (e.g., 'primeng/table', '@mycompany/ui/button', 'lodash')"
|
|
272
|
+
}
|
|
177
273
|
},
|
|
178
|
-
required: [
|
|
179
|
-
}
|
|
274
|
+
required: ['name']
|
|
275
|
+
}
|
|
180
276
|
},
|
|
181
277
|
{
|
|
182
|
-
name:
|
|
183
|
-
description:
|
|
184
|
-
|
|
185
|
-
|
|
278
|
+
name: 'detect_circular_dependencies',
|
|
279
|
+
description: 'Analyze the import graph to detect circular dependencies between files. ' +
|
|
280
|
+
'Circular dependencies can cause initialization issues, tight coupling, and maintenance problems. ' +
|
|
281
|
+
'Returns all detected cycles sorted by length (shorter cycles are often more problematic).',
|
|
186
282
|
inputSchema: {
|
|
187
|
-
type:
|
|
283
|
+
type: 'object',
|
|
188
284
|
properties: {
|
|
189
285
|
scope: {
|
|
190
|
-
type:
|
|
191
|
-
description: "Optional path prefix to limit analysis (e.g., 'src/features', 'libs/shared')"
|
|
286
|
+
type: 'string',
|
|
287
|
+
description: "Optional path prefix to limit analysis (e.g., 'src/features', 'libs/shared')"
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: 'remember',
|
|
294
|
+
description: '📝 CALL IMMEDIATELY when user explicitly asks to remember/record something.\n\n' +
|
|
295
|
+
'USER TRIGGERS:\n' +
|
|
296
|
+
'• "Remember this: [X]"\n' +
|
|
297
|
+
'• "Record this: [Y]"\n' +
|
|
298
|
+
'• "Save this for next time: [Z]"\n\n' +
|
|
299
|
+
'⚠️ DO NOT call unless user explicitly requests it.\n\n' +
|
|
300
|
+
'HOW TO WRITE:\n' +
|
|
301
|
+
'• ONE convention per memory (if user lists 5 things, call this 5 times)\n' +
|
|
302
|
+
'• memory: 5-10 words (the specific rule)\n' +
|
|
303
|
+
'• reason: 1 sentence (why it matters)\n' +
|
|
304
|
+
'• Skip: one-time features, code examples, essays',
|
|
305
|
+
inputSchema: {
|
|
306
|
+
type: 'object',
|
|
307
|
+
properties: {
|
|
308
|
+
type: {
|
|
309
|
+
type: 'string',
|
|
310
|
+
enum: ['convention', 'decision', 'gotcha', 'failure'],
|
|
311
|
+
description: 'Type of memory being recorded. Use "failure" for things that were tried and failed — ' +
|
|
312
|
+
'prevents repeating the same mistakes.'
|
|
313
|
+
},
|
|
314
|
+
category: {
|
|
315
|
+
type: 'string',
|
|
316
|
+
description: 'Broader category for filtering',
|
|
317
|
+
enum: ['tooling', 'architecture', 'testing', 'dependencies', 'conventions']
|
|
318
|
+
},
|
|
319
|
+
memory: {
|
|
320
|
+
type: 'string',
|
|
321
|
+
description: 'What to remember (concise)'
|
|
192
322
|
},
|
|
323
|
+
reason: {
|
|
324
|
+
type: 'string',
|
|
325
|
+
description: 'Why this matters or what breaks otherwise'
|
|
326
|
+
}
|
|
193
327
|
},
|
|
194
|
-
|
|
328
|
+
required: ['type', 'category', 'memory', 'reason']
|
|
329
|
+
}
|
|
195
330
|
},
|
|
331
|
+
{
|
|
332
|
+
name: 'get_memory',
|
|
333
|
+
description: 'Retrieves team conventions, architectural decisions, and known gotchas.\n' +
|
|
334
|
+
'CALL BEFORE suggesting patterns, libraries, or architecture.\n\n' +
|
|
335
|
+
'Filters: category (tooling/architecture/testing/dependencies/conventions), type (convention/decision/gotcha), query (keyword search).',
|
|
336
|
+
inputSchema: {
|
|
337
|
+
type: 'object',
|
|
338
|
+
properties: {
|
|
339
|
+
category: {
|
|
340
|
+
type: 'string',
|
|
341
|
+
description: 'Filter by category',
|
|
342
|
+
enum: ['tooling', 'architecture', 'testing', 'dependencies', 'conventions']
|
|
343
|
+
},
|
|
344
|
+
type: {
|
|
345
|
+
type: 'string',
|
|
346
|
+
description: 'Filter by memory type',
|
|
347
|
+
enum: ['convention', 'decision', 'gotcha', 'failure']
|
|
348
|
+
},
|
|
349
|
+
query: {
|
|
350
|
+
type: 'string',
|
|
351
|
+
description: 'Keyword search across memory and reason'
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
196
356
|
];
|
|
197
357
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
198
358
|
return { tools: TOOLS };
|
|
@@ -200,67 +360,86 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
200
360
|
// MCP Resources - Proactive context injection
|
|
201
361
|
const RESOURCES = [
|
|
202
362
|
{
|
|
203
|
-
uri:
|
|
204
|
-
name:
|
|
205
|
-
description:
|
|
206
|
-
|
|
207
|
-
mimeType:
|
|
208
|
-
}
|
|
363
|
+
uri: CONTEXT_RESOURCE_URI,
|
|
364
|
+
name: 'Codebase Intelligence',
|
|
365
|
+
description: 'Automatic codebase context: libraries used, team patterns, and conventions. ' +
|
|
366
|
+
'Read this BEFORE generating code to follow team standards.',
|
|
367
|
+
mimeType: 'text/plain'
|
|
368
|
+
}
|
|
209
369
|
];
|
|
210
370
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
211
371
|
return { resources: RESOURCES };
|
|
212
372
|
});
|
|
213
373
|
async function generateCodebaseContext() {
|
|
214
|
-
const intelligencePath =
|
|
374
|
+
const intelligencePath = PATHS.intelligence;
|
|
215
375
|
try {
|
|
216
|
-
const content = await fs.readFile(intelligencePath,
|
|
376
|
+
const content = await fs.readFile(intelligencePath, 'utf-8');
|
|
217
377
|
const intelligence = JSON.parse(content);
|
|
218
378
|
const lines = [];
|
|
219
|
-
lines.push(
|
|
220
|
-
lines.push(
|
|
221
|
-
lines.push(
|
|
222
|
-
lines.push(
|
|
223
|
-
lines.push(
|
|
379
|
+
lines.push('# Codebase Intelligence');
|
|
380
|
+
lines.push('');
|
|
381
|
+
lines.push('⚠️ CRITICAL: This is what YOUR codebase actually uses, not generic recommendations.');
|
|
382
|
+
lines.push('These are FACTS from analyzing your code, not best practices from the internet.');
|
|
383
|
+
lines.push('');
|
|
224
384
|
// Library usage - sorted by count
|
|
225
385
|
const libraryEntries = Object.entries(intelligence.libraryUsage || {})
|
|
226
386
|
.map(([lib, data]) => ({
|
|
227
387
|
lib,
|
|
228
|
-
count: data.count
|
|
388
|
+
count: data.count
|
|
229
389
|
}))
|
|
230
390
|
.sort((a, b) => b.count - a.count);
|
|
231
391
|
if (libraryEntries.length > 0) {
|
|
232
|
-
lines.push(
|
|
233
|
-
lines.push(
|
|
392
|
+
lines.push('## Libraries Actually Used (Top 15)');
|
|
393
|
+
lines.push('');
|
|
234
394
|
for (const { lib, count } of libraryEntries.slice(0, 15)) {
|
|
235
395
|
lines.push(`- **${lib}** (${count} uses)`);
|
|
236
396
|
}
|
|
237
|
-
lines.push(
|
|
397
|
+
lines.push('');
|
|
238
398
|
}
|
|
239
399
|
// Show tsconfig paths if available (helps AI understand internal imports)
|
|
240
400
|
if (intelligence.tsconfigPaths && Object.keys(intelligence.tsconfigPaths).length > 0) {
|
|
241
|
-
lines.push(
|
|
242
|
-
lines.push(
|
|
243
|
-
lines.push(
|
|
401
|
+
lines.push('## Import Aliases (from tsconfig.json)');
|
|
402
|
+
lines.push('');
|
|
403
|
+
lines.push('These path aliases map to internal project code:');
|
|
244
404
|
for (const [alias, paths] of Object.entries(intelligence.tsconfigPaths)) {
|
|
245
|
-
lines.push(`- \`${alias}\` → ${paths.join(
|
|
405
|
+
lines.push(`- \`${alias}\` → ${paths.join(', ')}`);
|
|
246
406
|
}
|
|
247
|
-
lines.push(
|
|
407
|
+
lines.push('');
|
|
248
408
|
}
|
|
249
409
|
// Pattern consensus
|
|
250
410
|
if (intelligence.patterns && Object.keys(intelligence.patterns).length > 0) {
|
|
411
|
+
const patterns = intelligence.patterns;
|
|
251
412
|
lines.push("## YOUR Codebase's Actual Patterns (Not Generic Best Practices)");
|
|
252
|
-
lines.push(
|
|
253
|
-
lines.push(
|
|
254
|
-
lines.push(
|
|
255
|
-
lines.push(
|
|
256
|
-
for (const [category, data] of Object.entries(
|
|
413
|
+
lines.push('');
|
|
414
|
+
lines.push('These patterns were detected by analyzing your actual code.');
|
|
415
|
+
lines.push('This is what YOUR team does in practice, not what tutorials recommend.');
|
|
416
|
+
lines.push('');
|
|
417
|
+
for (const [category, data] of Object.entries(patterns)) {
|
|
418
|
+
if (shouldSkipLegacyTestingFrameworkCategory(category, patterns)) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
257
421
|
const patternData = data;
|
|
258
422
|
const primary = patternData.primary;
|
|
423
|
+
const alternatives = patternData.alsoDetected ?? [];
|
|
259
424
|
if (!primary)
|
|
260
425
|
continue;
|
|
426
|
+
if (isComplementaryPatternCategory(category, [primary.name, ...alternatives.map((alt) => alt.name)].filter(Boolean))) {
|
|
427
|
+
const secondary = alternatives[0];
|
|
428
|
+
if (secondary) {
|
|
429
|
+
const categoryName = category
|
|
430
|
+
.replace(/([A-Z])/g, ' $1')
|
|
431
|
+
.trim()
|
|
432
|
+
.replace(/^./, (str) => str.toUpperCase());
|
|
433
|
+
lines.push(`### ${categoryName}: **${primary.name}** (${primary.frequency}) + **${secondary.name}** (${secondary.frequency})`);
|
|
434
|
+
lines.push(' → Computed and effect are complementary Signals primitives and are commonly used together.');
|
|
435
|
+
lines.push(' → Treat this as balanced usage, not a hard split decision.');
|
|
436
|
+
lines.push('');
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
261
440
|
const percentage = parseInt(primary.frequency);
|
|
262
441
|
const categoryName = category
|
|
263
|
-
.replace(/([A-Z])/g,
|
|
442
|
+
.replace(/([A-Z])/g, ' $1')
|
|
264
443
|
.trim()
|
|
265
444
|
.replace(/^./, (str) => str.toUpperCase());
|
|
266
445
|
if (percentage === 100) {
|
|
@@ -270,16 +449,16 @@ async function generateCodebaseContext() {
|
|
|
270
449
|
else if (percentage >= 80) {
|
|
271
450
|
lines.push(`### ${categoryName}: **${primary.name}** (${primary.frequency} - strong consensus)`);
|
|
272
451
|
lines.push(` → Your team strongly prefers ${primary.name}`);
|
|
273
|
-
if (
|
|
274
|
-
const alt =
|
|
452
|
+
if (alternatives.length) {
|
|
453
|
+
const alt = alternatives[0];
|
|
275
454
|
lines.push(` → Minority pattern: ${alt.name} (${alt.frequency}) - avoid for new code`);
|
|
276
455
|
}
|
|
277
456
|
}
|
|
278
457
|
else if (percentage >= 60) {
|
|
279
458
|
lines.push(`### ${categoryName}: **${primary.name}** (${primary.frequency} - majority)`);
|
|
280
459
|
lines.push(` → Most code uses ${primary.name}, but not unanimous`);
|
|
281
|
-
if (
|
|
282
|
-
lines.push(` → Also detected: ${
|
|
460
|
+
if (alternatives.length) {
|
|
461
|
+
lines.push(` → Also detected: ${alternatives[0].name} (${alternatives[0].frequency})`);
|
|
283
462
|
}
|
|
284
463
|
}
|
|
285
464
|
else {
|
|
@@ -287,49 +466,87 @@ async function generateCodebaseContext() {
|
|
|
287
466
|
lines.push(`### ${categoryName}: ⚠️ NO TEAM CONSENSUS`);
|
|
288
467
|
lines.push(` Your codebase is split between multiple approaches:`);
|
|
289
468
|
lines.push(` - ${primary.name} (${primary.frequency})`);
|
|
290
|
-
if (
|
|
291
|
-
for (const alt of
|
|
469
|
+
if (alternatives.length) {
|
|
470
|
+
for (const alt of alternatives.slice(0, 2)) {
|
|
292
471
|
lines.push(` - ${alt.name} (${alt.frequency})`);
|
|
293
472
|
}
|
|
294
473
|
}
|
|
295
474
|
lines.push(` → ASK the team which approach to use for new features`);
|
|
296
475
|
}
|
|
297
|
-
lines.push(
|
|
476
|
+
lines.push('');
|
|
298
477
|
}
|
|
299
478
|
}
|
|
300
|
-
lines.push(
|
|
479
|
+
lines.push('---');
|
|
301
480
|
lines.push(`Generated: ${intelligence.generatedAt || new Date().toISOString()}`);
|
|
302
|
-
return lines.join(
|
|
481
|
+
return lines.join('\n');
|
|
303
482
|
}
|
|
304
483
|
catch (error) {
|
|
305
|
-
return (
|
|
306
|
-
|
|
484
|
+
return ('# Codebase Intelligence\n\n' +
|
|
485
|
+
'Intelligence data not yet generated. Run indexing first.\n' +
|
|
307
486
|
`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
308
487
|
}
|
|
309
488
|
}
|
|
310
489
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
311
490
|
const uri = request.params.uri;
|
|
312
|
-
if (uri
|
|
491
|
+
if (isContextResourceUri(uri)) {
|
|
313
492
|
const content = await generateCodebaseContext();
|
|
314
493
|
return {
|
|
315
494
|
contents: [
|
|
316
495
|
{
|
|
317
|
-
uri,
|
|
318
|
-
mimeType:
|
|
319
|
-
text: content
|
|
320
|
-
}
|
|
321
|
-
]
|
|
496
|
+
uri: CONTEXT_RESOURCE_URI,
|
|
497
|
+
mimeType: 'text/plain',
|
|
498
|
+
text: content
|
|
499
|
+
}
|
|
500
|
+
]
|
|
322
501
|
};
|
|
323
502
|
}
|
|
324
503
|
throw new Error(`Unknown resource: ${uri}`);
|
|
325
504
|
});
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
505
|
+
/**
|
|
506
|
+
* Extract memories from conventional git commits (refactor:, migrate:, fix:, revert:).
|
|
507
|
+
* Scans last 90 days. Deduplicates via content hash. Zero friction alternative to manual memory.
|
|
508
|
+
*/
|
|
509
|
+
async function extractGitMemories() {
|
|
510
|
+
// Quick check: skip if not a git repo
|
|
511
|
+
if (!(await fileExists(path.join(ROOT_PATH, '.git'))))
|
|
512
|
+
return 0;
|
|
513
|
+
const { execSync } = await import('child_process');
|
|
514
|
+
let log;
|
|
329
515
|
try {
|
|
330
|
-
|
|
516
|
+
// Format: ISO-date<TAB>hash subject (e.g. "2026-01-15T10:00:00+00:00\tabc1234 fix: race condition")
|
|
517
|
+
log = execSync('git log --format="%aI\t%h %s" --since="90 days ago" --no-merges', {
|
|
518
|
+
cwd: ROOT_PATH,
|
|
519
|
+
encoding: 'utf-8',
|
|
520
|
+
timeout: 5000
|
|
521
|
+
}).trim();
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
// Git not available or command failed — silently skip
|
|
525
|
+
return 0;
|
|
526
|
+
}
|
|
527
|
+
if (!log)
|
|
528
|
+
return 0;
|
|
529
|
+
const lines = log.split('\n').filter(Boolean);
|
|
530
|
+
let added = 0;
|
|
531
|
+
for (const line of lines) {
|
|
532
|
+
const parsedMemory = parseGitLogLineToMemory(line);
|
|
533
|
+
if (!parsedMemory)
|
|
534
|
+
continue;
|
|
535
|
+
const result = await appendMemoryFile(PATHS.memory, parsedMemory);
|
|
536
|
+
if (result.status === 'added')
|
|
537
|
+
added++;
|
|
538
|
+
}
|
|
539
|
+
return added;
|
|
540
|
+
}
|
|
541
|
+
async function performIndexing(incrementalOnly) {
|
|
542
|
+
indexState.status = 'indexing';
|
|
543
|
+
const mode = incrementalOnly ? 'incremental' : 'full';
|
|
544
|
+
console.error(`Indexing (${mode}): ${ROOT_PATH}`);
|
|
545
|
+
try {
|
|
546
|
+
let lastLoggedProgress = { phase: '', percentage: -1 };
|
|
331
547
|
const indexer = new CodebaseIndexer({
|
|
332
548
|
rootPath: ROOT_PATH,
|
|
549
|
+
incrementalOnly,
|
|
333
550
|
onProgress: (progress) => {
|
|
334
551
|
// Only log when phase or percentage actually changes (prevents duplicate logs)
|
|
335
552
|
const shouldLog = progress.phase !== lastLoggedProgress.phase ||
|
|
@@ -338,23 +555,33 @@ async function performIndexing() {
|
|
|
338
555
|
console.error(`[${progress.phase}] ${progress.percentage}%`);
|
|
339
556
|
lastLoggedProgress = { phase: progress.phase, percentage: progress.percentage };
|
|
340
557
|
}
|
|
341
|
-
}
|
|
558
|
+
}
|
|
342
559
|
});
|
|
343
560
|
indexState.indexer = indexer;
|
|
344
561
|
const stats = await indexer.index();
|
|
345
|
-
indexState.status =
|
|
562
|
+
indexState.status = 'ready';
|
|
346
563
|
indexState.lastIndexed = new Date();
|
|
347
564
|
indexState.stats = stats;
|
|
348
565
|
console.error(`Complete: ${stats.indexedFiles} files, ${stats.totalChunks} chunks in ${(stats.duration / 1000).toFixed(2)}s`);
|
|
566
|
+
// Auto-extract memories from git history (non-blocking, best-effort)
|
|
567
|
+
try {
|
|
568
|
+
const gitMemories = await extractGitMemories();
|
|
569
|
+
if (gitMemories > 0) {
|
|
570
|
+
console.error(`[git-memory] Extracted ${gitMemories} new memor${gitMemories === 1 ? 'y' : 'ies'} from git history`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
// Git memory extraction is optional — never fail indexing over it
|
|
575
|
+
}
|
|
349
576
|
}
|
|
350
577
|
catch (error) {
|
|
351
|
-
indexState.status =
|
|
578
|
+
indexState.status = 'error';
|
|
352
579
|
indexState.error = error instanceof Error ? error.message : String(error);
|
|
353
|
-
console.error(
|
|
580
|
+
console.error('Indexing failed:', indexState.error);
|
|
354
581
|
}
|
|
355
582
|
}
|
|
356
583
|
async function shouldReindex() {
|
|
357
|
-
const indexPath =
|
|
584
|
+
const indexPath = PATHS.keywordIndex;
|
|
358
585
|
try {
|
|
359
586
|
await fs.access(indexPath);
|
|
360
587
|
return false;
|
|
@@ -367,68 +594,400 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
367
594
|
const { name, arguments: args } = request.params;
|
|
368
595
|
try {
|
|
369
596
|
switch (name) {
|
|
370
|
-
case
|
|
371
|
-
const { query, limit, filters } = args;
|
|
372
|
-
if (indexState.status ===
|
|
597
|
+
case 'search_codebase': {
|
|
598
|
+
const { query, limit, filters, intent } = args;
|
|
599
|
+
if (indexState.status === 'indexing') {
|
|
373
600
|
return {
|
|
374
601
|
content: [
|
|
375
602
|
{
|
|
376
|
-
type:
|
|
603
|
+
type: 'text',
|
|
377
604
|
text: JSON.stringify({
|
|
378
|
-
status:
|
|
379
|
-
message:
|
|
380
|
-
progress: indexState.indexer?.getProgress()
|
|
381
|
-
}, null, 2)
|
|
382
|
-
}
|
|
383
|
-
]
|
|
605
|
+
status: 'indexing',
|
|
606
|
+
message: 'Index is still being built. Retry in a moment.',
|
|
607
|
+
progress: indexState.indexer?.getProgress()
|
|
608
|
+
}, null, 2)
|
|
609
|
+
}
|
|
610
|
+
]
|
|
384
611
|
};
|
|
385
612
|
}
|
|
386
|
-
if (indexState.status ===
|
|
613
|
+
if (indexState.status === 'error') {
|
|
387
614
|
return {
|
|
388
615
|
content: [
|
|
389
616
|
{
|
|
390
|
-
type:
|
|
617
|
+
type: 'text',
|
|
391
618
|
text: JSON.stringify({
|
|
392
|
-
status:
|
|
393
|
-
message: `Indexing failed: ${indexState.error}
|
|
394
|
-
}, null, 2)
|
|
395
|
-
}
|
|
396
|
-
]
|
|
619
|
+
status: 'error',
|
|
620
|
+
message: `Indexing failed: ${indexState.error}`
|
|
621
|
+
}, null, 2)
|
|
622
|
+
}
|
|
623
|
+
]
|
|
397
624
|
};
|
|
398
625
|
}
|
|
399
626
|
const searcher = new CodebaseSearcher(ROOT_PATH);
|
|
400
|
-
|
|
627
|
+
let results;
|
|
628
|
+
const searchProfile = intent && ['explore', 'edit', 'refactor', 'migrate'].includes(intent)
|
|
629
|
+
? intent
|
|
630
|
+
: 'explore';
|
|
631
|
+
try {
|
|
632
|
+
results = await searcher.search(query, limit || 5, filters, {
|
|
633
|
+
profile: searchProfile
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
if (error instanceof IndexCorruptedError) {
|
|
638
|
+
console.error('[Auto-Heal] Index corrupted. Triggering full re-index...');
|
|
639
|
+
await performIndexing();
|
|
640
|
+
if (indexState.status === 'ready') {
|
|
641
|
+
console.error('[Auto-Heal] Success. Retrying search...');
|
|
642
|
+
const freshSearcher = new CodebaseSearcher(ROOT_PATH);
|
|
643
|
+
try {
|
|
644
|
+
results = await freshSearcher.search(query, limit || 5, filters, {
|
|
645
|
+
profile: searchProfile
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
catch (retryError) {
|
|
649
|
+
return {
|
|
650
|
+
content: [
|
|
651
|
+
{
|
|
652
|
+
type: 'text',
|
|
653
|
+
text: JSON.stringify({
|
|
654
|
+
status: 'error',
|
|
655
|
+
message: `Auto-heal retry failed: ${retryError instanceof Error ? retryError.message : String(retryError)}`
|
|
656
|
+
}, null, 2)
|
|
657
|
+
}
|
|
658
|
+
]
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
return {
|
|
664
|
+
content: [
|
|
665
|
+
{
|
|
666
|
+
type: 'text',
|
|
667
|
+
text: JSON.stringify({
|
|
668
|
+
status: 'error',
|
|
669
|
+
message: `Auto-heal failed: Indexing ended with status '${indexState.status}'`,
|
|
670
|
+
error: indexState.error
|
|
671
|
+
}, null, 2)
|
|
672
|
+
}
|
|
673
|
+
]
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
throw error; // Propagate unexpected errors
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// Load memories for keyword matching, enriched with confidence
|
|
682
|
+
const allMemories = await readMemoriesFile(PATHS.memory);
|
|
683
|
+
const allMemoriesWithConf = withConfidence(allMemories);
|
|
684
|
+
const queryTerms = query.toLowerCase().split(/\s+/);
|
|
685
|
+
const relatedMemories = allMemoriesWithConf
|
|
686
|
+
.filter((m) => {
|
|
687
|
+
const searchText = `${m.memory} ${m.reason}`.toLowerCase();
|
|
688
|
+
return queryTerms.some((term) => searchText.includes(term));
|
|
689
|
+
})
|
|
690
|
+
.sort((a, b) => b.effectiveConfidence - a.effectiveConfidence);
|
|
691
|
+
// Load intelligence data for enrichment (all intents, not just preflight)
|
|
692
|
+
let intelligence = null;
|
|
693
|
+
try {
|
|
694
|
+
const intelligenceContent = await fs.readFile(PATHS.intelligence, 'utf-8');
|
|
695
|
+
intelligence = JSON.parse(intelligenceContent);
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
/* graceful degradation — intelligence file may not exist yet */
|
|
699
|
+
}
|
|
700
|
+
// Build reverse import map from intelligence graph
|
|
701
|
+
const reverseImports = new Map();
|
|
702
|
+
if (intelligence?.internalFileGraph?.imports) {
|
|
703
|
+
for (const [file, deps] of Object.entries(intelligence.internalFileGraph.imports)) {
|
|
704
|
+
for (const dep of deps) {
|
|
705
|
+
if (!reverseImports.has(dep))
|
|
706
|
+
reverseImports.set(dep, []);
|
|
707
|
+
reverseImports.get(dep).push(file);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// Load git dates for lastModified enrichment
|
|
712
|
+
let gitDates = null;
|
|
713
|
+
try {
|
|
714
|
+
gitDates = await getFileCommitDates(ROOT_PATH);
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
/* not a git repo */
|
|
718
|
+
}
|
|
719
|
+
// Enrich a search result with relationship data
|
|
720
|
+
function enrichResult(r) {
|
|
721
|
+
const rPath = r.filePath;
|
|
722
|
+
// importedBy: files that import this result (reverse lookup)
|
|
723
|
+
const importedBy = [];
|
|
724
|
+
for (const [dep, importers] of reverseImports) {
|
|
725
|
+
if (dep.endsWith(rPath) || rPath.endsWith(dep)) {
|
|
726
|
+
importedBy.push(...importers);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// imports: files this result depends on (forward lookup)
|
|
730
|
+
const imports = [];
|
|
731
|
+
if (intelligence?.internalFileGraph?.imports) {
|
|
732
|
+
for (const [file, deps] of Object.entries(intelligence.internalFileGraph.imports)) {
|
|
733
|
+
if (file.endsWith(rPath) || rPath.endsWith(file)) {
|
|
734
|
+
imports.push(...deps);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
// testedIn: heuristic — same basename with .spec/.test extension
|
|
739
|
+
const testedIn = [];
|
|
740
|
+
const baseName = path.basename(rPath).replace(/\.[^.]+$/, '');
|
|
741
|
+
if (intelligence?.internalFileGraph?.imports) {
|
|
742
|
+
for (const file of Object.keys(intelligence.internalFileGraph.imports)) {
|
|
743
|
+
const fileBase = path.basename(file);
|
|
744
|
+
if ((fileBase.includes('.spec.') || fileBase.includes('.test.')) &&
|
|
745
|
+
fileBase.startsWith(baseName)) {
|
|
746
|
+
testedIn.push(file);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
// lastModified: from git dates
|
|
751
|
+
let lastModified;
|
|
752
|
+
if (gitDates) {
|
|
753
|
+
// Try matching by relative path (git dates use repo-relative forward-slash paths)
|
|
754
|
+
const relPath = path.relative(ROOT_PATH, rPath).replace(/\\/g, '/');
|
|
755
|
+
const date = gitDates.get(relPath);
|
|
756
|
+
if (date) {
|
|
757
|
+
lastModified = date.toISOString();
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// Only return if we have at least one piece of data
|
|
761
|
+
if (importedBy.length === 0 &&
|
|
762
|
+
imports.length === 0 &&
|
|
763
|
+
testedIn.length === 0 &&
|
|
764
|
+
!lastModified) {
|
|
765
|
+
return undefined;
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
...(importedBy.length > 0 && { importedBy }),
|
|
769
|
+
...(imports.length > 0 && { imports }),
|
|
770
|
+
...(testedIn.length > 0 && { testedIn }),
|
|
771
|
+
...(lastModified && { lastModified })
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const searchQuality = assessSearchQuality(query, results);
|
|
775
|
+
// Compose preflight card for edit/refactor/migrate intents
|
|
776
|
+
let preflight = undefined;
|
|
777
|
+
const preflightIntents = ['edit', 'refactor', 'migrate'];
|
|
778
|
+
if (intent && preflightIntents.includes(intent) && intelligence) {
|
|
779
|
+
try {
|
|
780
|
+
// --- Avoid / Prefer patterns ---
|
|
781
|
+
const avoidPatterns = [];
|
|
782
|
+
const preferredPatterns = [];
|
|
783
|
+
const patterns = intelligence.patterns || {};
|
|
784
|
+
for (const [category, data] of Object.entries(patterns)) {
|
|
785
|
+
// Primary pattern = preferred if Rising or Stable
|
|
786
|
+
if (data.primary) {
|
|
787
|
+
const p = data.primary;
|
|
788
|
+
if (p.trend === 'Rising' || p.trend === 'Stable') {
|
|
789
|
+
preferredPatterns.push({
|
|
790
|
+
pattern: p.name,
|
|
791
|
+
category,
|
|
792
|
+
adoption: p.frequency,
|
|
793
|
+
trend: p.trend,
|
|
794
|
+
guidance: p.guidance,
|
|
795
|
+
...(p.canonicalExample && { example: p.canonicalExample.file })
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// Also-detected patterns that are Declining = avoid
|
|
800
|
+
if (data.alsoDetected) {
|
|
801
|
+
for (const alt of data.alsoDetected) {
|
|
802
|
+
if (alt.trend === 'Declining') {
|
|
803
|
+
avoidPatterns.push({
|
|
804
|
+
pattern: alt.name,
|
|
805
|
+
category,
|
|
806
|
+
adoption: alt.frequency,
|
|
807
|
+
trend: 'Declining',
|
|
808
|
+
guidance: alt.guidance
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// --- Impact candidates (files importing the result files) ---
|
|
815
|
+
const impactCandidates = [];
|
|
816
|
+
const resultPaths = results.map((r) => r.filePath);
|
|
817
|
+
if (intelligence.internalFileGraph?.imports) {
|
|
818
|
+
const allImports = intelligence.internalFileGraph.imports;
|
|
819
|
+
for (const [file, deps] of Object.entries(allImports)) {
|
|
820
|
+
if (deps.some((dep) => resultPaths.some((rp) => dep.endsWith(rp) || rp.endsWith(dep)))) {
|
|
821
|
+
if (!resultPaths.some((rp) => file.endsWith(rp) || rp.endsWith(file))) {
|
|
822
|
+
impactCandidates.push(file);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
// --- Risk level (based on circular deps + impact breadth) ---
|
|
828
|
+
let riskLevel = 'low';
|
|
829
|
+
let cycleCount = 0;
|
|
830
|
+
if (intelligence.internalFileGraph) {
|
|
831
|
+
try {
|
|
832
|
+
const graph = InternalFileGraph.fromJSON(intelligence.internalFileGraph, ROOT_PATH);
|
|
833
|
+
// Use directory prefixes as scope (not full file paths)
|
|
834
|
+
// findCycles(scope) filters files by startsWith, so a full path would only match itself
|
|
835
|
+
const scopes = new Set(resultPaths.map((rp) => {
|
|
836
|
+
const lastSlash = rp.lastIndexOf('/');
|
|
837
|
+
return lastSlash > 0 ? rp.substring(0, lastSlash + 1) : rp;
|
|
838
|
+
}));
|
|
839
|
+
for (const scope of scopes) {
|
|
840
|
+
const cycles = graph.findCycles(scope);
|
|
841
|
+
cycleCount += cycles.length;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
// Graph reconstruction failed — skip cycle check
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (cycleCount > 0 || impactCandidates.length > 10) {
|
|
849
|
+
riskLevel = 'high';
|
|
850
|
+
}
|
|
851
|
+
else if (impactCandidates.length > 3) {
|
|
852
|
+
riskLevel = 'medium';
|
|
853
|
+
}
|
|
854
|
+
// --- Golden files (exemplar code) ---
|
|
855
|
+
const goldenFiles = (intelligence.goldenFiles || []).slice(0, 3).map((g) => ({
|
|
856
|
+
file: g.file,
|
|
857
|
+
score: g.score
|
|
858
|
+
}));
|
|
859
|
+
// --- Confidence (index freshness) ---
|
|
860
|
+
let confidence = 'stale';
|
|
861
|
+
if (intelligence.generatedAt) {
|
|
862
|
+
const indexAge = Date.now() - new Date(intelligence.generatedAt).getTime();
|
|
863
|
+
const hoursOld = indexAge / (1000 * 60 * 60);
|
|
864
|
+
if (hoursOld < 24) {
|
|
865
|
+
confidence = 'fresh';
|
|
866
|
+
}
|
|
867
|
+
else if (hoursOld < 168) {
|
|
868
|
+
confidence = 'aging';
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
// --- Failure memories (1.5x relevance boost) ---
|
|
872
|
+
const failureWarnings = relatedMemories
|
|
873
|
+
.filter((m) => m.type === 'failure' && !m.stale)
|
|
874
|
+
.map((m) => ({
|
|
875
|
+
memory: m.memory,
|
|
876
|
+
reason: m.reason,
|
|
877
|
+
confidence: m.effectiveConfidence
|
|
878
|
+
}))
|
|
879
|
+
.slice(0, 3);
|
|
880
|
+
const preferredPatternsForOutput = preferredPatterns.slice(0, 5);
|
|
881
|
+
const avoidPatternsForOutput = avoidPatterns.slice(0, 5);
|
|
882
|
+
// --- Pattern conflicts (split decisions within categories) ---
|
|
883
|
+
const patternConflicts = [];
|
|
884
|
+
const hasUnitTestFramework = Boolean(patterns.unitTestFramework?.primary);
|
|
885
|
+
for (const [cat, data] of Object.entries(patterns)) {
|
|
886
|
+
if (shouldSkipLegacyTestingFrameworkCategory(cat, patterns))
|
|
887
|
+
continue;
|
|
888
|
+
if (!shouldIncludePatternConflictCategory(cat, query))
|
|
889
|
+
continue;
|
|
890
|
+
if (!data.primary || !data.alsoDetected?.length)
|
|
891
|
+
continue;
|
|
892
|
+
const primaryFreq = parseFloat(data.primary.frequency) || 100;
|
|
893
|
+
if (primaryFreq >= 80)
|
|
894
|
+
continue;
|
|
895
|
+
for (const alt of data.alsoDetected) {
|
|
896
|
+
const altFreq = parseFloat(alt.frequency) || 0;
|
|
897
|
+
if (altFreq >= 20) {
|
|
898
|
+
if (isComplementaryPatternConflict(cat, data.primary.name, alt.name))
|
|
899
|
+
continue;
|
|
900
|
+
if (hasUnitTestFramework && cat === 'testingFramework')
|
|
901
|
+
continue;
|
|
902
|
+
patternConflicts.push({
|
|
903
|
+
category: cat,
|
|
904
|
+
primary: { name: data.primary.name, adoption: data.primary.frequency },
|
|
905
|
+
alternative: { name: alt.name, adoption: alt.frequency }
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
const evidenceLock = buildEvidenceLock({
|
|
911
|
+
results,
|
|
912
|
+
preferredPatterns: preferredPatternsForOutput,
|
|
913
|
+
relatedMemories,
|
|
914
|
+
failureWarnings,
|
|
915
|
+
patternConflicts
|
|
916
|
+
});
|
|
917
|
+
// Bump risk if there are active failure memories for this area
|
|
918
|
+
if (failureWarnings.length > 0 && riskLevel === 'low') {
|
|
919
|
+
riskLevel = 'medium';
|
|
920
|
+
}
|
|
921
|
+
// If evidence triangulation is weak, avoid claiming low risk
|
|
922
|
+
if (evidenceLock.status === 'block' && riskLevel === 'low') {
|
|
923
|
+
riskLevel = 'medium';
|
|
924
|
+
}
|
|
925
|
+
// If epistemic stress says abstain, bump risk
|
|
926
|
+
if (evidenceLock.epistemicStress?.abstain && riskLevel === 'low') {
|
|
927
|
+
riskLevel = 'medium';
|
|
928
|
+
}
|
|
929
|
+
preflight = {
|
|
930
|
+
intent,
|
|
931
|
+
riskLevel,
|
|
932
|
+
confidence,
|
|
933
|
+
evidenceLock,
|
|
934
|
+
...(preferredPatternsForOutput.length > 0 && {
|
|
935
|
+
preferredPatterns: preferredPatternsForOutput
|
|
936
|
+
}),
|
|
937
|
+
...(avoidPatternsForOutput.length > 0 && {
|
|
938
|
+
avoidPatterns: avoidPatternsForOutput
|
|
939
|
+
}),
|
|
940
|
+
...(goldenFiles.length > 0 && { goldenFiles }),
|
|
941
|
+
...(impactCandidates.length > 0 && {
|
|
942
|
+
impactCandidates: impactCandidates.slice(0, 10)
|
|
943
|
+
}),
|
|
944
|
+
...(cycleCount > 0 && { circularDependencies: cycleCount }),
|
|
945
|
+
...(failureWarnings.length > 0 && { failureWarnings })
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
catch {
|
|
949
|
+
// Preflight construction failed — skip preflight, don't fail the search
|
|
950
|
+
}
|
|
951
|
+
}
|
|
401
952
|
return {
|
|
402
953
|
content: [
|
|
403
954
|
{
|
|
404
|
-
type:
|
|
955
|
+
type: 'text',
|
|
405
956
|
text: JSON.stringify({
|
|
406
|
-
status:
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
957
|
+
status: 'success',
|
|
958
|
+
...(preflight && { preflight }),
|
|
959
|
+
searchQuality,
|
|
960
|
+
results: results.map((r) => {
|
|
961
|
+
const relationships = enrichResult(r);
|
|
962
|
+
return {
|
|
963
|
+
summary: r.summary,
|
|
964
|
+
snippet: r.snippet,
|
|
965
|
+
filePath: `${r.filePath}:${r.startLine}-${r.endLine}`,
|
|
966
|
+
score: r.score,
|
|
967
|
+
relevanceReason: r.relevanceReason,
|
|
968
|
+
componentType: r.componentType,
|
|
969
|
+
layer: r.layer,
|
|
970
|
+
framework: r.framework,
|
|
971
|
+
trend: r.trend,
|
|
972
|
+
patternWarning: r.patternWarning,
|
|
973
|
+
...(relationships && { relationships })
|
|
974
|
+
};
|
|
975
|
+
}),
|
|
420
976
|
totalResults: results.length,
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
977
|
+
...(relatedMemories.length > 0 && {
|
|
978
|
+
relatedMemories: relatedMemories.slice(0, 5)
|
|
979
|
+
})
|
|
980
|
+
}, null, 2)
|
|
981
|
+
}
|
|
982
|
+
]
|
|
424
983
|
};
|
|
425
984
|
}
|
|
426
|
-
case
|
|
985
|
+
case 'get_indexing_status': {
|
|
427
986
|
const progress = indexState.indexer?.getProgress();
|
|
428
987
|
return {
|
|
429
988
|
content: [
|
|
430
989
|
{
|
|
431
|
-
type:
|
|
990
|
+
type: 'text',
|
|
432
991
|
text: JSON.stringify({
|
|
433
992
|
status: indexState.status,
|
|
434
993
|
rootPath: ROOT_PATH,
|
|
@@ -439,6 +998,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
439
998
|
indexedFiles: indexState.stats.indexedFiles,
|
|
440
999
|
totalChunks: indexState.stats.totalChunks,
|
|
441
1000
|
duration: `${(indexState.stats.duration / 1000).toFixed(2)}s`,
|
|
1001
|
+
incremental: indexState.stats.incremental
|
|
442
1002
|
}
|
|
443
1003
|
: undefined,
|
|
444
1004
|
progress: progress
|
|
@@ -446,69 +1006,63 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
446
1006
|
phase: progress.phase,
|
|
447
1007
|
percentage: progress.percentage,
|
|
448
1008
|
filesProcessed: progress.filesProcessed,
|
|
449
|
-
totalFiles: progress.totalFiles
|
|
1009
|
+
totalFiles: progress.totalFiles
|
|
450
1010
|
}
|
|
451
1011
|
: undefined,
|
|
452
1012
|
error: indexState.error,
|
|
453
|
-
hint:
|
|
454
|
-
}, null, 2)
|
|
455
|
-
}
|
|
456
|
-
]
|
|
1013
|
+
hint: 'Use refresh_index to manually trigger re-indexing when needed.'
|
|
1014
|
+
}, null, 2)
|
|
1015
|
+
}
|
|
1016
|
+
]
|
|
457
1017
|
};
|
|
458
1018
|
}
|
|
459
|
-
case
|
|
1019
|
+
case 'refresh_index': {
|
|
460
1020
|
const { reason, incrementalOnly } = args;
|
|
461
|
-
const mode = incrementalOnly ?
|
|
462
|
-
console.error(`Refresh requested (${mode}): ${reason ||
|
|
463
|
-
|
|
464
|
-
// use `incrementalOnly` to only re-index changed files.
|
|
465
|
-
// For now, always do full re-index but acknowledge the intention.
|
|
466
|
-
performIndexing();
|
|
1021
|
+
const mode = incrementalOnly ? 'incremental' : 'full';
|
|
1022
|
+
console.error(`Refresh requested (${mode}): ${reason || 'Manual trigger'}`);
|
|
1023
|
+
performIndexing(incrementalOnly);
|
|
467
1024
|
return {
|
|
468
1025
|
content: [
|
|
469
1026
|
{
|
|
470
|
-
type:
|
|
1027
|
+
type: 'text',
|
|
471
1028
|
text: JSON.stringify({
|
|
472
|
-
status:
|
|
1029
|
+
status: 'started',
|
|
473
1030
|
mode,
|
|
474
1031
|
message: incrementalOnly
|
|
475
|
-
?
|
|
476
|
-
:
|
|
477
|
-
reason
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
}, null, 2),
|
|
482
|
-
},
|
|
483
|
-
],
|
|
1032
|
+
? 'Incremental re-indexing started. Only changed files will be re-embedded.'
|
|
1033
|
+
: 'Full re-indexing started. Check status with get_indexing_status.',
|
|
1034
|
+
reason
|
|
1035
|
+
}, null, 2)
|
|
1036
|
+
}
|
|
1037
|
+
]
|
|
484
1038
|
};
|
|
485
1039
|
}
|
|
486
|
-
case
|
|
1040
|
+
case 'get_codebase_metadata': {
|
|
487
1041
|
const indexer = new CodebaseIndexer({ rootPath: ROOT_PATH });
|
|
488
1042
|
const metadata = await indexer.detectMetadata();
|
|
489
1043
|
// Load team patterns from intelligence file
|
|
490
1044
|
let teamPatterns = {};
|
|
491
1045
|
try {
|
|
492
|
-
const intelligencePath =
|
|
493
|
-
const intelligenceContent = await fs.readFile(intelligencePath,
|
|
1046
|
+
const intelligencePath = PATHS.intelligence;
|
|
1047
|
+
const intelligenceContent = await fs.readFile(intelligencePath, 'utf-8');
|
|
494
1048
|
const intelligence = JSON.parse(intelligenceContent);
|
|
495
1049
|
if (intelligence.patterns) {
|
|
496
1050
|
teamPatterns = {
|
|
497
1051
|
dependencyInjection: intelligence.patterns.dependencyInjection,
|
|
498
1052
|
stateManagement: intelligence.patterns.stateManagement,
|
|
499
|
-
componentInputs: intelligence.patterns.componentInputs
|
|
1053
|
+
componentInputs: intelligence.patterns.componentInputs
|
|
500
1054
|
};
|
|
501
1055
|
}
|
|
502
1056
|
}
|
|
503
|
-
catch (
|
|
1057
|
+
catch (_error) {
|
|
504
1058
|
// No intelligence file or parsing error
|
|
505
1059
|
}
|
|
506
1060
|
return {
|
|
507
1061
|
content: [
|
|
508
1062
|
{
|
|
509
|
-
type:
|
|
1063
|
+
type: 'text',
|
|
510
1064
|
text: JSON.stringify({
|
|
511
|
-
status:
|
|
1065
|
+
status: 'success',
|
|
512
1066
|
metadata: {
|
|
513
1067
|
name: metadata.name,
|
|
514
1068
|
framework: metadata.framework,
|
|
@@ -517,23 +1071,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
517
1071
|
architecture: metadata.architecture,
|
|
518
1072
|
projectStructure: metadata.projectStructure,
|
|
519
1073
|
statistics: metadata.statistics,
|
|
520
|
-
teamPatterns
|
|
521
|
-
}
|
|
522
|
-
}, null, 2)
|
|
523
|
-
}
|
|
524
|
-
]
|
|
1074
|
+
teamPatterns
|
|
1075
|
+
}
|
|
1076
|
+
}, null, 2)
|
|
1077
|
+
}
|
|
1078
|
+
]
|
|
525
1079
|
};
|
|
526
1080
|
}
|
|
527
|
-
case
|
|
1081
|
+
case 'get_style_guide': {
|
|
528
1082
|
const { query, category } = args;
|
|
529
1083
|
const styleGuidePatterns = [
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
1084
|
+
'STYLE_GUIDE.md',
|
|
1085
|
+
'CODING_STYLE.md',
|
|
1086
|
+
'ARCHITECTURE.md',
|
|
1087
|
+
'CONTRIBUTING.md',
|
|
1088
|
+
'docs/style-guide.md',
|
|
1089
|
+
'docs/coding-style.md',
|
|
1090
|
+
'docs/ARCHITECTURE.md'
|
|
537
1091
|
];
|
|
538
1092
|
const foundGuides = [];
|
|
539
1093
|
const queryLower = query.toLowerCase();
|
|
@@ -542,13 +1096,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
542
1096
|
try {
|
|
543
1097
|
const files = await glob(pattern, {
|
|
544
1098
|
cwd: ROOT_PATH,
|
|
545
|
-
absolute: true
|
|
1099
|
+
absolute: true
|
|
546
1100
|
});
|
|
547
1101
|
for (const file of files) {
|
|
548
1102
|
try {
|
|
549
1103
|
// Normalize line endings to \n for consistent output
|
|
550
|
-
const rawContent = await fs.readFile(file,
|
|
551
|
-
const content = rawContent.replace(/\r\n/g,
|
|
1104
|
+
const rawContent = await fs.readFile(file, 'utf-8');
|
|
1105
|
+
const content = rawContent.replace(/\r\n/g, '\n');
|
|
552
1106
|
const relativePath = path.relative(ROOT_PATH, file);
|
|
553
1107
|
// Find relevant sections based on query
|
|
554
1108
|
const sections = content.split(/^##\s+/m);
|
|
@@ -559,27 +1113,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
559
1113
|
if (isRelevant) {
|
|
560
1114
|
// Limit section size to ~500 words
|
|
561
1115
|
const words = section.split(/\s+/);
|
|
562
|
-
const truncated = words.slice(0, 500).join(
|
|
563
|
-
relevantSections.push(
|
|
564
|
-
(words.length > 500
|
|
565
|
-
? truncated + "..."
|
|
566
|
-
: section.trim()));
|
|
1116
|
+
const truncated = words.slice(0, 500).join(' ');
|
|
1117
|
+
relevantSections.push('## ' + (words.length > 500 ? truncated + '...' : section.trim()));
|
|
567
1118
|
}
|
|
568
1119
|
}
|
|
569
1120
|
if (relevantSections.length > 0) {
|
|
570
1121
|
foundGuides.push({
|
|
571
1122
|
file: relativePath,
|
|
572
|
-
content: content.slice(0, 200) +
|
|
573
|
-
relevantSections: relevantSections.slice(0, 3)
|
|
1123
|
+
content: content.slice(0, 200) + '...',
|
|
1124
|
+
relevantSections: relevantSections.slice(0, 3) // Max 3 sections per file
|
|
574
1125
|
});
|
|
575
1126
|
}
|
|
576
1127
|
}
|
|
577
|
-
catch (
|
|
1128
|
+
catch (_e) {
|
|
578
1129
|
// Skip unreadable files
|
|
579
1130
|
}
|
|
580
1131
|
}
|
|
581
1132
|
}
|
|
582
|
-
catch (
|
|
1133
|
+
catch (_e) {
|
|
583
1134
|
// Pattern didn't match, continue
|
|
584
1135
|
}
|
|
585
1136
|
}
|
|
@@ -587,86 +1138,149 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
587
1138
|
return {
|
|
588
1139
|
content: [
|
|
589
1140
|
{
|
|
590
|
-
type:
|
|
1141
|
+
type: 'text',
|
|
591
1142
|
text: JSON.stringify({
|
|
592
|
-
status:
|
|
1143
|
+
status: 'no_results',
|
|
593
1144
|
message: `No style guide content found matching: ${query}`,
|
|
594
1145
|
searchedPatterns: styleGuidePatterns,
|
|
595
|
-
hint: "Try broader terms like 'naming', 'patterns', 'testing', 'components'"
|
|
596
|
-
}, null, 2)
|
|
597
|
-
}
|
|
598
|
-
]
|
|
1146
|
+
hint: "Try broader terms like 'naming', 'patterns', 'testing', 'components'"
|
|
1147
|
+
}, null, 2)
|
|
1148
|
+
}
|
|
1149
|
+
]
|
|
599
1150
|
};
|
|
600
1151
|
}
|
|
601
1152
|
return {
|
|
602
1153
|
content: [
|
|
603
1154
|
{
|
|
604
|
-
type:
|
|
1155
|
+
type: 'text',
|
|
605
1156
|
text: JSON.stringify({
|
|
606
|
-
status:
|
|
1157
|
+
status: 'success',
|
|
607
1158
|
query,
|
|
608
1159
|
category,
|
|
609
1160
|
results: foundGuides,
|
|
610
|
-
totalFiles: foundGuides.length
|
|
611
|
-
}, null, 2)
|
|
612
|
-
}
|
|
613
|
-
]
|
|
1161
|
+
totalFiles: foundGuides.length
|
|
1162
|
+
}, null, 2)
|
|
1163
|
+
}
|
|
1164
|
+
]
|
|
614
1165
|
};
|
|
615
1166
|
}
|
|
616
|
-
case
|
|
1167
|
+
case 'get_team_patterns': {
|
|
617
1168
|
const { category } = args;
|
|
618
1169
|
try {
|
|
619
|
-
const intelligencePath =
|
|
620
|
-
const content = await fs.readFile(intelligencePath,
|
|
1170
|
+
const intelligencePath = PATHS.intelligence;
|
|
1171
|
+
const content = await fs.readFile(intelligencePath, 'utf-8');
|
|
621
1172
|
const intelligence = JSON.parse(content);
|
|
622
|
-
const result = { status:
|
|
623
|
-
if (category ===
|
|
1173
|
+
const result = { status: 'success' };
|
|
1174
|
+
if (category === 'all' || !category) {
|
|
624
1175
|
result.patterns = intelligence.patterns || {};
|
|
625
1176
|
result.goldenFiles = intelligence.goldenFiles || [];
|
|
626
1177
|
if (intelligence.tsconfigPaths) {
|
|
627
1178
|
result.tsconfigPaths = intelligence.tsconfigPaths;
|
|
628
1179
|
}
|
|
629
1180
|
}
|
|
630
|
-
else if (category ===
|
|
1181
|
+
else if (category === 'di') {
|
|
631
1182
|
result.dependencyInjection = intelligence.patterns?.dependencyInjection;
|
|
632
1183
|
}
|
|
633
|
-
else if (category ===
|
|
1184
|
+
else if (category === 'state') {
|
|
634
1185
|
result.stateManagement = intelligence.patterns?.stateManagement;
|
|
635
1186
|
}
|
|
636
|
-
else if (category ===
|
|
1187
|
+
else if (category === 'testing') {
|
|
1188
|
+
result.unitTestFramework = intelligence.patterns?.unitTestFramework;
|
|
1189
|
+
result.e2eFramework = intelligence.patterns?.e2eFramework;
|
|
637
1190
|
result.testingFramework = intelligence.patterns?.testingFramework;
|
|
638
1191
|
result.testMocking = intelligence.patterns?.testMocking;
|
|
639
1192
|
}
|
|
640
|
-
else if (category ===
|
|
1193
|
+
else if (category === 'libraries') {
|
|
641
1194
|
result.topUsed = intelligence.importGraph?.topUsed || [];
|
|
642
1195
|
if (intelligence.tsconfigPaths) {
|
|
643
1196
|
result.tsconfigPaths = intelligence.tsconfigPaths;
|
|
644
1197
|
}
|
|
645
1198
|
}
|
|
1199
|
+
// Load and append matching memories
|
|
1200
|
+
try {
|
|
1201
|
+
const allMemories = await readMemoriesFile(PATHS.memory);
|
|
1202
|
+
// Map pattern categories to decision categories
|
|
1203
|
+
const categoryMap = {
|
|
1204
|
+
all: ['tooling', 'architecture', 'testing', 'dependencies', 'conventions'],
|
|
1205
|
+
di: ['architecture', 'conventions'],
|
|
1206
|
+
state: ['architecture', 'conventions'],
|
|
1207
|
+
testing: ['testing'],
|
|
1208
|
+
libraries: ['dependencies']
|
|
1209
|
+
};
|
|
1210
|
+
const relevantCategories = categoryMap[category || 'all'] || [];
|
|
1211
|
+
const matchingMemories = allMemories.filter((m) => relevantCategories.includes(m.category));
|
|
1212
|
+
if (matchingMemories.length > 0) {
|
|
1213
|
+
result.memories = matchingMemories;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
catch (_error) {
|
|
1217
|
+
// No memory file yet, that's fine - don't fail the whole request
|
|
1218
|
+
}
|
|
1219
|
+
// Detect pattern conflicts: primary < 80% and any alternative > 20%
|
|
1220
|
+
const conflicts = [];
|
|
1221
|
+
const patternsData = intelligence.patterns || {};
|
|
1222
|
+
const hasUnitTestFramework = Boolean(patternsData.unitTestFramework?.primary);
|
|
1223
|
+
for (const [cat, data] of Object.entries(patternsData)) {
|
|
1224
|
+
if (shouldSkipLegacyTestingFrameworkCategory(cat, patternsData))
|
|
1225
|
+
continue;
|
|
1226
|
+
if (category && category !== 'all' && cat !== category)
|
|
1227
|
+
continue;
|
|
1228
|
+
if (!data.primary || !data.alsoDetected?.length)
|
|
1229
|
+
continue;
|
|
1230
|
+
const primaryFreq = parseFloat(data.primary.frequency) || 100;
|
|
1231
|
+
if (primaryFreq >= 80)
|
|
1232
|
+
continue;
|
|
1233
|
+
for (const alt of data.alsoDetected) {
|
|
1234
|
+
const altFreq = parseFloat(alt.frequency) || 0;
|
|
1235
|
+
if (altFreq < 20)
|
|
1236
|
+
continue;
|
|
1237
|
+
if (isComplementaryPatternConflict(cat, data.primary.name, alt.name))
|
|
1238
|
+
continue;
|
|
1239
|
+
if (hasUnitTestFramework && cat === 'testingFramework')
|
|
1240
|
+
continue;
|
|
1241
|
+
conflicts.push({
|
|
1242
|
+
category: cat,
|
|
1243
|
+
primary: {
|
|
1244
|
+
name: data.primary.name,
|
|
1245
|
+
adoption: data.primary.frequency,
|
|
1246
|
+
trend: data.primary.trend
|
|
1247
|
+
},
|
|
1248
|
+
alternative: {
|
|
1249
|
+
name: alt.name,
|
|
1250
|
+
adoption: alt.frequency,
|
|
1251
|
+
trend: alt.trend
|
|
1252
|
+
},
|
|
1253
|
+
note: `Split decision: ${data.primary.frequency} ${data.primary.name} (${data.primary.trend || 'unknown'}) vs ${alt.frequency} ${alt.name} (${alt.trend || 'unknown'})`
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
if (conflicts.length > 0) {
|
|
1258
|
+
result.conflicts = conflicts;
|
|
1259
|
+
}
|
|
646
1260
|
return {
|
|
647
|
-
content: [{ type:
|
|
1261
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
648
1262
|
};
|
|
649
1263
|
}
|
|
650
1264
|
catch (error) {
|
|
651
1265
|
return {
|
|
652
1266
|
content: [
|
|
653
1267
|
{
|
|
654
|
-
type:
|
|
1268
|
+
type: 'text',
|
|
655
1269
|
text: JSON.stringify({
|
|
656
|
-
status:
|
|
657
|
-
message:
|
|
658
|
-
error: error instanceof Error ? error.message : String(error)
|
|
659
|
-
}, null, 2)
|
|
660
|
-
}
|
|
661
|
-
]
|
|
1270
|
+
status: 'error',
|
|
1271
|
+
message: 'Failed to load team patterns',
|
|
1272
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1273
|
+
}, null, 2)
|
|
1274
|
+
}
|
|
1275
|
+
]
|
|
662
1276
|
};
|
|
663
1277
|
}
|
|
664
1278
|
}
|
|
665
|
-
case
|
|
1279
|
+
case 'get_component_usage': {
|
|
666
1280
|
const { name: componentName } = args;
|
|
667
1281
|
try {
|
|
668
|
-
const intelligencePath =
|
|
669
|
-
const content = await fs.readFile(intelligencePath,
|
|
1282
|
+
const intelligencePath = PATHS.intelligence;
|
|
1283
|
+
const content = await fs.readFile(intelligencePath, 'utf-8');
|
|
670
1284
|
const intelligence = JSON.parse(content);
|
|
671
1285
|
const importGraph = intelligence.importGraph || {};
|
|
672
1286
|
const usages = importGraph.usages || {};
|
|
@@ -674,68 +1288,76 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
674
1288
|
let matchedUsage = usages[componentName];
|
|
675
1289
|
// Try partial match if exact match not found
|
|
676
1290
|
if (!matchedUsage) {
|
|
677
|
-
const matchingKeys = Object.keys(usages).filter(key => key.includes(componentName) || componentName.includes(key));
|
|
1291
|
+
const matchingKeys = Object.keys(usages).filter((key) => key.includes(componentName) || componentName.includes(key));
|
|
678
1292
|
if (matchingKeys.length > 0) {
|
|
679
1293
|
matchedUsage = usages[matchingKeys[0]];
|
|
680
1294
|
}
|
|
681
1295
|
}
|
|
682
1296
|
if (matchedUsage) {
|
|
683
1297
|
return {
|
|
684
|
-
content: [
|
|
685
|
-
|
|
1298
|
+
content: [
|
|
1299
|
+
{
|
|
1300
|
+
type: 'text',
|
|
686
1301
|
text: JSON.stringify({
|
|
687
|
-
status:
|
|
1302
|
+
status: 'success',
|
|
688
1303
|
component: componentName,
|
|
689
1304
|
usageCount: matchedUsage.usageCount,
|
|
690
|
-
usedIn: matchedUsage.usedIn
|
|
691
|
-
}, null, 2)
|
|
692
|
-
}
|
|
1305
|
+
usedIn: matchedUsage.usedIn
|
|
1306
|
+
}, null, 2)
|
|
1307
|
+
}
|
|
1308
|
+
]
|
|
693
1309
|
};
|
|
694
1310
|
}
|
|
695
1311
|
else {
|
|
696
1312
|
// Show top used as alternatives
|
|
697
1313
|
const topUsed = importGraph.topUsed || [];
|
|
698
1314
|
return {
|
|
699
|
-
content: [
|
|
700
|
-
|
|
1315
|
+
content: [
|
|
1316
|
+
{
|
|
1317
|
+
type: 'text',
|
|
701
1318
|
text: JSON.stringify({
|
|
702
|
-
status:
|
|
1319
|
+
status: 'not_found',
|
|
703
1320
|
component: componentName,
|
|
704
1321
|
message: `No usages found for '${componentName}'.`,
|
|
705
|
-
suggestions: topUsed.slice(0, 10)
|
|
706
|
-
}, null, 2)
|
|
707
|
-
}
|
|
1322
|
+
suggestions: topUsed.slice(0, 10)
|
|
1323
|
+
}, null, 2)
|
|
1324
|
+
}
|
|
1325
|
+
]
|
|
708
1326
|
};
|
|
709
1327
|
}
|
|
710
1328
|
}
|
|
711
1329
|
catch (error) {
|
|
712
1330
|
return {
|
|
713
|
-
content: [
|
|
714
|
-
|
|
1331
|
+
content: [
|
|
1332
|
+
{
|
|
1333
|
+
type: 'text',
|
|
715
1334
|
text: JSON.stringify({
|
|
716
|
-
status:
|
|
717
|
-
message:
|
|
718
|
-
error: error instanceof Error ? error.message : String(error)
|
|
719
|
-
}, null, 2)
|
|
720
|
-
}
|
|
1335
|
+
status: 'error',
|
|
1336
|
+
message: 'Failed to get component usage. Run indexing first.',
|
|
1337
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1338
|
+
}, null, 2)
|
|
1339
|
+
}
|
|
1340
|
+
]
|
|
721
1341
|
};
|
|
722
1342
|
}
|
|
723
1343
|
}
|
|
724
|
-
case
|
|
1344
|
+
case 'detect_circular_dependencies': {
|
|
725
1345
|
const { scope } = args;
|
|
726
1346
|
try {
|
|
727
|
-
const intelligencePath =
|
|
728
|
-
const content = await fs.readFile(intelligencePath,
|
|
1347
|
+
const intelligencePath = PATHS.intelligence;
|
|
1348
|
+
const content = await fs.readFile(intelligencePath, 'utf-8');
|
|
729
1349
|
const intelligence = JSON.parse(content);
|
|
730
1350
|
if (!intelligence.internalFileGraph) {
|
|
731
1351
|
return {
|
|
732
|
-
content: [
|
|
733
|
-
|
|
1352
|
+
content: [
|
|
1353
|
+
{
|
|
1354
|
+
type: 'text',
|
|
734
1355
|
text: JSON.stringify({
|
|
735
|
-
status:
|
|
736
|
-
message:
|
|
737
|
-
}, null, 2)
|
|
738
|
-
}
|
|
1356
|
+
status: 'error',
|
|
1357
|
+
message: 'Internal file graph not found. Please run refresh_index to rebuild the index with cycle detection support.'
|
|
1358
|
+
}, null, 2)
|
|
1359
|
+
}
|
|
1360
|
+
]
|
|
739
1361
|
};
|
|
740
1362
|
}
|
|
741
1363
|
// Reconstruct the graph from stored data
|
|
@@ -744,48 +1366,176 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
744
1366
|
const graphStats = intelligence.internalFileGraph.stats || graph.getStats();
|
|
745
1367
|
if (cycles.length === 0) {
|
|
746
1368
|
return {
|
|
747
|
-
content: [
|
|
748
|
-
|
|
1369
|
+
content: [
|
|
1370
|
+
{
|
|
1371
|
+
type: 'text',
|
|
749
1372
|
text: JSON.stringify({
|
|
750
|
-
status:
|
|
1373
|
+
status: 'success',
|
|
751
1374
|
message: scope
|
|
752
1375
|
? `No circular dependencies detected in scope: ${scope}`
|
|
753
|
-
:
|
|
1376
|
+
: 'No circular dependencies detected in the codebase.',
|
|
754
1377
|
scope,
|
|
755
|
-
graphStats
|
|
756
|
-
}, null, 2)
|
|
757
|
-
}
|
|
1378
|
+
graphStats
|
|
1379
|
+
}, null, 2)
|
|
1380
|
+
}
|
|
1381
|
+
]
|
|
758
1382
|
};
|
|
759
1383
|
}
|
|
760
1384
|
return {
|
|
761
|
-
content: [
|
|
762
|
-
|
|
1385
|
+
content: [
|
|
1386
|
+
{
|
|
1387
|
+
type: 'text',
|
|
763
1388
|
text: JSON.stringify({
|
|
764
|
-
status:
|
|
1389
|
+
status: 'warning',
|
|
765
1390
|
message: `Found ${cycles.length} circular dependency cycle(s).`,
|
|
766
1391
|
scope,
|
|
767
|
-
cycles: cycles.map(c => ({
|
|
1392
|
+
cycles: cycles.map((c) => ({
|
|
768
1393
|
files: c.files,
|
|
769
1394
|
length: c.length,
|
|
770
|
-
severity: c.length === 2 ?
|
|
1395
|
+
severity: c.length === 2 ? 'high' : c.length <= 3 ? 'medium' : 'low'
|
|
771
1396
|
})),
|
|
772
1397
|
count: cycles.length,
|
|
773
1398
|
graphStats,
|
|
774
|
-
advice:
|
|
775
|
-
}, null, 2)
|
|
776
|
-
}
|
|
1399
|
+
advice: 'Shorter cycles (length 2-3) are typically more problematic. Consider breaking the cycle by extracting shared dependencies.'
|
|
1400
|
+
}, null, 2)
|
|
1401
|
+
}
|
|
1402
|
+
]
|
|
777
1403
|
};
|
|
778
1404
|
}
|
|
779
1405
|
catch (error) {
|
|
780
1406
|
return {
|
|
781
|
-
content: [
|
|
782
|
-
|
|
1407
|
+
content: [
|
|
1408
|
+
{
|
|
1409
|
+
type: 'text',
|
|
1410
|
+
text: JSON.stringify({
|
|
1411
|
+
status: 'error',
|
|
1412
|
+
message: 'Failed to detect circular dependencies. Run indexing first.',
|
|
1413
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1414
|
+
}, null, 2)
|
|
1415
|
+
}
|
|
1416
|
+
]
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
case 'remember': {
|
|
1421
|
+
const args_typed = args;
|
|
1422
|
+
const { type = 'decision', category, memory, reason } = args_typed;
|
|
1423
|
+
try {
|
|
1424
|
+
const crypto = await import('crypto');
|
|
1425
|
+
const memoryPath = PATHS.memory;
|
|
1426
|
+
const hashContent = `${type}:${category}:${memory}:${reason}`;
|
|
1427
|
+
const hash = crypto.createHash('sha256').update(hashContent).digest('hex');
|
|
1428
|
+
const id = hash.substring(0, 12);
|
|
1429
|
+
const newMemory = {
|
|
1430
|
+
id,
|
|
1431
|
+
type,
|
|
1432
|
+
category,
|
|
1433
|
+
memory,
|
|
1434
|
+
reason,
|
|
1435
|
+
date: new Date().toISOString()
|
|
1436
|
+
};
|
|
1437
|
+
const result = await appendMemoryFile(memoryPath, newMemory);
|
|
1438
|
+
if (result.status === 'duplicate') {
|
|
1439
|
+
return {
|
|
1440
|
+
content: [
|
|
1441
|
+
{
|
|
1442
|
+
type: 'text',
|
|
1443
|
+
text: JSON.stringify({
|
|
1444
|
+
status: 'info',
|
|
1445
|
+
message: 'This memory was already recorded.',
|
|
1446
|
+
memory: result.memory
|
|
1447
|
+
}, null, 2)
|
|
1448
|
+
}
|
|
1449
|
+
]
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
return {
|
|
1453
|
+
content: [
|
|
1454
|
+
{
|
|
1455
|
+
type: 'text',
|
|
1456
|
+
text: JSON.stringify({
|
|
1457
|
+
status: 'success',
|
|
1458
|
+
message: 'Memory recorded successfully.',
|
|
1459
|
+
memory: result.memory
|
|
1460
|
+
}, null, 2)
|
|
1461
|
+
}
|
|
1462
|
+
]
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
catch (error) {
|
|
1466
|
+
return {
|
|
1467
|
+
content: [
|
|
1468
|
+
{
|
|
1469
|
+
type: 'text',
|
|
1470
|
+
text: JSON.stringify({
|
|
1471
|
+
status: 'error',
|
|
1472
|
+
message: 'Failed to record memory.',
|
|
1473
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1474
|
+
}, null, 2)
|
|
1475
|
+
}
|
|
1476
|
+
]
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
case 'get_memory': {
|
|
1481
|
+
const { category, type, query } = args;
|
|
1482
|
+
try {
|
|
1483
|
+
const memoryPath = PATHS.memory;
|
|
1484
|
+
const allMemories = await readMemoriesFile(memoryPath);
|
|
1485
|
+
if (allMemories.length === 0) {
|
|
1486
|
+
return {
|
|
1487
|
+
content: [
|
|
1488
|
+
{
|
|
1489
|
+
type: 'text',
|
|
1490
|
+
text: JSON.stringify({
|
|
1491
|
+
status: 'success',
|
|
1492
|
+
message: "No team conventions recorded yet. Use 'remember' to build tribal knowledge or memory when the user corrects you over a repeatable pattern.",
|
|
1493
|
+
memories: [],
|
|
1494
|
+
count: 0
|
|
1495
|
+
}, null, 2)
|
|
1496
|
+
}
|
|
1497
|
+
]
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
const filtered = filterMemories(allMemories, { category, type, query });
|
|
1501
|
+
const limited = applyUnfilteredLimit(filtered, { category, type, query }, 20);
|
|
1502
|
+
// Enrich with confidence decay
|
|
1503
|
+
const enriched = withConfidence(limited.memories);
|
|
1504
|
+
const staleCount = enriched.filter((m) => m.stale).length;
|
|
1505
|
+
return {
|
|
1506
|
+
content: [
|
|
1507
|
+
{
|
|
1508
|
+
type: 'text',
|
|
1509
|
+
text: JSON.stringify({
|
|
1510
|
+
status: 'success',
|
|
1511
|
+
count: enriched.length,
|
|
1512
|
+
totalCount: limited.totalCount,
|
|
1513
|
+
truncated: limited.truncated,
|
|
1514
|
+
...(staleCount > 0 && {
|
|
1515
|
+
staleCount,
|
|
1516
|
+
staleNote: `${staleCount} memor${staleCount === 1 ? 'y' : 'ies'} below 30% confidence. Consider reviewing or removing.`
|
|
1517
|
+
}),
|
|
1518
|
+
message: limited.truncated
|
|
1519
|
+
? 'Showing 20 most recent. Use filters (category/type/query) for targeted results.'
|
|
1520
|
+
: undefined,
|
|
1521
|
+
memories: enriched
|
|
1522
|
+
}, null, 2)
|
|
1523
|
+
}
|
|
1524
|
+
]
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
catch (error) {
|
|
1528
|
+
return {
|
|
1529
|
+
content: [
|
|
1530
|
+
{
|
|
1531
|
+
type: 'text',
|
|
783
1532
|
text: JSON.stringify({
|
|
784
|
-
status:
|
|
785
|
-
message:
|
|
786
|
-
error: error instanceof Error ? error.message : String(error)
|
|
787
|
-
}, null, 2)
|
|
788
|
-
}
|
|
1533
|
+
status: 'error',
|
|
1534
|
+
message: 'Failed to retrieve memories.',
|
|
1535
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1536
|
+
}, null, 2)
|
|
1537
|
+
}
|
|
1538
|
+
]
|
|
789
1539
|
};
|
|
790
1540
|
}
|
|
791
1541
|
}
|
|
@@ -793,13 +1543,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
793
1543
|
return {
|
|
794
1544
|
content: [
|
|
795
1545
|
{
|
|
796
|
-
type:
|
|
1546
|
+
type: 'text',
|
|
797
1547
|
text: JSON.stringify({
|
|
798
|
-
error: `Unknown tool: ${name}
|
|
799
|
-
}, null, 2)
|
|
800
|
-
}
|
|
1548
|
+
error: `Unknown tool: ${name}`
|
|
1549
|
+
}, null, 2)
|
|
1550
|
+
}
|
|
801
1551
|
],
|
|
802
|
-
isError: true
|
|
1552
|
+
isError: true
|
|
803
1553
|
};
|
|
804
1554
|
}
|
|
805
1555
|
}
|
|
@@ -807,27 +1557,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
807
1557
|
return {
|
|
808
1558
|
content: [
|
|
809
1559
|
{
|
|
810
|
-
type:
|
|
1560
|
+
type: 'text',
|
|
811
1561
|
text: JSON.stringify({
|
|
812
1562
|
error: error instanceof Error ? error.message : String(error),
|
|
813
|
-
stack: error instanceof Error ? error.stack : undefined
|
|
814
|
-
}, null, 2)
|
|
815
|
-
}
|
|
1563
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
1564
|
+
}, null, 2)
|
|
1565
|
+
}
|
|
816
1566
|
],
|
|
817
|
-
isError: true
|
|
1567
|
+
isError: true
|
|
818
1568
|
};
|
|
819
1569
|
}
|
|
820
1570
|
});
|
|
821
1571
|
async function main() {
|
|
822
|
-
// Server startup banner (guarded to avoid stderr during MCP STDIO handshake)
|
|
823
|
-
if (process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
824
|
-
console.error("[DEBUG] Codebase Context MCP Server");
|
|
825
|
-
console.error(`[DEBUG] Root: ${ROOT_PATH}`);
|
|
826
|
-
console.error(`[DEBUG] Analyzers: ${analyzerRegistry
|
|
827
|
-
.getAll()
|
|
828
|
-
.map((a) => a.name)
|
|
829
|
-
.join(", ")}`);
|
|
830
|
-
}
|
|
831
1572
|
// Validate root path exists and is a directory
|
|
832
1573
|
try {
|
|
833
1574
|
const stats = await fs.stat(ROOT_PATH);
|
|
@@ -837,15 +1578,37 @@ async function main() {
|
|
|
837
1578
|
process.exit(1);
|
|
838
1579
|
}
|
|
839
1580
|
}
|
|
840
|
-
catch (
|
|
1581
|
+
catch (_error) {
|
|
841
1582
|
console.error(`ERROR: Root path does not exist: ${ROOT_PATH}`);
|
|
842
1583
|
console.error(`Please specify a valid project directory.`);
|
|
843
1584
|
process.exit(1);
|
|
844
1585
|
}
|
|
1586
|
+
// Migrate legacy structure before server starts
|
|
1587
|
+
try {
|
|
1588
|
+
const migrated = await migrateToNewStructure();
|
|
1589
|
+
if (migrated && process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
1590
|
+
console.error('[DEBUG] Migrated to .codebase-context/ structure');
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
catch (error) {
|
|
1594
|
+
// Non-fatal: continue with current paths
|
|
1595
|
+
if (process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
1596
|
+
console.error('[DEBUG] Migration failed:', error);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
// Server startup banner (guarded to avoid stderr during MCP STDIO handshake)
|
|
1600
|
+
if (process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
1601
|
+
console.error('[DEBUG] Codebase Context MCP Server');
|
|
1602
|
+
console.error(`[DEBUG] Root: ${ROOT_PATH}`);
|
|
1603
|
+
console.error(`[DEBUG] Analyzers: ${analyzerRegistry
|
|
1604
|
+
.getAll()
|
|
1605
|
+
.map((a) => a.name)
|
|
1606
|
+
.join(', ')}`);
|
|
1607
|
+
}
|
|
845
1608
|
// Check for package.json to confirm it's a project root (guarded to avoid stderr during handshake)
|
|
846
1609
|
if (process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
847
1610
|
try {
|
|
848
|
-
await fs.access(path.join(ROOT_PATH,
|
|
1611
|
+
await fs.access(path.join(ROOT_PATH, 'package.json'));
|
|
849
1612
|
console.error(`[DEBUG] Project detected: ${path.basename(ROOT_PATH)}`);
|
|
850
1613
|
}
|
|
851
1614
|
catch {
|
|
@@ -855,29 +1618,29 @@ async function main() {
|
|
|
855
1618
|
const needsIndex = await shouldReindex();
|
|
856
1619
|
if (needsIndex) {
|
|
857
1620
|
if (process.env.CODEBASE_CONTEXT_DEBUG)
|
|
858
|
-
console.error(
|
|
1621
|
+
console.error('[DEBUG] Starting indexing...');
|
|
859
1622
|
performIndexing();
|
|
860
1623
|
}
|
|
861
1624
|
else {
|
|
862
1625
|
if (process.env.CODEBASE_CONTEXT_DEBUG)
|
|
863
|
-
console.error(
|
|
864
|
-
indexState.status =
|
|
1626
|
+
console.error('[DEBUG] Index found. Ready.');
|
|
1627
|
+
indexState.status = 'ready';
|
|
865
1628
|
indexState.lastIndexed = new Date();
|
|
866
1629
|
}
|
|
867
1630
|
const transport = new StdioServerTransport();
|
|
868
1631
|
await server.connect(transport);
|
|
869
1632
|
if (process.env.CODEBASE_CONTEXT_DEBUG)
|
|
870
|
-
console.error(
|
|
1633
|
+
console.error('[DEBUG] Server ready');
|
|
871
1634
|
}
|
|
872
1635
|
// Export server components for programmatic use
|
|
873
1636
|
export { server, performIndexing, resolveRootPath, shouldReindex, TOOLS };
|
|
874
1637
|
// Only auto-start when run directly as CLI (not when imported as module)
|
|
875
1638
|
// Check if this module is the entry point
|
|
876
|
-
const isDirectRun = process.argv[1]?.replace(/\\/g,
|
|
877
|
-
process.argv[1]?.replace(/\\/g,
|
|
1639
|
+
const isDirectRun = process.argv[1]?.replace(/\\/g, '/').endsWith('index.js') ||
|
|
1640
|
+
process.argv[1]?.replace(/\\/g, '/').endsWith('index.ts');
|
|
878
1641
|
if (isDirectRun) {
|
|
879
1642
|
main().catch((error) => {
|
|
880
|
-
console.error(
|
|
1643
|
+
console.error('Fatal:', error);
|
|
881
1644
|
process.exit(1);
|
|
882
1645
|
});
|
|
883
1646
|
}
|