codebase-context 1.6.2 → 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 (137) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +358 -282
  3. package/dist/analyzers/angular/index.d.ts.map +1 -1
  4. package/dist/analyzers/angular/index.js +7 -2
  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 +2 -0
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +240 -1
  13. package/dist/cli.js.map +1 -1
  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.map +1 -1
  23. package/dist/core/indexer.js +253 -14
  24. package/dist/core/indexer.js.map +1 -1
  25. package/dist/core/search.d.ts +1 -0
  26. package/dist/core/search.d.ts.map +1 -1
  27. package/dist/core/search.js +60 -5
  28. package/dist/core/search.js.map +1 -1
  29. package/dist/core/symbol-references.d.ts +21 -0
  30. package/dist/core/symbol-references.d.ts.map +1 -0
  31. package/dist/core/symbol-references.js +91 -0
  32. package/dist/core/symbol-references.js.map +1 -0
  33. package/dist/eval/harness.d.ts +5 -0
  34. package/dist/eval/harness.d.ts.map +1 -0
  35. package/dist/eval/harness.js +153 -0
  36. package/dist/eval/harness.js.map +1 -0
  37. package/dist/eval/types.d.ts +59 -0
  38. package/dist/eval/types.d.ts.map +1 -0
  39. package/dist/eval/types.js +2 -0
  40. package/dist/eval/types.js.map +1 -0
  41. package/dist/grammars/manifest.d.ts +26 -0
  42. package/dist/grammars/manifest.d.ts.map +1 -0
  43. package/dist/grammars/manifest.js +63 -0
  44. package/dist/grammars/manifest.js.map +1 -0
  45. package/dist/index.d.ts +12 -2
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +136 -1298
  48. package/dist/index.js.map +1 -1
  49. package/dist/preflight/evidence-lock.d.ts +6 -0
  50. package/dist/preflight/evidence-lock.d.ts.map +1 -1
  51. package/dist/preflight/evidence-lock.js +33 -1
  52. package/dist/preflight/evidence-lock.js.map +1 -1
  53. package/dist/storage/lancedb.d.ts +9 -1
  54. package/dist/storage/lancedb.d.ts.map +1 -1
  55. package/dist/storage/lancedb.js +26 -8
  56. package/dist/storage/lancedb.js.map +1 -1
  57. package/dist/tools/detect-circular-dependencies.d.ts +5 -0
  58. package/dist/tools/detect-circular-dependencies.d.ts.map +1 -0
  59. package/dist/tools/detect-circular-dependencies.js +117 -0
  60. package/dist/tools/detect-circular-dependencies.js.map +1 -0
  61. package/dist/tools/get-codebase-metadata.d.ts +5 -0
  62. package/dist/tools/get-codebase-metadata.d.ts.map +1 -0
  63. package/dist/tools/get-codebase-metadata.js +53 -0
  64. package/dist/tools/get-codebase-metadata.js.map +1 -0
  65. package/dist/tools/get-component-usage.d.ts +5 -0
  66. package/dist/tools/get-component-usage.d.ts.map +1 -0
  67. package/dist/tools/get-component-usage.js +83 -0
  68. package/dist/tools/get-component-usage.js.map +1 -0
  69. package/dist/tools/get-indexing-status.d.ts +5 -0
  70. package/dist/tools/get-indexing-status.d.ts.map +1 -0
  71. package/dist/tools/get-indexing-status.js +44 -0
  72. package/dist/tools/get-indexing-status.js.map +1 -0
  73. package/dist/tools/get-memory.d.ts +5 -0
  74. package/dist/tools/get-memory.d.ts.map +1 -0
  75. package/dist/tools/get-memory.js +89 -0
  76. package/dist/tools/get-memory.js.map +1 -0
  77. package/dist/tools/get-style-guide.d.ts +5 -0
  78. package/dist/tools/get-style-guide.d.ts.map +1 -0
  79. package/dist/tools/get-style-guide.js +151 -0
  80. package/dist/tools/get-style-guide.js.map +1 -0
  81. package/dist/tools/get-symbol-references.d.ts +5 -0
  82. package/dist/tools/get-symbol-references.d.ts.map +1 -0
  83. package/dist/tools/get-symbol-references.js +70 -0
  84. package/dist/tools/get-symbol-references.js.map +1 -0
  85. package/dist/tools/get-team-patterns.d.ts +5 -0
  86. package/dist/tools/get-team-patterns.d.ts.map +1 -0
  87. package/dist/tools/get-team-patterns.js +131 -0
  88. package/dist/tools/get-team-patterns.js.map +1 -0
  89. package/dist/tools/index.d.ts +6 -0
  90. package/dist/tools/index.d.ts.map +1 -0
  91. package/dist/tools/index.js +41 -0
  92. package/dist/tools/index.js.map +1 -0
  93. package/dist/tools/refresh-index.d.ts +5 -0
  94. package/dist/tools/refresh-index.d.ts.map +1 -0
  95. package/dist/tools/refresh-index.js +40 -0
  96. package/dist/tools/refresh-index.js.map +1 -0
  97. package/dist/tools/remember.d.ts +5 -0
  98. package/dist/tools/remember.d.ts.map +1 -0
  99. package/dist/tools/remember.js +101 -0
  100. package/dist/tools/remember.js.map +1 -0
  101. package/dist/tools/search-codebase.d.ts +5 -0
  102. package/dist/tools/search-codebase.d.ts.map +1 -0
  103. package/dist/tools/search-codebase.js +638 -0
  104. package/dist/tools/search-codebase.js.map +1 -0
  105. package/dist/tools/types.d.ts +31 -0
  106. package/dist/tools/types.d.ts.map +1 -0
  107. package/dist/tools/types.js +2 -0
  108. package/dist/tools/types.js.map +1 -0
  109. package/dist/types/index.d.ts +6 -0
  110. package/dist/types/index.d.ts.map +1 -1
  111. package/dist/utils/ast-chunker.d.ts +71 -0
  112. package/dist/utils/ast-chunker.d.ts.map +1 -0
  113. package/dist/utils/ast-chunker.js +453 -0
  114. package/dist/utils/ast-chunker.js.map +1 -0
  115. package/dist/utils/chunking.d.ts.map +1 -1
  116. package/dist/utils/chunking.js +10 -3
  117. package/dist/utils/chunking.js.map +1 -1
  118. package/dist/utils/language-detection.d.ts.map +1 -1
  119. package/dist/utils/language-detection.js +20 -0
  120. package/dist/utils/language-detection.js.map +1 -1
  121. package/dist/utils/tree-sitter.d.ts +17 -0
  122. package/dist/utils/tree-sitter.d.ts.map +1 -0
  123. package/dist/utils/tree-sitter.js +311 -0
  124. package/dist/utils/tree-sitter.js.map +1 -0
  125. package/docs/capabilities.md +169 -92
  126. package/grammars/.gitkeep +0 -0
  127. package/grammars/tree-sitter-c.wasm +0 -0
  128. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  129. package/grammars/tree-sitter-cpp.wasm +0 -0
  130. package/grammars/tree-sitter-go.wasm +0 -0
  131. package/grammars/tree-sitter-java.wasm +0 -0
  132. package/grammars/tree-sitter-javascript.wasm +0 -0
  133. package/grammars/tree-sitter-python.wasm +0 -0
  134. package/grammars/tree-sitter-rust.wasm +0 -0
  135. package/grammars/tree-sitter-tsx.wasm +0 -0
  136. package/grammars/tree-sitter-typescript.wasm +0 -0
  137. package/package.json +151 -157
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
16
  import { IndexCorruptedError } from './errors/index.js';
20
17
  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';
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,228 +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. 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
193
  server.setRequestHandler(ListToolsRequestSchema, async () => {
355
194
  return { tools: TOOLS };
356
195
  });
@@ -369,12 +208,27 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
369
208
  });
370
209
  async function generateCodebaseContext() {
371
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
+ }
372
224
  try {
373
225
  const content = await fs.readFile(intelligencePath, 'utf-8');
374
226
  const intelligence = JSON.parse(content);
375
227
  const lines = [];
376
228
  lines.push('# Codebase Intelligence');
377
229
  lines.push('');
230
+ lines.push(`Index: ${index.status} (${index.confidence}, ${index.action})${index.reason ? ` — ${index.reason}` : ''}`);
231
+ lines.push('');
378
232
  lines.push('WARNING: This is what YOUR codebase actually uses, not generic recommendations.');
379
233
  lines.push('These are FACTS from analyzing your code, not best practices from the internet.');
380
234
  lines.push('');
@@ -590,1106 +444,79 @@ async function shouldReindex() {
590
444
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
591
445
  const { name, arguments: args } = request.params;
592
446
  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
- }
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') {
1018
451
  return {
1019
452
  content: [
1020
453
  {
1021
454
  type: 'text',
1022
455
  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)
456
+ status: 'indexing',
457
+ message: 'Index build in progress — please retry shortly'
458
+ })
1065
459
  }
1066
460
  ]
1067
461
  };
1068
462
  }
1069
- case 'get_indexing_status': {
1070
- const progress = indexState.indexer?.getProgress();
463
+ if (indexState.status === 'error') {
1071
464
  return {
1072
465
  content: [
1073
466
  {
1074
467
  type: 'text',
1075
468
  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)
469
+ status: 'error',
470
+ message: `Indexer error: ${indexState.error}`
471
+ })
1099
472
  }
1100
473
  ]
1101
474
  };
1102
475
  }
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);
476
+ indexSignal = await ensureValidIndexOrAutoHeal();
477
+ if (indexSignal.action === 'rebuild-failed') {
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
+ error: 'Index is corrupt and could not be rebuilt automatically.',
484
+ index: indexSignal
485
+ })
1120
486
  }
1121
- ]
487
+ ],
488
+ isError: true
1122
489
  };
1123
490
  }
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
- }
1144
- return {
1145
- content: [
1146
- {
1147
- type: 'text',
1148
- 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)
1161
- }
1162
- ]
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 })
1163
506
  };
1164
507
  }
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
- ]
1293
- };
508
+ catch {
509
+ /* response wasn't JSON, skip injection */
1294
510
  }
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
- }
1547
- }
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
511
  }
512
+ return result;
1683
513
  }
1684
514
  catch (error) {
1685
515
  return {
1686
516
  content: [
1687
517
  {
1688
518
  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)
519
+ text: `Unexpected error: ${error instanceof Error ? error.message : String(error)}`
1693
520
  }
1694
521
  ],
1695
522
  isError: true
@@ -1766,10 +593,21 @@ export { server, performIndexing, resolveRootPath, shouldReindex, TOOLS };
1766
593
  // Check if this module is the entry point
1767
594
  const isDirectRun = process.argv[1]?.replace(/\\/g, '/').endsWith('index.js') ||
1768
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
+ ];
1769
607
  if (isDirectRun) {
1770
- // CLI subcommand: memory list/add/remove
1771
- if (process.argv[2] === 'memory') {
1772
- handleMemoryCli(process.argv.slice(3)).catch((error) => {
608
+ const subcommand = process.argv[2];
609
+ if (CLI_SUBCOMMANDS.includes(subcommand) || subcommand === '--help') {
610
+ handleCliCommand(process.argv.slice(2)).catch((error) => {
1773
611
  console.error('Error:', error instanceof Error ? error.message : String(error));
1774
612
  process.exit(1);
1775
613
  });