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.
Files changed (131) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +292 -87
  3. package/dist/analyzers/angular/index.d.ts +1 -1
  4. package/dist/analyzers/angular/index.d.ts.map +1 -1
  5. package/dist/analyzers/angular/index.js +298 -309
  6. package/dist/analyzers/angular/index.js.map +1 -1
  7. package/dist/analyzers/generic/index.d.ts +1 -2
  8. package/dist/analyzers/generic/index.d.ts.map +1 -1
  9. package/dist/analyzers/generic/index.js +93 -60
  10. package/dist/analyzers/generic/index.js.map +1 -1
  11. package/dist/constants/codebase-context.d.ts +8 -0
  12. package/dist/constants/codebase-context.d.ts.map +1 -0
  13. package/dist/constants/codebase-context.js +10 -0
  14. package/dist/constants/codebase-context.js.map +1 -0
  15. package/dist/constants/git-patterns.d.ts +12 -0
  16. package/dist/constants/git-patterns.d.ts.map +1 -0
  17. package/dist/constants/git-patterns.js +11 -0
  18. package/dist/constants/git-patterns.js.map +1 -0
  19. package/dist/core/analyzer-registry.d.ts.map +1 -1
  20. package/dist/core/analyzer-registry.js +8 -8
  21. package/dist/core/analyzer-registry.js.map +1 -1
  22. package/dist/core/indexer.d.ts +11 -1
  23. package/dist/core/indexer.d.ts.map +1 -1
  24. package/dist/core/indexer.js +359 -157
  25. package/dist/core/indexer.js.map +1 -1
  26. package/dist/core/manifest.d.ts +39 -0
  27. package/dist/core/manifest.d.ts.map +1 -0
  28. package/dist/core/manifest.js +86 -0
  29. package/dist/core/manifest.js.map +1 -0
  30. package/dist/core/search-quality.d.ts +10 -0
  31. package/dist/core/search-quality.d.ts.map +1 -0
  32. package/dist/core/search-quality.js +64 -0
  33. package/dist/core/search-quality.js.map +1 -0
  34. package/dist/core/search.d.ts +17 -1
  35. package/dist/core/search.d.ts.map +1 -1
  36. package/dist/core/search.js +303 -104
  37. package/dist/core/search.js.map +1 -1
  38. package/dist/embeddings/openai.d.ts.map +1 -1
  39. package/dist/embeddings/openai.js +2 -2
  40. package/dist/embeddings/openai.js.map +1 -1
  41. package/dist/embeddings/transformers.d.ts +1 -1
  42. package/dist/embeddings/transformers.d.ts.map +1 -1
  43. package/dist/embeddings/transformers.js +19 -15
  44. package/dist/embeddings/transformers.js.map +1 -1
  45. package/dist/embeddings/types.d.ts +1 -1
  46. package/dist/embeddings/types.d.ts.map +1 -1
  47. package/dist/embeddings/types.js +3 -3
  48. package/dist/embeddings/types.js.map +1 -1
  49. package/dist/errors/index.d.ts +8 -0
  50. package/dist/errors/index.d.ts.map +1 -0
  51. package/dist/errors/index.js +11 -0
  52. package/dist/errors/index.js.map +1 -0
  53. package/dist/index.d.ts +7 -29
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +1125 -362
  56. package/dist/index.js.map +1 -1
  57. package/dist/lib.d.ts +18 -18
  58. package/dist/lib.d.ts.map +1 -1
  59. package/dist/lib.js +23 -23
  60. package/dist/lib.js.map +1 -1
  61. package/dist/memory/git-memory.d.ts +9 -0
  62. package/dist/memory/git-memory.d.ts.map +1 -0
  63. package/dist/memory/git-memory.js +51 -0
  64. package/dist/memory/git-memory.js.map +1 -0
  65. package/dist/memory/store.d.ts +38 -0
  66. package/dist/memory/store.d.ts.map +1 -0
  67. package/dist/memory/store.js +136 -0
  68. package/dist/memory/store.js.map +1 -0
  69. package/dist/patterns/semantics.d.ts +4 -0
  70. package/dist/patterns/semantics.d.ts.map +1 -0
  71. package/dist/patterns/semantics.js +24 -0
  72. package/dist/patterns/semantics.js.map +1 -0
  73. package/dist/preflight/evidence-lock.d.ts +50 -0
  74. package/dist/preflight/evidence-lock.d.ts.map +1 -0
  75. package/dist/preflight/evidence-lock.js +130 -0
  76. package/dist/preflight/evidence-lock.js.map +1 -0
  77. package/dist/preflight/query-scope.d.ts +3 -0
  78. package/dist/preflight/query-scope.d.ts.map +1 -0
  79. package/dist/preflight/query-scope.js +40 -0
  80. package/dist/preflight/query-scope.js.map +1 -0
  81. package/dist/resources/uri.d.ts +5 -0
  82. package/dist/resources/uri.d.ts.map +1 -0
  83. package/dist/resources/uri.js +15 -0
  84. package/dist/resources/uri.js.map +1 -0
  85. package/dist/storage/lancedb.d.ts +1 -0
  86. package/dist/storage/lancedb.d.ts.map +1 -1
  87. package/dist/storage/lancedb.js +51 -34
  88. package/dist/storage/lancedb.js.map +1 -1
  89. package/dist/storage/types.d.ts +5 -0
  90. package/dist/storage/types.d.ts.map +1 -1
  91. package/dist/storage/types.js +2 -1
  92. package/dist/storage/types.js.map +1 -1
  93. package/dist/types/index.d.ts +47 -0
  94. package/dist/types/index.d.ts.map +1 -1
  95. package/dist/types/index.js +1 -0
  96. package/dist/types/index.js.map +1 -1
  97. package/dist/utils/chunking.d.ts.map +1 -1
  98. package/dist/utils/chunking.js +10 -9
  99. package/dist/utils/chunking.js.map +1 -1
  100. package/dist/utils/dependency-detection.d.ts +18 -0
  101. package/dist/utils/dependency-detection.d.ts.map +1 -0
  102. package/dist/utils/dependency-detection.js +102 -0
  103. package/dist/utils/dependency-detection.js.map +1 -0
  104. package/dist/utils/git-dates.d.ts +1 -0
  105. package/dist/utils/git-dates.d.ts.map +1 -1
  106. package/dist/utils/git-dates.js +23 -3
  107. package/dist/utils/git-dates.js.map +1 -1
  108. package/dist/utils/language-detection.d.ts.map +1 -1
  109. package/dist/utils/language-detection.js +69 -17
  110. package/dist/utils/language-detection.js.map +1 -1
  111. package/dist/utils/usage-tracker.d.ts +2 -2
  112. package/dist/utils/usage-tracker.d.ts.map +1 -1
  113. package/dist/utils/usage-tracker.js +67 -38
  114. package/dist/utils/usage-tracker.js.map +1 -1
  115. package/dist/utils/workspace-detection.d.ts +32 -0
  116. package/dist/utils/workspace-detection.d.ts.map +1 -0
  117. package/dist/utils/workspace-detection.js +107 -0
  118. package/dist/utils/workspace-detection.js.map +1 -0
  119. package/package.json +122 -97
  120. package/dist/core/file-watcher.d.ts +0 -63
  121. package/dist/core/file-watcher.d.ts.map +0 -1
  122. package/dist/core/file-watcher.js +0 -210
  123. package/dist/core/file-watcher.js.map +0 -1
  124. package/dist/utils/logger.d.ts +0 -36
  125. package/dist/utils/logger.d.ts.map +0 -1
  126. package/dist/utils/logger.js +0 -111
  127. package/dist/utils/logger.js.map +0 -1
  128. package/dist/utils/pattern-detector.d.ts +0 -41
  129. package/dist/utils/pattern-detector.d.ts.map +0 -1
  130. package/dist/utils/pattern-detector.js +0 -101
  131. 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 "fs";
7
- import path from "path";
8
- import { glob } from "glob";
9
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
10
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
- import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
12
- import { CodebaseIndexer } from "./core/indexer.js";
13
- import { CodebaseSearcher } from "./core/search.js";
14
- import { analyzerRegistry } from "./core/analyzer-registry.js";
15
- import { AngularAnalyzer } from "./analyzers/angular/index.js";
16
- import { GenericAnalyzer } from "./analyzers/generic/index.js";
17
- import { InternalFileGraph } from "./utils/usage-tracker.js";
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: "idle",
123
+ status: 'idle'
37
124
  };
38
125
  const server = new Server({
39
- name: "codebase-context",
40
- version: "1.2.2",
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: "search_codebase",
50
- description: "Search the indexed codebase using natural language queries. Returns code summaries with file locations. " +
51
- "Supports framework-specific queries and architectural layer filtering. " +
52
- "Use the returned filePath with other tools to read complete file contents.",
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: "object",
143
+ type: 'object',
55
144
  properties: {
56
145
  query: {
57
- type: "string",
58
- description: "Natural language search query",
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: "number",
62
- description: "Maximum number of results to return (default: 5)",
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: "object",
67
- description: "Optional filters",
162
+ type: 'object',
163
+ description: 'Optional filters',
68
164
  properties: {
69
165
  framework: {
70
- type: "string",
71
- description: "Filter by framework (angular, react, vue)",
166
+ type: 'string',
167
+ description: 'Filter by framework (angular, react, vue)'
72
168
  },
73
169
  language: {
74
- type: "string",
75
- description: "Filter by programming language",
170
+ type: 'string',
171
+ description: 'Filter by programming language'
76
172
  },
77
173
  componentType: {
78
- type: "string",
79
- description: "Filter by component type (component, service, directive, etc.)",
174
+ type: 'string',
175
+ description: 'Filter by component type (component, service, directive, etc.)'
80
176
  },
81
177
  layer: {
82
- type: "string",
83
- description: "Filter by architectural layer (presentation, business, data, state, core, shared)",
178
+ type: 'string',
179
+ description: 'Filter by architectural layer (presentation, business, data, state, core, shared)'
84
180
  },
85
181
  tags: {
86
- type: "array",
87
- items: { type: "string" },
88
- description: "Filter by tags",
89
- },
90
- },
91
- },
182
+ type: 'array',
183
+ items: { type: 'string' },
184
+ description: 'Filter by tags'
185
+ }
186
+ }
187
+ }
92
188
  },
93
- required: ["query"],
94
- },
189
+ required: ['query']
190
+ }
95
191
  },
96
192
  {
97
- name: "get_codebase_metadata",
98
- description: "Get codebase metadata including framework information, dependencies, architecture patterns, " +
99
- "and project statistics.",
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: "object",
102
- properties: {},
103
- },
197
+ type: 'object',
198
+ properties: {}
199
+ }
104
200
  },
105
201
  {
106
- name: "get_indexing_status",
107
- description: "Get current indexing status: state, statistics, and progress. " +
108
- "Use refresh_index to manually trigger re-indexing when needed.",
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: "object",
111
- properties: {},
112
- },
206
+ type: 'object',
207
+ properties: {}
208
+ }
113
209
  },
114
210
  {
115
- name: "refresh_index",
116
- description: "Re-index the codebase. Supports full re-index or incremental mode. " +
117
- "Use incrementalOnly=true to only process files changed since last index.",
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: "object",
215
+ type: 'object',
120
216
  properties: {
121
217
  reason: {
122
- type: "string",
123
- description: "Reason for refreshing the index (for logging)",
218
+ type: 'string',
219
+ description: 'Reason for refreshing the index (for logging)'
124
220
  },
125
221
  incrementalOnly: {
126
- type: "boolean",
127
- description: "If true, only re-index files changed since last full index (faster). Default: false (full re-index)",
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: "get_style_guide",
134
- description: "Query style guide rules and architectural patterns from project documentation.",
229
+ name: 'get_style_guide',
230
+ description: 'Query style guide rules and architectural patterns from project documentation.',
135
231
  inputSchema: {
136
- type: "object",
232
+ type: 'object',
137
233
  properties: {
138
234
  query: {
139
- type: "string",
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: "string",
144
- description: "Filter by category (naming, structure, patterns, testing)",
145
- },
239
+ type: 'string',
240
+ description: 'Filter by category (naming, structure, patterns, testing)'
241
+ }
146
242
  },
147
- required: ["query"],
148
- },
243
+ required: ['query']
244
+ }
149
245
  },
150
246
  {
151
- name: "get_team_patterns",
152
- description: "Get actionable team pattern recommendations based on codebase analysis. " +
153
- "Returns consensus patterns for DI, state management, testing, library wrappers, etc.",
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: "object",
251
+ type: 'object',
156
252
  properties: {
157
253
  category: {
158
- type: "string",
159
- description: "Pattern category to retrieve",
160
- enum: ["all", "di", "state", "testing", "libraries"],
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: "get_component_usage",
167
- description: "Find WHERE a library or component is used in the codebase. " +
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: "object",
267
+ type: 'object',
172
268
  properties: {
173
269
  name: {
174
- type: "string",
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: ["name"],
179
- },
274
+ required: ['name']
275
+ }
180
276
  },
181
277
  {
182
- name: "detect_circular_dependencies",
183
- description: "Analyze the import graph to detect circular dependencies between files. " +
184
- "Circular dependencies can cause initialization issues, tight coupling, and maintenance problems. " +
185
- "Returns all detected cycles sorted by length (shorter cycles are often more problematic).",
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: "object",
283
+ type: 'object',
188
284
  properties: {
189
285
  scope: {
190
- type: "string",
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: "codebase://context",
204
- name: "Codebase Intelligence",
205
- description: "Automatic codebase context: libraries used, team patterns, and conventions. " +
206
- "Read this BEFORE generating code to follow team standards.",
207
- mimeType: "text/plain",
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 = path.join(ROOT_PATH, ".codebase-intelligence.json");
374
+ const intelligencePath = PATHS.intelligence;
215
375
  try {
216
- const content = await fs.readFile(intelligencePath, "utf-8");
376
+ const content = await fs.readFile(intelligencePath, 'utf-8');
217
377
  const intelligence = JSON.parse(content);
218
378
  const lines = [];
219
- lines.push("# Codebase Intelligence");
220
- lines.push("");
221
- lines.push("⚠️ CRITICAL: This is what YOUR codebase actually uses, not generic recommendations.");
222
- lines.push("These are FACTS from analyzing your code, not best practices from the internet.");
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("## Libraries Actually Used (Top 15)");
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("## Import Aliases (from tsconfig.json)");
242
- lines.push("");
243
- lines.push("These path aliases map to internal project code:");
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("These patterns were detected by analyzing your actual code.");
254
- lines.push("This is what YOUR team does in practice, not what tutorials recommend.");
255
- lines.push("");
256
- for (const [category, data] of Object.entries(intelligence.patterns)) {
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, " $1")
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 (patternData.alsoDetected?.length) {
274
- const alt = patternData.alsoDetected[0];
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 (patternData.alsoDetected?.length) {
282
- lines.push(` → Also detected: ${patternData.alsoDetected[0].name} (${patternData.alsoDetected[0].frequency})`);
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 (patternData.alsoDetected?.length) {
291
- for (const alt of patternData.alsoDetected.slice(0, 2)) {
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("\n");
481
+ return lines.join('\n');
303
482
  }
304
483
  catch (error) {
305
- return ("# Codebase Intelligence\n\n" +
306
- "Intelligence data not yet generated. Run indexing first.\n" +
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 === "codebase://context") {
491
+ if (isContextResourceUri(uri)) {
313
492
  const content = await generateCodebaseContext();
314
493
  return {
315
494
  contents: [
316
495
  {
317
- uri,
318
- mimeType: "text/plain",
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
- async function performIndexing() {
327
- indexState.status = "indexing";
328
- console.error(`Indexing: ${ROOT_PATH}`);
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
- let lastLoggedProgress = { phase: "", percentage: -1 };
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 = "ready";
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 = "error";
578
+ indexState.status = 'error';
352
579
  indexState.error = error instanceof Error ? error.message : String(error);
353
- console.error("Indexing failed:", indexState.error);
580
+ console.error('Indexing failed:', indexState.error);
354
581
  }
355
582
  }
356
583
  async function shouldReindex() {
357
- const indexPath = path.join(ROOT_PATH, ".codebase-index.json");
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 "search_codebase": {
371
- const { query, limit, filters } = args;
372
- if (indexState.status === "indexing") {
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: "text",
603
+ type: 'text',
377
604
  text: JSON.stringify({
378
- status: "indexing",
379
- message: "Index is still being built. Retry in a moment.",
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 === "error") {
613
+ if (indexState.status === 'error') {
387
614
  return {
388
615
  content: [
389
616
  {
390
- type: "text",
617
+ type: 'text',
391
618
  text: JSON.stringify({
392
- status: "error",
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
- const results = await searcher.search(query, limit || 5, filters);
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: "text",
955
+ type: 'text',
405
956
  text: JSON.stringify({
406
- status: "success",
407
- results: results.map((r) => ({
408
- summary: r.summary,
409
- snippet: r.snippet,
410
- filePath: `${r.filePath}:${r.startLine}-${r.endLine}`,
411
- score: r.score,
412
- relevanceReason: r.relevanceReason,
413
- componentType: r.componentType,
414
- layer: r.layer,
415
- framework: r.framework,
416
- // v1.2: Pattern momentum awareness
417
- trend: r.trend,
418
- patternWarning: r.patternWarning,
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
- }, null, 2),
422
- },
423
- ],
977
+ ...(relatedMemories.length > 0 && {
978
+ relatedMemories: relatedMemories.slice(0, 5)
979
+ })
980
+ }, null, 2)
981
+ }
982
+ ]
424
983
  };
425
984
  }
426
- case "get_indexing_status": {
985
+ case 'get_indexing_status': {
427
986
  const progress = indexState.indexer?.getProgress();
428
987
  return {
429
988
  content: [
430
989
  {
431
- type: "text",
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: "Use refresh_index to manually trigger re-indexing when needed.",
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 "refresh_index": {
1019
+ case 'refresh_index': {
460
1020
  const { reason, incrementalOnly } = args;
461
- const mode = incrementalOnly ? "incremental" : "full";
462
- console.error(`Refresh requested (${mode}): ${reason || "Manual trigger"}`);
463
- // TODO: When incremental indexing is implemented (Phase 2),
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: "text",
1027
+ type: 'text',
471
1028
  text: JSON.stringify({
472
- status: "started",
1029
+ status: 'started',
473
1030
  mode,
474
1031
  message: incrementalOnly
475
- ? "Incremental re-indexing requested. Check status with get_indexing_status."
476
- : "Full re-indexing started. Check status with get_indexing_status.",
477
- reason,
478
- note: incrementalOnly
479
- ? "Incremental mode requested. Full re-index for now; true incremental indexing coming in Phase 2."
480
- : undefined,
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 "get_codebase_metadata": {
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 = path.join(ROOT_PATH, ".codebase-intelligence.json");
493
- const intelligenceContent = await fs.readFile(intelligencePath, "utf-8");
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 (error) {
1057
+ catch (_error) {
504
1058
  // No intelligence file or parsing error
505
1059
  }
506
1060
  return {
507
1061
  content: [
508
1062
  {
509
- type: "text",
1063
+ type: 'text',
510
1064
  text: JSON.stringify({
511
- status: "success",
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 "get_style_guide": {
1081
+ case 'get_style_guide': {
528
1082
  const { query, category } = args;
529
1083
  const styleGuidePatterns = [
530
- "STYLE_GUIDE.md",
531
- "CODING_STYLE.md",
532
- "ARCHITECTURE.md",
533
- "CONTRIBUTING.md",
534
- "docs/style-guide.md",
535
- "docs/coding-style.md",
536
- "docs/ARCHITECTURE.md",
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, "utf-8");
551
- const content = rawContent.replace(/\r\n/g, "\n");
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), // Max 3 sections per file
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 (e) {
1128
+ catch (_e) {
578
1129
  // Skip unreadable files
579
1130
  }
580
1131
  }
581
1132
  }
582
- catch (e) {
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: "text",
1141
+ type: 'text',
591
1142
  text: JSON.stringify({
592
- status: "no_results",
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: "text",
1155
+ type: 'text',
605
1156
  text: JSON.stringify({
606
- status: "success",
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 "get_team_patterns": {
1167
+ case 'get_team_patterns': {
617
1168
  const { category } = args;
618
1169
  try {
619
- const intelligencePath = path.join(ROOT_PATH, ".codebase-intelligence.json");
620
- const content = await fs.readFile(intelligencePath, "utf-8");
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: "success" };
623
- if (category === "all" || !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 === "di") {
1181
+ else if (category === 'di') {
631
1182
  result.dependencyInjection = intelligence.patterns?.dependencyInjection;
632
1183
  }
633
- else if (category === "state") {
1184
+ else if (category === 'state') {
634
1185
  result.stateManagement = intelligence.patterns?.stateManagement;
635
1186
  }
636
- else if (category === "testing") {
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 === "libraries") {
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: "text", text: JSON.stringify(result, null, 2) }],
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: "text",
1268
+ type: 'text',
655
1269
  text: JSON.stringify({
656
- status: "error",
657
- message: "Failed to load team patterns",
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 "get_component_usage": {
1279
+ case 'get_component_usage': {
666
1280
  const { name: componentName } = args;
667
1281
  try {
668
- const intelligencePath = path.join(ROOT_PATH, ".codebase-intelligence.json");
669
- const content = await fs.readFile(intelligencePath, "utf-8");
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
- type: "text",
1298
+ content: [
1299
+ {
1300
+ type: 'text',
686
1301
  text: JSON.stringify({
687
- status: "success",
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
- type: "text",
1315
+ content: [
1316
+ {
1317
+ type: 'text',
701
1318
  text: JSON.stringify({
702
- status: "not_found",
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
- type: "text",
1331
+ content: [
1332
+ {
1333
+ type: 'text',
715
1334
  text: JSON.stringify({
716
- status: "error",
717
- message: "Failed to get component usage. Run indexing first.",
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 "detect_circular_dependencies": {
1344
+ case 'detect_circular_dependencies': {
725
1345
  const { scope } = args;
726
1346
  try {
727
- const intelligencePath = path.join(ROOT_PATH, ".codebase-intelligence.json");
728
- const content = await fs.readFile(intelligencePath, "utf-8");
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
- type: "text",
1352
+ content: [
1353
+ {
1354
+ type: 'text',
734
1355
  text: JSON.stringify({
735
- status: "error",
736
- message: "Internal file graph not found. Please run refresh_index to rebuild the index with cycle detection support.",
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
- type: "text",
1369
+ content: [
1370
+ {
1371
+ type: 'text',
749
1372
  text: JSON.stringify({
750
- status: "success",
1373
+ status: 'success',
751
1374
  message: scope
752
1375
  ? `No circular dependencies detected in scope: ${scope}`
753
- : "No circular dependencies detected in the codebase.",
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
- type: "text",
1385
+ content: [
1386
+ {
1387
+ type: 'text',
763
1388
  text: JSON.stringify({
764
- status: "warning",
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 ? "high" : c.length <= 3 ? "medium" : "low",
1395
+ severity: c.length === 2 ? 'high' : c.length <= 3 ? 'medium' : 'low'
771
1396
  })),
772
1397
  count: cycles.length,
773
1398
  graphStats,
774
- advice: "Shorter cycles (length 2-3) are typically more problematic. Consider breaking the cycle by extracting shared dependencies.",
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
- type: "text",
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: "error",
785
- message: "Failed to detect circular dependencies. Run indexing first.",
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: "text",
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: "text",
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 (error) {
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, "package.json"));
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("[DEBUG] Starting indexing...");
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("[DEBUG] Index found. Ready.");
864
- indexState.status = "ready";
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("[DEBUG] Server ready");
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, "/").endsWith("index.js") ||
877
- process.argv[1]?.replace(/\\/g, "/").endsWith("index.ts");
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("Fatal:", error);
1643
+ console.error('Fatal:', error);
881
1644
  process.exit(1);
882
1645
  });
883
1646
  }