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