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