codebase-context 1.6.2 → 1.8.0

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