codebase-context 1.8.2 → 1.9.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/README.md +215 -13
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +12 -4
- package/dist/cli.js.map +1 -1
- package/dist/constants/codebase-context.d.ts +12 -0
- package/dist/constants/codebase-context.d.ts.map +1 -1
- package/dist/constants/codebase-context.js +36 -0
- package/dist/constants/codebase-context.js.map +1 -1
- package/dist/core/file-watcher.d.ts.map +1 -1
- package/dist/core/file-watcher.js +2 -12
- package/dist/core/file-watcher.js.map +1 -1
- package/dist/core/indexer.d.ts.map +1 -1
- package/dist/core/indexer.js +2 -9
- package/dist/core/indexer.js.map +1 -1
- package/dist/index.d.ts +10 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +884 -192
- package/dist/index.js.map +1 -1
- package/dist/project-state.d.ts +24 -0
- package/dist/project-state.d.ts.map +1 -0
- package/dist/project-state.js +68 -0
- package/dist/project-state.js.map +1 -0
- package/dist/resources/uri.d.ts +4 -1
- package/dist/resources/uri.d.ts.map +1 -1
- package/dist/resources/uri.js +18 -1
- package/dist/resources/uri.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +31 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/search-codebase.d.ts.map +1 -1
- package/dist/tools/search-codebase.js +14 -41
- package/dist/tools/search-codebase.js.map +1 -1
- package/dist/tools/types.d.ts +12 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/utils/project-discovery.d.ts +12 -0
- package/dist/utils/project-discovery.d.ts.map +1 -0
- package/dist/utils/project-discovery.js +183 -0
- package/dist/utils/project-discovery.js.map +1 -0
- package/docs/capabilities.md +37 -7
- package/docs/cli.md +3 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,54 +5,374 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { promises as fs } from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
8
9
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
9
10
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, RootsListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
11
12
|
import { CodebaseIndexer } from './core/indexer.js';
|
|
12
13
|
import { analyzerRegistry } from './core/analyzer-registry.js';
|
|
13
14
|
import { AngularAnalyzer } from './analyzers/angular/index.js';
|
|
14
15
|
import { GenericAnalyzer } from './analyzers/generic/index.js';
|
|
15
16
|
import { IndexCorruptedError } from './errors/index.js';
|
|
16
|
-
import { CODEBASE_CONTEXT_DIRNAME, MEMORY_FILENAME, INTELLIGENCE_FILENAME, KEYWORD_INDEX_FILENAME, VECTOR_DB_DIRNAME } from './constants/codebase-context.js';
|
|
17
17
|
import { appendMemoryFile } from './memory/store.js';
|
|
18
18
|
import { handleCliCommand } from './cli.js';
|
|
19
19
|
import { startFileWatcher } from './core/file-watcher.js';
|
|
20
|
-
import { createAutoRefreshController } from './core/auto-refresh.js';
|
|
21
20
|
import { parseGitLogLineToMemory } from './memory/git-memory.js';
|
|
22
21
|
import { isComplementaryPatternCategory, shouldSkipLegacyTestingFrameworkCategory } from './patterns/semantics.js';
|
|
23
|
-
import { CONTEXT_RESOURCE_URI, isContextResourceUri } from './resources/uri.js';
|
|
22
|
+
import { CONTEXT_RESOURCE_URI, buildProjectContextResourceUri, getProjectPathFromContextResourceUri, isContextResourceUri } from './resources/uri.js';
|
|
23
|
+
import { discoverProjectsWithinRoot, findNearestProjectBoundary, isPathWithin } from './utils/project-discovery.js';
|
|
24
24
|
import { readIndexMeta, validateIndexArtifacts } from './core/index-meta.js';
|
|
25
25
|
import { TOOLS, dispatchTool } from './tools/index.js';
|
|
26
|
+
import { getOrCreateProject, getAllProjects, getProject, makePaths, makeLegacyPaths, normalizeRootKey, removeProject } from './project-state.js';
|
|
26
27
|
analyzerRegistry.register(new AngularAnalyzer());
|
|
27
28
|
analyzerRegistry.register(new GenericAnalyzer());
|
|
28
|
-
// Resolve root
|
|
29
|
+
// Resolve optional bootstrap root with validation handled later in main().
|
|
29
30
|
function resolveRootPath() {
|
|
30
31
|
const arg = process.argv[2];
|
|
31
32
|
const envPath = process.env.CODEBASE_ROOT;
|
|
32
|
-
// Priority: CLI arg > env var
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
33
|
+
// Priority: CLI arg > env var. Do not fall back to cwd in MCP mode.
|
|
34
|
+
const configuredRoot = arg || envPath;
|
|
35
|
+
if (!configuredRoot) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
return path.resolve(configuredRoot);
|
|
39
|
+
}
|
|
40
|
+
const primaryRootPath = resolveRootPath();
|
|
41
|
+
const toolNames = new Set(TOOLS.map((tool) => tool.name));
|
|
42
|
+
const knownRoots = new Map();
|
|
43
|
+
const discoveredProjectPaths = new Map();
|
|
44
|
+
let clientRootsEnabled = false;
|
|
45
|
+
const projectSourcesByKey = new Map();
|
|
46
|
+
const projectAccessOrder = new Map();
|
|
47
|
+
let activeProjectKey;
|
|
48
|
+
let nextProjectAccessOrder = 1;
|
|
49
|
+
const MAX_WATCHED_PROJECTS = 5;
|
|
50
|
+
const PROJECT_DISCOVERY_MAX_DEPTH = 4;
|
|
51
|
+
const debounceEnv = Number.parseInt(process.env.CODEBASE_CONTEXT_DEBOUNCE_MS ?? '', 10);
|
|
52
|
+
const watcherDebounceMs = Number.isFinite(debounceEnv) && debounceEnv >= 0 ? debounceEnv : 2000;
|
|
53
|
+
function registerKnownRoot(rootPath) {
|
|
54
|
+
const resolvedRootPath = path.resolve(rootPath);
|
|
55
|
+
knownRoots.set(normalizeRootKey(resolvedRootPath), { rootPath: resolvedRootPath });
|
|
56
|
+
rememberProjectPath(resolvedRootPath, 'root');
|
|
57
|
+
return resolvedRootPath;
|
|
58
|
+
}
|
|
59
|
+
function getKnownRootPaths() {
|
|
60
|
+
return Array.from(knownRoots.values())
|
|
61
|
+
.map((entry) => entry.rootPath)
|
|
62
|
+
.sort((a, b) => a.localeCompare(b));
|
|
63
|
+
}
|
|
64
|
+
function getKnownRootLabel(rootPath) {
|
|
65
|
+
return knownRoots.get(normalizeRootKey(rootPath))?.label;
|
|
66
|
+
}
|
|
67
|
+
function getContainingKnownRoot(rootPath) {
|
|
68
|
+
const orderedRoots = getKnownRootPaths().sort((a, b) => b.length - a.length);
|
|
69
|
+
return orderedRoots.find((knownRootPath) => isPathWithin(knownRootPath, rootPath));
|
|
70
|
+
}
|
|
71
|
+
function classifyProjectSource(rootPath) {
|
|
72
|
+
const rootKey = normalizeRootKey(rootPath);
|
|
73
|
+
if (knownRoots.has(rootKey)) {
|
|
74
|
+
return 'root';
|
|
75
|
+
}
|
|
76
|
+
return getContainingKnownRoot(rootPath) ? 'subdirectory' : 'ad_hoc';
|
|
77
|
+
}
|
|
78
|
+
function touchProject(rootPath) {
|
|
79
|
+
projectAccessOrder.set(normalizeRootKey(rootPath), nextProjectAccessOrder++);
|
|
80
|
+
}
|
|
81
|
+
function rememberProjectPath(rootPath, source = classifyProjectSource(rootPath), options = {}) {
|
|
82
|
+
const resolvedRootPath = path.resolve(rootPath);
|
|
83
|
+
const rootKey = normalizeRootKey(resolvedRootPath);
|
|
84
|
+
const existingSource = projectSourcesByKey.get(rootKey);
|
|
85
|
+
if (!existingSource ||
|
|
86
|
+
source === 'root' ||
|
|
87
|
+
(source === 'subdirectory' && existingSource === 'ad_hoc')) {
|
|
88
|
+
projectSourcesByKey.set(rootKey, source);
|
|
89
|
+
}
|
|
90
|
+
if (options.touch !== false) {
|
|
91
|
+
touchProject(resolvedRootPath);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function registerDiscoveredProjectPath(rootPath, source = 'subdirectory') {
|
|
95
|
+
const resolvedRootPath = path.resolve(rootPath);
|
|
96
|
+
discoveredProjectPaths.set(normalizeRootKey(resolvedRootPath), resolvedRootPath);
|
|
97
|
+
rememberProjectPath(resolvedRootPath, source, { touch: false });
|
|
98
|
+
}
|
|
99
|
+
function clearDiscoveredProjectPaths() {
|
|
100
|
+
discoveredProjectPaths.clear();
|
|
101
|
+
}
|
|
102
|
+
function getTrackedRootPathByKey(rootKey) {
|
|
103
|
+
if (knownRoots.has(rootKey)) {
|
|
104
|
+
return knownRoots.get(rootKey)?.rootPath;
|
|
105
|
+
}
|
|
106
|
+
const project = Array.from(getAllProjects()).find((entry) => normalizeRootKey(entry.rootPath) === rootKey);
|
|
107
|
+
return project?.rootPath;
|
|
108
|
+
}
|
|
109
|
+
function forgetProjectPath(rootPath) {
|
|
110
|
+
const rootKey = normalizeRootKey(rootPath);
|
|
111
|
+
projectSourcesByKey.delete(rootKey);
|
|
112
|
+
projectAccessOrder.delete(rootKey);
|
|
113
|
+
if (activeProjectKey === rootKey) {
|
|
114
|
+
activeProjectKey = undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function formatProjectLabel(rootPath) {
|
|
118
|
+
const knownRootLabel = getKnownRootLabel(rootPath);
|
|
119
|
+
if (knownRootLabel) {
|
|
120
|
+
return knownRootLabel;
|
|
121
|
+
}
|
|
122
|
+
const containingRoot = getContainingKnownRoot(rootPath);
|
|
123
|
+
if (containingRoot) {
|
|
124
|
+
const relativePath = path.relative(containingRoot, rootPath);
|
|
125
|
+
if (!relativePath) {
|
|
126
|
+
return getKnownRootLabel(containingRoot) ?? (path.basename(rootPath) || rootPath);
|
|
127
|
+
}
|
|
128
|
+
return relativePath.replace(/\\/g, '/');
|
|
129
|
+
}
|
|
130
|
+
return path.basename(rootPath) || rootPath;
|
|
131
|
+
}
|
|
132
|
+
function getRelativeProjectPath(rootPath) {
|
|
133
|
+
const containingRoot = getContainingKnownRoot(rootPath);
|
|
134
|
+
if (!containingRoot)
|
|
135
|
+
return undefined;
|
|
136
|
+
const relativePath = path.relative(containingRoot, rootPath).replace(/\\/g, '/');
|
|
137
|
+
return relativePath || undefined;
|
|
138
|
+
}
|
|
139
|
+
function getProjectIndexStatus(rootPath) {
|
|
140
|
+
return getProject(rootPath)?.indexState.status ?? 'idle';
|
|
141
|
+
}
|
|
142
|
+
function buildProjectDescriptor(rootPath) {
|
|
143
|
+
const resolvedRootPath = path.resolve(rootPath);
|
|
144
|
+
const rootKey = normalizeRootKey(resolvedRootPath);
|
|
145
|
+
rememberProjectPath(resolvedRootPath, classifyProjectSource(resolvedRootPath), { touch: false });
|
|
146
|
+
return {
|
|
147
|
+
project: resolvedRootPath,
|
|
148
|
+
label: formatProjectLabel(resolvedRootPath),
|
|
149
|
+
rootPath: resolvedRootPath,
|
|
150
|
+
relativePath: getRelativeProjectPath(resolvedRootPath),
|
|
151
|
+
active: activeProjectKey === rootKey,
|
|
152
|
+
source: projectSourcesByKey.get(rootKey) ?? classifyProjectSource(resolvedRootPath),
|
|
153
|
+
indexStatus: getProjectIndexStatus(resolvedRootPath)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function listProjectDescriptors() {
|
|
157
|
+
const rootPaths = new Map();
|
|
158
|
+
for (const rootPath of getKnownRootPaths()) {
|
|
159
|
+
rootPaths.set(normalizeRootKey(rootPath), rootPath);
|
|
160
|
+
}
|
|
161
|
+
for (const [projectKey, rootPath] of discoveredProjectPaths.entries()) {
|
|
162
|
+
rootPaths.set(projectKey, rootPath);
|
|
163
|
+
}
|
|
164
|
+
for (const project of getAllProjects()) {
|
|
165
|
+
rootPaths.set(normalizeRootKey(project.rootPath), project.rootPath);
|
|
166
|
+
}
|
|
167
|
+
const descriptors = Array.from(rootPaths.values())
|
|
168
|
+
.map((rootPath) => buildProjectDescriptor(rootPath))
|
|
169
|
+
.sort((a, b) => {
|
|
170
|
+
if (a.active !== b.active)
|
|
171
|
+
return a.active ? -1 : 1;
|
|
172
|
+
if (a.source !== b.source) {
|
|
173
|
+
const weight = {
|
|
174
|
+
root: 0,
|
|
175
|
+
subdirectory: 1,
|
|
176
|
+
ad_hoc: 2
|
|
177
|
+
};
|
|
178
|
+
return weight[a.source] - weight[b.source];
|
|
179
|
+
}
|
|
180
|
+
return a.label.localeCompare(b.label);
|
|
181
|
+
});
|
|
182
|
+
const duplicates = new Set();
|
|
183
|
+
const counts = new Map();
|
|
184
|
+
for (const descriptor of descriptors) {
|
|
185
|
+
counts.set(descriptor.label, (counts.get(descriptor.label) ?? 0) + 1);
|
|
186
|
+
}
|
|
187
|
+
for (const [label, count] of counts.entries()) {
|
|
188
|
+
if (count > 1) {
|
|
189
|
+
duplicates.add(label);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return descriptors.map((descriptor) => {
|
|
193
|
+
if (!duplicates.has(descriptor.label)) {
|
|
194
|
+
return descriptor;
|
|
195
|
+
}
|
|
196
|
+
const containingRoot = getContainingKnownRoot(descriptor.rootPath);
|
|
197
|
+
const rootHint = (containingRoot && getKnownRootLabel(containingRoot)) ||
|
|
198
|
+
(containingRoot && path.basename(containingRoot)) ||
|
|
199
|
+
path.basename(descriptor.rootPath);
|
|
200
|
+
return {
|
|
201
|
+
...descriptor,
|
|
202
|
+
label: `${descriptor.label} (${rootHint})`
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function getActiveProjectDescriptor() {
|
|
207
|
+
if (!activeProjectKey)
|
|
208
|
+
return undefined;
|
|
209
|
+
const trackedRootPath = getTrackedRootPathByKey(activeProjectKey);
|
|
210
|
+
if (!trackedRootPath) {
|
|
211
|
+
activeProjectKey = undefined;
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
return buildProjectDescriptor(trackedRootPath);
|
|
215
|
+
}
|
|
216
|
+
function setActiveProject(rootPath) {
|
|
217
|
+
const resolvedRootPath = path.resolve(rootPath);
|
|
218
|
+
activeProjectKey = normalizeRootKey(resolvedRootPath);
|
|
219
|
+
rememberProjectPath(resolvedRootPath);
|
|
220
|
+
}
|
|
221
|
+
function syncKnownRoots(rootEntries) {
|
|
222
|
+
const nextRoots = new Map();
|
|
223
|
+
const normalizedRoots = rootEntries.length > 0 ? rootEntries : primaryRootPath ? [{ rootPath: primaryRootPath }] : [];
|
|
224
|
+
for (const entry of normalizedRoots) {
|
|
225
|
+
const resolvedRootPath = path.resolve(entry.rootPath);
|
|
226
|
+
nextRoots.set(normalizeRootKey(resolvedRootPath), {
|
|
227
|
+
rootPath: resolvedRootPath,
|
|
228
|
+
label: entry.label?.trim() || undefined
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
for (const [rootKey, existingRoot] of knownRoots.entries()) {
|
|
232
|
+
if (!nextRoots.has(rootKey)) {
|
|
233
|
+
removeProject(existingRoot.rootPath);
|
|
234
|
+
forgetProjectPath(existingRoot.rootPath);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
for (const project of getAllProjects()) {
|
|
238
|
+
const stillAllowed = Array.from(nextRoots.values()).some((knownRoot) => isPathWithin(knownRoot.rootPath, project.rootPath));
|
|
239
|
+
if (!stillAllowed) {
|
|
240
|
+
removeProject(project.rootPath);
|
|
241
|
+
forgetProjectPath(project.rootPath);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
knownRoots.clear();
|
|
245
|
+
clearDiscoveredProjectPaths();
|
|
246
|
+
for (const [rootKey, rootEntry] of nextRoots.entries()) {
|
|
247
|
+
knownRoots.set(rootKey, rootEntry);
|
|
248
|
+
rememberProjectPath(rootEntry.rootPath, 'root', { touch: false });
|
|
249
|
+
}
|
|
250
|
+
if (activeProjectKey) {
|
|
251
|
+
if (!getTrackedRootPathByKey(activeProjectKey)) {
|
|
252
|
+
activeProjectKey = undefined;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function parseProjectSelector(value) {
|
|
257
|
+
if (typeof value !== 'string')
|
|
258
|
+
return undefined;
|
|
259
|
+
const trimmedValue = value.trim();
|
|
260
|
+
if (!trimmedValue)
|
|
261
|
+
return undefined;
|
|
262
|
+
return trimmedValue;
|
|
263
|
+
}
|
|
264
|
+
function parseProjectDirectory(value) {
|
|
265
|
+
const selector = parseProjectSelector(value);
|
|
266
|
+
if (!selector)
|
|
267
|
+
return undefined;
|
|
268
|
+
return selector.startsWith('file://')
|
|
269
|
+
? path.resolve(fileURLToPath(selector))
|
|
270
|
+
: path.resolve(selector);
|
|
271
|
+
}
|
|
272
|
+
function getProjectSourceForResolvedPath(rootPath) {
|
|
273
|
+
return getContainingKnownRoot(rootPath) ? 'subdirectory' : 'ad_hoc';
|
|
274
|
+
}
|
|
275
|
+
async function resolveProjectFromAbsolutePath(resolvedPath) {
|
|
276
|
+
const absolutePath = path.resolve(resolvedPath);
|
|
277
|
+
const containingRoot = getContainingKnownRoot(absolutePath);
|
|
278
|
+
if (clientRootsEnabled && getKnownRootPaths().length > 0 && !containingRoot) {
|
|
279
|
+
return {
|
|
280
|
+
ok: false,
|
|
281
|
+
response: buildProjectSelectionError('unknown_project', 'Requested project is not under an active MCP root.')
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
let stats;
|
|
285
|
+
try {
|
|
286
|
+
stats = await fs.stat(absolutePath);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return {
|
|
290
|
+
ok: false,
|
|
291
|
+
response: buildProjectSelectionError('unknown_project', `project does not exist: ${absolutePath}`)
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
const lookupPath = stats.isDirectory() ? absolutePath : path.dirname(absolutePath);
|
|
295
|
+
const exactDescriptor = listProjectDescriptors().find((descriptor) => normalizeRootKey(descriptor.rootPath) === normalizeRootKey(lookupPath));
|
|
296
|
+
if (exactDescriptor) {
|
|
297
|
+
const project = getOrCreateProject(exactDescriptor.rootPath);
|
|
298
|
+
if (exactDescriptor.source === 'subdirectory') {
|
|
299
|
+
registerDiscoveredProjectPath(exactDescriptor.rootPath, 'subdirectory');
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
rememberProjectPath(exactDescriptor.rootPath, exactDescriptor.source, { touch: false });
|
|
303
|
+
}
|
|
304
|
+
return { ok: true, project };
|
|
305
|
+
}
|
|
306
|
+
const nearestBoundary = await findNearestProjectBoundary(absolutePath, containingRoot);
|
|
307
|
+
const resolvedProjectPath = nearestBoundary?.rootPath ?? containingRoot ?? (stats.isDirectory() ? absolutePath : undefined);
|
|
308
|
+
if (!resolvedProjectPath) {
|
|
309
|
+
return {
|
|
310
|
+
ok: false,
|
|
311
|
+
response: buildProjectSelectionError('unknown_project', `project was not found from path: ${absolutePath}`)
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const invalidProjectResponse = await validateResolvedProjectPath(resolvedProjectPath);
|
|
315
|
+
if (invalidProjectResponse) {
|
|
316
|
+
return { ok: false, response: invalidProjectResponse };
|
|
317
|
+
}
|
|
318
|
+
const projectSource = getProjectSourceForResolvedPath(resolvedProjectPath);
|
|
319
|
+
if (projectSource === 'subdirectory') {
|
|
320
|
+
registerDiscoveredProjectPath(resolvedProjectPath, 'subdirectory');
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
rememberProjectPath(resolvedProjectPath, projectSource, { touch: false });
|
|
324
|
+
}
|
|
325
|
+
const project = getOrCreateProject(resolvedProjectPath);
|
|
326
|
+
return { ok: true, project };
|
|
327
|
+
}
|
|
328
|
+
function buildProjectSelectionPayload(status, message, project, extras = {}) {
|
|
329
|
+
return {
|
|
330
|
+
status,
|
|
331
|
+
message,
|
|
332
|
+
activeProject: project
|
|
333
|
+
? buildProjectDescriptor(project.rootPath)
|
|
334
|
+
: (getActiveProjectDescriptor() ?? null),
|
|
335
|
+
availableProjects: listProjectDescriptors(),
|
|
336
|
+
...extras
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function buildProjectSelectionError(errorCode, message, extras = {}) {
|
|
340
|
+
const status = errorCode === 'selection_required' ? 'selection_required' : 'error';
|
|
341
|
+
return {
|
|
342
|
+
content: [
|
|
343
|
+
{
|
|
344
|
+
type: 'text',
|
|
345
|
+
text: JSON.stringify({ ...buildProjectSelectionPayload(status, message, undefined, extras), errorCode }, null, 2)
|
|
346
|
+
}
|
|
347
|
+
],
|
|
348
|
+
isError: true
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function createToolContext(project) {
|
|
352
|
+
return {
|
|
353
|
+
indexState: project.indexState,
|
|
354
|
+
paths: project.paths,
|
|
355
|
+
rootPath: project.rootPath,
|
|
356
|
+
project: buildProjectDescriptor(project.rootPath),
|
|
357
|
+
performIndexing: (incrementalOnly) => performIndexing(project, incrementalOnly),
|
|
358
|
+
listProjects: () => listProjectDescriptors(),
|
|
359
|
+
getActiveProject: () => getActiveProjectDescriptor()
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function createWorkspaceToolContext() {
|
|
363
|
+
const fallbackRootPath = primaryRootPath ?? path.resolve(process.cwd());
|
|
364
|
+
return {
|
|
365
|
+
indexState: { status: 'idle' },
|
|
366
|
+
paths: makePaths(fallbackRootPath),
|
|
367
|
+
rootPath: fallbackRootPath,
|
|
368
|
+
performIndexing: () => undefined,
|
|
369
|
+
listProjects: () => listProjectDescriptors(),
|
|
370
|
+
getActiveProject: () => getActiveProjectDescriptor()
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (primaryRootPath) {
|
|
374
|
+
registerKnownRoot(primaryRootPath);
|
|
375
|
+
}
|
|
56
376
|
export const INDEX_CONSUMING_TOOL_NAMES = [
|
|
57
377
|
'search_codebase',
|
|
58
378
|
'get_symbol_references',
|
|
@@ -61,11 +381,11 @@ export const INDEX_CONSUMING_TOOL_NAMES = [
|
|
|
61
381
|
'get_codebase_metadata'
|
|
62
382
|
];
|
|
63
383
|
export const INDEX_CONSUMING_RESOURCE_NAMES = ['Codebase Intelligence'];
|
|
64
|
-
async function requireValidIndex(rootPath) {
|
|
384
|
+
async function requireValidIndex(rootPath, paths) {
|
|
65
385
|
const meta = await readIndexMeta(rootPath);
|
|
66
386
|
await validateIndexArtifacts(rootPath, meta);
|
|
67
387
|
// Optional artifact presence informs confidence.
|
|
68
|
-
const hasIntelligence = await fileExists(
|
|
388
|
+
const hasIntelligence = await fileExists(paths.intelligence);
|
|
69
389
|
return {
|
|
70
390
|
status: 'ready',
|
|
71
391
|
confidence: hasIntelligence ? 'high' : 'low',
|
|
@@ -73,8 +393,8 @@ async function requireValidIndex(rootPath) {
|
|
|
73
393
|
...(hasIntelligence ? {} : { reason: 'Optional intelligence artifact missing' })
|
|
74
394
|
};
|
|
75
395
|
}
|
|
76
|
-
async function ensureValidIndexOrAutoHeal() {
|
|
77
|
-
if (indexState.status === 'indexing') {
|
|
396
|
+
async function ensureValidIndexOrAutoHeal(project) {
|
|
397
|
+
if (project.indexState.status === 'indexing') {
|
|
78
398
|
return {
|
|
79
399
|
status: 'indexing',
|
|
80
400
|
confidence: 'low',
|
|
@@ -83,40 +403,118 @@ async function ensureValidIndexOrAutoHeal() {
|
|
|
83
403
|
};
|
|
84
404
|
}
|
|
85
405
|
try {
|
|
86
|
-
return await requireValidIndex(
|
|
406
|
+
return await requireValidIndex(project.rootPath, project.paths);
|
|
87
407
|
}
|
|
88
408
|
catch (error) {
|
|
89
409
|
if (error instanceof IndexCorruptedError) {
|
|
90
410
|
const reason = error.message;
|
|
91
411
|
console.error(`[Index] ${reason}`);
|
|
92
|
-
console.error('[Auto-Heal] Triggering
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
}
|
|
412
|
+
console.error('[Auto-Heal] Triggering background re-index...');
|
|
413
|
+
// Fire-and-forget: don't block the tool call
|
|
414
|
+
void performIndexing(project);
|
|
110
415
|
return {
|
|
111
|
-
status: '
|
|
416
|
+
status: 'indexing',
|
|
112
417
|
confidence: 'low',
|
|
113
|
-
action: 'rebuild-
|
|
114
|
-
reason: `Auto-heal
|
|
418
|
+
action: 'rebuild-started',
|
|
419
|
+
reason: `Auto-heal triggered: ${reason}`
|
|
115
420
|
};
|
|
116
421
|
}
|
|
117
422
|
throw error;
|
|
118
423
|
}
|
|
119
424
|
}
|
|
425
|
+
async function validateResolvedProjectPath(rootPath) {
|
|
426
|
+
try {
|
|
427
|
+
const stats = await fs.stat(rootPath);
|
|
428
|
+
if (!stats.isDirectory()) {
|
|
429
|
+
return buildProjectSelectionError('unknown_project', `project is not a directory: ${rootPath}`);
|
|
430
|
+
}
|
|
431
|
+
if (clientRootsEnabled && getKnownRootPaths().length > 0 && !getContainingKnownRoot(rootPath)) {
|
|
432
|
+
return buildProjectSelectionError('unknown_project', 'Requested project is not under an active MCP root.');
|
|
433
|
+
}
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return buildProjectSelectionError('unknown_project', `project does not exist: ${rootPath}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async function resolveProjectSelector(selector) {
|
|
441
|
+
const trimmedSelector = selector.trim();
|
|
442
|
+
if (!trimmedSelector) {
|
|
443
|
+
return {
|
|
444
|
+
ok: false,
|
|
445
|
+
response: buildProjectSelectionError('unknown_project', 'project must be a non-empty absolute path, file:// URI, or relative subproject path.')
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
if (trimmedSelector.startsWith('file://') || path.isAbsolute(trimmedSelector)) {
|
|
449
|
+
const resolvedPath = parseProjectDirectory(trimmedSelector);
|
|
450
|
+
if (!resolvedPath) {
|
|
451
|
+
return {
|
|
452
|
+
ok: false,
|
|
453
|
+
response: buildProjectSelectionError('unknown_project', 'project must be a non-empty absolute path, file:// URI, or relative subproject path.')
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
return resolveProjectFromAbsolutePath(resolvedPath);
|
|
457
|
+
}
|
|
458
|
+
const normalizedSelector = trimmedSelector.replace(/\\/g, '/').replace(/^\.\/+/, '');
|
|
459
|
+
const descriptorMatches = listProjectDescriptors().filter((descriptor) => descriptor.label === normalizedSelector ||
|
|
460
|
+
descriptor.relativePath === normalizedSelector ||
|
|
461
|
+
path.basename(descriptor.rootPath) === normalizedSelector);
|
|
462
|
+
if (descriptorMatches.length === 1) {
|
|
463
|
+
const matchedRootPath = descriptorMatches[0].rootPath;
|
|
464
|
+
if (descriptorMatches[0].source === 'subdirectory') {
|
|
465
|
+
registerDiscoveredProjectPath(matchedRootPath, 'subdirectory');
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
rememberProjectPath(matchedRootPath, classifyProjectSource(matchedRootPath), {
|
|
469
|
+
touch: false
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
const project = getOrCreateProject(matchedRootPath);
|
|
473
|
+
return { ok: true, project };
|
|
474
|
+
}
|
|
475
|
+
if (descriptorMatches.length > 1) {
|
|
476
|
+
return {
|
|
477
|
+
ok: false,
|
|
478
|
+
response: buildProjectSelectionError('selection_required', `Project selector "${normalizedSelector}" matches multiple known projects. Retry with an absolute path.`, {
|
|
479
|
+
reason: 'project_selector_ambiguous',
|
|
480
|
+
nextAction: 'retry_with_project'
|
|
481
|
+
})
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
const matchingProjects = getKnownRootPaths()
|
|
485
|
+
.map((rootPath) => ({ rootPath, candidatePath: path.resolve(rootPath, normalizedSelector) }))
|
|
486
|
+
.filter(({ rootPath, candidatePath }) => isPathWithin(rootPath, candidatePath))
|
|
487
|
+
.map(({ candidatePath }) => candidatePath);
|
|
488
|
+
const resolvedMatches = new Map();
|
|
489
|
+
for (const candidatePath of matchingProjects) {
|
|
490
|
+
const resolution = await resolveProjectFromAbsolutePath(candidatePath);
|
|
491
|
+
if (resolution.ok) {
|
|
492
|
+
resolvedMatches.set(normalizeRootKey(resolution.project.rootPath), resolution.project);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const payload = JSON.parse(resolution.response.content?.[0]?.text ?? '{}');
|
|
496
|
+
if (payload.errorCode !== 'unknown_project') {
|
|
497
|
+
return resolution;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (resolvedMatches.size === 1) {
|
|
501
|
+
const project = Array.from(resolvedMatches.values())[0];
|
|
502
|
+
return { ok: true, project };
|
|
503
|
+
}
|
|
504
|
+
if (resolvedMatches.size > 1) {
|
|
505
|
+
return {
|
|
506
|
+
ok: false,
|
|
507
|
+
response: buildProjectSelectionError('selection_required', `Relative project path "${normalizedSelector}" matches multiple configured roots. Retry with an absolute path.`, {
|
|
508
|
+
reason: 'relative_project_ambiguous',
|
|
509
|
+
nextAction: 'retry_with_project'
|
|
510
|
+
})
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
ok: false,
|
|
515
|
+
response: buildProjectSelectionError('unknown_project', `Relative project path "${normalizedSelector}" was not found under any configured root.`)
|
|
516
|
+
};
|
|
517
|
+
}
|
|
120
518
|
/**
|
|
121
519
|
* Check if file/directory exists
|
|
122
520
|
*/
|
|
@@ -133,14 +531,14 @@ async function fileExists(filePath) {
|
|
|
133
531
|
* Migrate legacy file structure to .codebase-context/ folder.
|
|
134
532
|
* Idempotent, fail-safe. Rollback compatibility is not required.
|
|
135
533
|
*/
|
|
136
|
-
async function migrateToNewStructure() {
|
|
534
|
+
async function migrateToNewStructure(paths, legacyPaths) {
|
|
137
535
|
let migrated = false;
|
|
138
536
|
try {
|
|
139
|
-
await fs.mkdir(
|
|
537
|
+
await fs.mkdir(paths.baseDir, { recursive: true });
|
|
140
538
|
// intelligence.json
|
|
141
|
-
if (!(await fileExists(
|
|
142
|
-
if (await fileExists(
|
|
143
|
-
await fs.copyFile(
|
|
539
|
+
if (!(await fileExists(paths.intelligence))) {
|
|
540
|
+
if (await fileExists(legacyPaths.intelligence)) {
|
|
541
|
+
await fs.copyFile(legacyPaths.intelligence, paths.intelligence);
|
|
144
542
|
migrated = true;
|
|
145
543
|
if (process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
146
544
|
console.error('[DEBUG] Migrated intelligence.json');
|
|
@@ -148,9 +546,9 @@ async function migrateToNewStructure() {
|
|
|
148
546
|
}
|
|
149
547
|
}
|
|
150
548
|
// index.json (keyword index)
|
|
151
|
-
if (!(await fileExists(
|
|
152
|
-
if (await fileExists(
|
|
153
|
-
await fs.copyFile(
|
|
549
|
+
if (!(await fileExists(paths.keywordIndex))) {
|
|
550
|
+
if (await fileExists(legacyPaths.keywordIndex)) {
|
|
551
|
+
await fs.copyFile(legacyPaths.keywordIndex, paths.keywordIndex);
|
|
154
552
|
migrated = true;
|
|
155
553
|
if (process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
156
554
|
console.error('[DEBUG] Migrated index.json');
|
|
@@ -158,9 +556,9 @@ async function migrateToNewStructure() {
|
|
|
158
556
|
}
|
|
159
557
|
}
|
|
160
558
|
// Vector DB directory
|
|
161
|
-
if (!(await fileExists(
|
|
162
|
-
if (await fileExists(
|
|
163
|
-
await fs.rename(
|
|
559
|
+
if (!(await fileExists(paths.vectorDb))) {
|
|
560
|
+
if (await fileExists(legacyPaths.vectorDb)) {
|
|
561
|
+
await fs.rename(legacyPaths.vectorDb, paths.vectorDb);
|
|
164
562
|
migrated = true;
|
|
165
563
|
if (process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
166
564
|
console.error('[DEBUG] Migrated vector database');
|
|
@@ -178,10 +576,6 @@ async function migrateToNewStructure() {
|
|
|
178
576
|
}
|
|
179
577
|
// Read version from package.json so it never drifts
|
|
180
578
|
const PKG_VERSION = JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url), 'utf-8')).version;
|
|
181
|
-
const indexState = {
|
|
182
|
-
status: 'idle'
|
|
183
|
-
};
|
|
184
|
-
const autoRefresh = createAutoRefreshController();
|
|
185
579
|
const server = new Server({
|
|
186
580
|
name: 'codebase-context',
|
|
187
581
|
version: PKG_VERSION
|
|
@@ -194,34 +588,37 @@ const server = new Server({
|
|
|
194
588
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
195
589
|
return { tools: TOOLS };
|
|
196
590
|
});
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
591
|
+
function buildResources() {
|
|
592
|
+
const resources = [
|
|
593
|
+
{
|
|
594
|
+
uri: CONTEXT_RESOURCE_URI,
|
|
595
|
+
name: 'Codebase Intelligence',
|
|
596
|
+
description: 'Context for the active project in this MCP session. In multi-project sessions, this falls back to a workspace overview until a project is selected.',
|
|
597
|
+
mimeType: 'text/plain'
|
|
598
|
+
}
|
|
599
|
+
];
|
|
600
|
+
for (const project of listProjectDescriptors()) {
|
|
601
|
+
resources.push({
|
|
602
|
+
uri: buildProjectContextResourceUri(project.rootPath),
|
|
603
|
+
name: `Codebase Intelligence (${project.label})`,
|
|
604
|
+
description: `Project-scoped context for ${project.label}.`,
|
|
605
|
+
mimeType: 'text/plain'
|
|
606
|
+
});
|
|
205
607
|
}
|
|
206
|
-
|
|
608
|
+
return resources;
|
|
609
|
+
}
|
|
207
610
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
208
|
-
return { resources:
|
|
611
|
+
return { resources: buildResources() };
|
|
209
612
|
});
|
|
210
|
-
async function generateCodebaseContext() {
|
|
211
|
-
const intelligencePath =
|
|
212
|
-
const index = await ensureValidIndexOrAutoHeal();
|
|
613
|
+
async function generateCodebaseContext(project) {
|
|
614
|
+
const intelligencePath = project.paths.intelligence;
|
|
615
|
+
const index = await ensureValidIndexOrAutoHeal(project);
|
|
213
616
|
if (index.status === 'indexing') {
|
|
214
617
|
return ('# Codebase Intelligence\n\n' +
|
|
215
618
|
'Index is still being built. Retry in a moment.\n\n' +
|
|
216
619
|
`Index: ${index.status} (${index.confidence}, ${index.action})` +
|
|
217
620
|
(index.reason ? `\nReason: ${index.reason}` : ''));
|
|
218
621
|
}
|
|
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
|
-
}
|
|
225
622
|
try {
|
|
226
623
|
const content = await fs.readFile(intelligencePath, 'utf-8');
|
|
227
624
|
const intelligence = JSON.parse(content);
|
|
@@ -338,16 +735,68 @@ async function generateCodebaseContext() {
|
|
|
338
735
|
`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
339
736
|
}
|
|
340
737
|
}
|
|
738
|
+
function buildProjectSelectionMessage() {
|
|
739
|
+
const projects = listProjectDescriptors();
|
|
740
|
+
if (projects.length === 0) {
|
|
741
|
+
return [
|
|
742
|
+
'# Codebase Workspace',
|
|
743
|
+
'',
|
|
744
|
+
'This MCP session is waiting for project context.',
|
|
745
|
+
'If your host supports MCP roots, project discovery will begin after the client announces its workspace roots.',
|
|
746
|
+
'Otherwise retry the tool call with `project` using an absolute project path, file path, or file:// URI.'
|
|
747
|
+
].join('\n');
|
|
748
|
+
}
|
|
749
|
+
const lines = [
|
|
750
|
+
'# Codebase Workspace',
|
|
751
|
+
'',
|
|
752
|
+
'This MCP session is using client-announced roots as the workspace boundary.',
|
|
753
|
+
'Automatic routing is only possible when one project is unambiguous or this session already has an active project.',
|
|
754
|
+
'If the MCP client does not provide enough context, retry tool calls with `project` using a root path, subproject path, or file path.',
|
|
755
|
+
'',
|
|
756
|
+
'Available projects:',
|
|
757
|
+
''
|
|
758
|
+
];
|
|
759
|
+
for (const project of projects) {
|
|
760
|
+
const projectPathHint = project.relativePath
|
|
761
|
+
? `${project.relativePath} | ${project.rootPath}`
|
|
762
|
+
: project.rootPath;
|
|
763
|
+
lines.push(`- ${project.label} [${project.indexStatus}]`);
|
|
764
|
+
lines.push(` project: ${projectPathHint}`);
|
|
765
|
+
lines.push(` resource: ${buildProjectContextResourceUri(project.rootPath)}`);
|
|
766
|
+
}
|
|
767
|
+
lines.push('');
|
|
768
|
+
lines.push('Recommended flow: retry the tool call with `project`.');
|
|
769
|
+
return lines.join('\n');
|
|
770
|
+
}
|
|
341
771
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
342
772
|
const uri = request.params.uri;
|
|
773
|
+
const explicitProjectPath = getProjectPathFromContextResourceUri(uri);
|
|
774
|
+
if (explicitProjectPath) {
|
|
775
|
+
const selection = await resolveProjectSelector(explicitProjectPath);
|
|
776
|
+
if (!selection.ok) {
|
|
777
|
+
throw new Error(`Unknown project resource: ${uri}`);
|
|
778
|
+
}
|
|
779
|
+
const project = selection.project;
|
|
780
|
+
await initProject(project.rootPath, watcherDebounceMs, { enableWatcher: true });
|
|
781
|
+
setActiveProject(project.rootPath);
|
|
782
|
+
return {
|
|
783
|
+
contents: [
|
|
784
|
+
{
|
|
785
|
+
uri: buildProjectContextResourceUri(project.rootPath),
|
|
786
|
+
mimeType: 'text/plain',
|
|
787
|
+
text: await generateCodebaseContext(project)
|
|
788
|
+
}
|
|
789
|
+
]
|
|
790
|
+
};
|
|
791
|
+
}
|
|
343
792
|
if (isContextResourceUri(uri)) {
|
|
344
|
-
const
|
|
793
|
+
const project = await resolveProjectForResource();
|
|
345
794
|
return {
|
|
346
795
|
contents: [
|
|
347
796
|
{
|
|
348
797
|
uri: CONTEXT_RESOURCE_URI,
|
|
349
798
|
mimeType: 'text/plain',
|
|
350
|
-
text:
|
|
799
|
+
text: project ? await generateCodebaseContext(project) : buildProjectSelectionMessage()
|
|
351
800
|
}
|
|
352
801
|
]
|
|
353
802
|
};
|
|
@@ -358,16 +807,16 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
358
807
|
* Extract memories from conventional git commits (refactor:, migrate:, fix:, revert:).
|
|
359
808
|
* Scans last 90 days. Deduplicates via content hash. Zero friction alternative to manual memory.
|
|
360
809
|
*/
|
|
361
|
-
async function extractGitMemories() {
|
|
810
|
+
async function extractGitMemories(rootPath, memoryPath) {
|
|
362
811
|
// Quick check: skip if not a git repo
|
|
363
|
-
if (!(await fileExists(path.join(
|
|
812
|
+
if (!(await fileExists(path.join(rootPath, '.git'))))
|
|
364
813
|
return 0;
|
|
365
814
|
const { execSync } = await import('child_process');
|
|
366
815
|
let log;
|
|
367
816
|
try {
|
|
368
817
|
// Format: ISO-date<TAB>hash subject (e.g. "2026-01-15T10:00:00+00:00\tabc1234 fix: race condition")
|
|
369
818
|
log = execSync('git log --format="%aI\t%h %s" --since="90 days ago" --no-merges', {
|
|
370
|
-
cwd:
|
|
819
|
+
cwd: rootPath,
|
|
371
820
|
encoding: 'utf-8',
|
|
372
821
|
timeout: 5000
|
|
373
822
|
}).trim();
|
|
@@ -384,20 +833,20 @@ async function extractGitMemories() {
|
|
|
384
833
|
const parsedMemory = parseGitLogLineToMemory(line);
|
|
385
834
|
if (!parsedMemory)
|
|
386
835
|
continue;
|
|
387
|
-
const result = await appendMemoryFile(
|
|
836
|
+
const result = await appendMemoryFile(memoryPath, parsedMemory);
|
|
388
837
|
if (result.status === 'added')
|
|
389
838
|
added++;
|
|
390
839
|
}
|
|
391
840
|
return added;
|
|
392
841
|
}
|
|
393
|
-
async function performIndexingOnce(incrementalOnly) {
|
|
394
|
-
indexState.status = 'indexing';
|
|
842
|
+
async function performIndexingOnce(project, incrementalOnly) {
|
|
843
|
+
project.indexState.status = 'indexing';
|
|
395
844
|
const mode = incrementalOnly ? 'incremental' : 'full';
|
|
396
|
-
console.error(`Indexing (${mode}): ${
|
|
845
|
+
console.error(`Indexing (${mode}): ${project.rootPath}`);
|
|
397
846
|
try {
|
|
398
847
|
let lastLoggedProgress = { phase: '', percentage: -1 };
|
|
399
848
|
const indexer = new CodebaseIndexer({
|
|
400
|
-
rootPath:
|
|
849
|
+
rootPath: project.rootPath,
|
|
401
850
|
incrementalOnly,
|
|
402
851
|
onProgress: (progress) => {
|
|
403
852
|
// Only log when phase or percentage actually changes (prevents duplicate logs)
|
|
@@ -409,15 +858,15 @@ async function performIndexingOnce(incrementalOnly) {
|
|
|
409
858
|
}
|
|
410
859
|
}
|
|
411
860
|
});
|
|
412
|
-
indexState.indexer = indexer;
|
|
861
|
+
project.indexState.indexer = indexer;
|
|
413
862
|
const stats = await indexer.index();
|
|
414
|
-
indexState.status = 'ready';
|
|
415
|
-
indexState.lastIndexed = new Date();
|
|
416
|
-
indexState.stats = stats;
|
|
863
|
+
project.indexState.status = 'ready';
|
|
864
|
+
project.indexState.lastIndexed = new Date();
|
|
865
|
+
project.indexState.stats = stats;
|
|
417
866
|
console.error(`Complete: ${stats.indexedFiles} files, ${stats.totalChunks} chunks in ${(stats.duration / 1000).toFixed(2)}s`);
|
|
418
867
|
// Auto-extract memories from git history (non-blocking, best-effort)
|
|
419
868
|
try {
|
|
420
|
-
const gitMemories = await extractGitMemories();
|
|
869
|
+
const gitMemories = await extractGitMemories(project.rootPath, project.paths.memory);
|
|
421
870
|
if (gitMemories > 0) {
|
|
422
871
|
console.error(`[git-memory] Extracted ${gitMemories} new memor${gitMemories === 1 ? 'y' : 'ies'} from git history`);
|
|
423
872
|
}
|
|
@@ -427,16 +876,16 @@ async function performIndexingOnce(incrementalOnly) {
|
|
|
427
876
|
}
|
|
428
877
|
}
|
|
429
878
|
catch (error) {
|
|
430
|
-
indexState.status = 'error';
|
|
431
|
-
indexState.error = error instanceof Error ? error.message : String(error);
|
|
432
|
-
console.error('Indexing failed:', indexState.error);
|
|
879
|
+
project.indexState.status = 'error';
|
|
880
|
+
project.indexState.error = error instanceof Error ? error.message : String(error);
|
|
881
|
+
console.error('Indexing failed:', project.indexState.error);
|
|
433
882
|
}
|
|
434
883
|
}
|
|
435
|
-
async function performIndexing(incrementalOnly) {
|
|
884
|
+
async function performIndexing(project, incrementalOnly) {
|
|
436
885
|
let nextMode = incrementalOnly;
|
|
437
886
|
for (;;) {
|
|
438
|
-
await performIndexingOnce(nextMode);
|
|
439
|
-
const shouldRunQueuedRefresh = autoRefresh.consumeQueuedRefresh(indexState.status);
|
|
887
|
+
await performIndexingOnce(project, nextMode);
|
|
888
|
+
const shouldRunQueuedRefresh = project.autoRefresh.consumeQueuedRefresh(project.indexState.status);
|
|
440
889
|
if (!shouldRunQueuedRefresh)
|
|
441
890
|
return;
|
|
442
891
|
if (process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
@@ -445,23 +894,156 @@ async function performIndexing(incrementalOnly) {
|
|
|
445
894
|
nextMode = true;
|
|
446
895
|
}
|
|
447
896
|
}
|
|
448
|
-
async function shouldReindex() {
|
|
449
|
-
const indexPath = PATHS.keywordIndex;
|
|
897
|
+
async function shouldReindex(paths) {
|
|
450
898
|
try {
|
|
451
|
-
await fs.access(
|
|
899
|
+
await fs.access(paths.keywordIndex);
|
|
452
900
|
return false;
|
|
453
901
|
}
|
|
454
902
|
catch {
|
|
455
903
|
return true;
|
|
456
904
|
}
|
|
457
905
|
}
|
|
906
|
+
async function refreshDiscoveredProjectsForKnownRoots() {
|
|
907
|
+
clearDiscoveredProjectPaths();
|
|
908
|
+
await Promise.all(getKnownRootPaths().map(async (rootPath) => {
|
|
909
|
+
const candidates = await discoverProjectsWithinRoot(rootPath, {
|
|
910
|
+
maxDepth: PROJECT_DISCOVERY_MAX_DEPTH
|
|
911
|
+
});
|
|
912
|
+
for (const candidate of candidates) {
|
|
913
|
+
registerDiscoveredProjectPath(candidate.rootPath, 'subdirectory');
|
|
914
|
+
}
|
|
915
|
+
}));
|
|
916
|
+
}
|
|
917
|
+
async function validateClientRootEntries(rootEntries) {
|
|
918
|
+
const validatedRoots = await Promise.all(rootEntries.map(async (entry) => {
|
|
919
|
+
try {
|
|
920
|
+
const stats = await fs.stat(entry.rootPath);
|
|
921
|
+
if (!stats.isDirectory()) {
|
|
922
|
+
return undefined;
|
|
923
|
+
}
|
|
924
|
+
return entry;
|
|
925
|
+
}
|
|
926
|
+
catch {
|
|
927
|
+
return undefined;
|
|
928
|
+
}
|
|
929
|
+
}));
|
|
930
|
+
return validatedRoots.filter((entry) => !!entry);
|
|
931
|
+
}
|
|
932
|
+
async function refreshKnownRootsFromClient() {
|
|
933
|
+
try {
|
|
934
|
+
const { roots } = await server.listRoots();
|
|
935
|
+
const fileRoots = await validateClientRootEntries(roots
|
|
936
|
+
.map((root) => ({
|
|
937
|
+
uri: root.uri,
|
|
938
|
+
label: typeof root.name === 'string' && root.name.trim() ? root.name.trim() : undefined
|
|
939
|
+
}))
|
|
940
|
+
.filter((root) => root.uri.startsWith('file://'))
|
|
941
|
+
.map((root) => ({
|
|
942
|
+
rootPath: fileURLToPath(root.uri),
|
|
943
|
+
label: root.label
|
|
944
|
+
})));
|
|
945
|
+
clientRootsEnabled = fileRoots.length > 0;
|
|
946
|
+
syncKnownRoots(fileRoots);
|
|
947
|
+
}
|
|
948
|
+
catch {
|
|
949
|
+
clientRootsEnabled = false;
|
|
950
|
+
syncKnownRoots(primaryRootPath ? [{ rootPath: primaryRootPath }] : []);
|
|
951
|
+
}
|
|
952
|
+
await refreshDiscoveredProjectsForKnownRoots();
|
|
953
|
+
}
|
|
954
|
+
async function resolveExplicitProjectSelection(selection) {
|
|
955
|
+
const explicitProject = selection.project ?? selection.projectDirectory;
|
|
956
|
+
if (explicitProject) {
|
|
957
|
+
const resolution = await resolveProjectSelector(explicitProject);
|
|
958
|
+
if (!resolution.ok) {
|
|
959
|
+
return resolution;
|
|
960
|
+
}
|
|
961
|
+
await initProject(resolution.project.rootPath, watcherDebounceMs, { enableWatcher: true });
|
|
962
|
+
setActiveProject(resolution.project.rootPath);
|
|
963
|
+
return resolution;
|
|
964
|
+
}
|
|
965
|
+
return {
|
|
966
|
+
ok: false,
|
|
967
|
+
response: buildProjectSelectionError('selection_required', 'No project selector was provided.')
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
async function resolveProjectForTool(args) {
|
|
971
|
+
const requestedProject = parseProjectSelector(args.project);
|
|
972
|
+
const requestedProjectDirectory = parseProjectSelector(args.project_directory);
|
|
973
|
+
if (requestedProject || requestedProjectDirectory) {
|
|
974
|
+
return resolveExplicitProjectSelection({
|
|
975
|
+
project: requestedProject,
|
|
976
|
+
projectDirectory: requestedProjectDirectory
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
const activeProject = activeProjectKey ? getTrackedRootPathByKey(activeProjectKey) : undefined;
|
|
980
|
+
if (activeProject) {
|
|
981
|
+
const project = getOrCreateProject(activeProject);
|
|
982
|
+
await initProject(project.rootPath, watcherDebounceMs, { enableWatcher: true });
|
|
983
|
+
touchProject(project.rootPath);
|
|
984
|
+
return { ok: true, project };
|
|
985
|
+
}
|
|
986
|
+
const availableProjects = listProjectDescriptors();
|
|
987
|
+
if (availableProjects.length === 0) {
|
|
988
|
+
return {
|
|
989
|
+
ok: false,
|
|
990
|
+
response: buildProjectSelectionError('selection_required', 'No active project is available yet. Retry with project or wait for MCP roots to arrive.', {
|
|
991
|
+
reason: clientRootsEnabled
|
|
992
|
+
? 'workspace_waiting_for_project_selection'
|
|
993
|
+
: 'workspace_waiting_for_roots_or_project',
|
|
994
|
+
nextAction: 'retry_with_project'
|
|
995
|
+
})
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
if (availableProjects.length === 1) {
|
|
999
|
+
const project = getOrCreateProject(availableProjects[0].rootPath);
|
|
1000
|
+
await initProject(project.rootPath, watcherDebounceMs, { enableWatcher: true });
|
|
1001
|
+
setActiveProject(project.rootPath);
|
|
1002
|
+
return { ok: true, project };
|
|
1003
|
+
}
|
|
1004
|
+
return {
|
|
1005
|
+
ok: false,
|
|
1006
|
+
response: buildProjectSelectionError('selection_required', 'Multiple projects are available and no active project could be inferred. Retry with project.', {
|
|
1007
|
+
reason: 'multiple_projects_configured_no_active_context',
|
|
1008
|
+
nextAction: 'retry_with_project'
|
|
1009
|
+
})
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
async function resolveProjectForResource() {
|
|
1013
|
+
const activeProject = activeProjectKey ? getTrackedRootPathByKey(activeProjectKey) : undefined;
|
|
1014
|
+
if (activeProject) {
|
|
1015
|
+
const project = getOrCreateProject(activeProject);
|
|
1016
|
+
await initProject(project.rootPath, watcherDebounceMs, { enableWatcher: true });
|
|
1017
|
+
touchProject(project.rootPath);
|
|
1018
|
+
return project;
|
|
1019
|
+
}
|
|
1020
|
+
const availableProjects = listProjectDescriptors();
|
|
1021
|
+
if (availableProjects.length !== 1) {
|
|
1022
|
+
return undefined;
|
|
1023
|
+
}
|
|
1024
|
+
const project = getOrCreateProject(availableProjects[0].rootPath);
|
|
1025
|
+
await initProject(project.rootPath, watcherDebounceMs, { enableWatcher: true });
|
|
1026
|
+
setActiveProject(project.rootPath);
|
|
1027
|
+
return project;
|
|
1028
|
+
}
|
|
458
1029
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
459
1030
|
const { name, arguments: args } = request.params;
|
|
1031
|
+
const normalizedArgs = args && typeof args === 'object' && !Array.isArray(args)
|
|
1032
|
+
? args
|
|
1033
|
+
: {};
|
|
460
1034
|
try {
|
|
1035
|
+
if (!toolNames.has(name)) {
|
|
1036
|
+
return await dispatchTool(name, normalizedArgs, createWorkspaceToolContext());
|
|
1037
|
+
}
|
|
1038
|
+
const projectResolution = await resolveProjectForTool(normalizedArgs);
|
|
1039
|
+
if (!projectResolution.ok) {
|
|
1040
|
+
return projectResolution.response;
|
|
1041
|
+
}
|
|
1042
|
+
const project = projectResolution.project;
|
|
461
1043
|
// Gate INDEX_CONSUMING tools on a valid, healthy index
|
|
462
1044
|
let indexSignal;
|
|
463
1045
|
if (INDEX_CONSUMING_TOOL_NAMES.includes(name)) {
|
|
464
|
-
if (indexState.status === 'indexing') {
|
|
1046
|
+
if (project.indexState.status === 'indexing') {
|
|
465
1047
|
return {
|
|
466
1048
|
content: [
|
|
467
1049
|
{
|
|
@@ -474,49 +1056,59 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
474
1056
|
]
|
|
475
1057
|
};
|
|
476
1058
|
}
|
|
477
|
-
if (indexState.status === 'error') {
|
|
1059
|
+
if (project.indexState.status === 'error') {
|
|
478
1060
|
return {
|
|
479
1061
|
content: [
|
|
480
1062
|
{
|
|
481
1063
|
type: 'text',
|
|
482
1064
|
text: JSON.stringify({
|
|
483
1065
|
status: 'error',
|
|
484
|
-
message: `Indexer error: ${indexState.error}`
|
|
1066
|
+
message: `Indexer error: ${project.indexState.error}`
|
|
485
1067
|
})
|
|
486
1068
|
}
|
|
487
1069
|
]
|
|
488
1070
|
};
|
|
489
1071
|
}
|
|
490
|
-
indexSignal = await ensureValidIndexOrAutoHeal();
|
|
491
|
-
if (indexSignal.action === 'rebuild-
|
|
1072
|
+
indexSignal = await ensureValidIndexOrAutoHeal(project);
|
|
1073
|
+
if (indexSignal.action === 'rebuild-started') {
|
|
492
1074
|
return {
|
|
493
1075
|
content: [
|
|
494
1076
|
{
|
|
495
1077
|
type: 'text',
|
|
496
1078
|
text: JSON.stringify({
|
|
497
|
-
|
|
1079
|
+
status: 'indexing',
|
|
1080
|
+
message: 'Index rebuild in progress — please retry shortly',
|
|
498
1081
|
index: indexSignal
|
|
499
1082
|
})
|
|
500
1083
|
}
|
|
501
|
-
]
|
|
502
|
-
isError: true
|
|
1084
|
+
]
|
|
503
1085
|
};
|
|
504
1086
|
}
|
|
505
1087
|
}
|
|
506
|
-
const
|
|
507
|
-
|
|
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
|
|
1088
|
+
const result = await dispatchTool(name, normalizedArgs, createToolContext(project));
|
|
1089
|
+
// Inject routing/index metadata into JSON responses so agents can reuse the resolved project safely.
|
|
514
1090
|
if (indexSignal !== undefined && result.content?.[0]) {
|
|
515
1091
|
try {
|
|
516
1092
|
const parsed = JSON.parse(result.content[0].text);
|
|
517
1093
|
result.content[0] = {
|
|
518
1094
|
type: 'text',
|
|
519
|
-
text: JSON.stringify({
|
|
1095
|
+
text: JSON.stringify({
|
|
1096
|
+
...parsed,
|
|
1097
|
+
index: indexSignal,
|
|
1098
|
+
project: buildProjectDescriptor(project.rootPath)
|
|
1099
|
+
})
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
catch {
|
|
1103
|
+
/* response wasn't JSON, skip injection */
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
else if (result.content?.[0]) {
|
|
1107
|
+
try {
|
|
1108
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
1109
|
+
result.content[0] = {
|
|
1110
|
+
type: 'text',
|
|
1111
|
+
text: JSON.stringify({ ...parsed, project: buildProjectDescriptor(project.rootPath) })
|
|
520
1112
|
};
|
|
521
1113
|
}
|
|
522
1114
|
catch {
|
|
@@ -537,101 +1129,201 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
537
1129
|
};
|
|
538
1130
|
}
|
|
539
1131
|
});
|
|
540
|
-
async function
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if (!stats.isDirectory()) {
|
|
545
|
-
console.error(`ERROR: Root path is not a directory: ${ROOT_PATH}`);
|
|
546
|
-
console.error(`Please specify a valid project directory.`);
|
|
547
|
-
process.exit(1);
|
|
548
|
-
}
|
|
1132
|
+
async function ensureProjectInitialized(project) {
|
|
1133
|
+
if (project.initPromise) {
|
|
1134
|
+
await project.initPromise;
|
|
1135
|
+
return;
|
|
549
1136
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
console.error(`Please specify a valid project directory.`);
|
|
553
|
-
process.exit(1);
|
|
1137
|
+
if (project.indexState.status !== 'idle') {
|
|
1138
|
+
return;
|
|
554
1139
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
1140
|
+
project.initPromise = (async () => {
|
|
1141
|
+
// Migrate legacy structure
|
|
1142
|
+
try {
|
|
1143
|
+
const legacyPaths = makeLegacyPaths(project.rootPath);
|
|
1144
|
+
const migrated = await migrateToNewStructure(project.paths, legacyPaths);
|
|
1145
|
+
if (migrated && process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
1146
|
+
console.error(`[DEBUG] Migrated to .codebase-context/ structure: ${project.rootPath}`);
|
|
1147
|
+
}
|
|
560
1148
|
}
|
|
1149
|
+
catch {
|
|
1150
|
+
// Non-fatal
|
|
1151
|
+
}
|
|
1152
|
+
// Check if indexing is needed
|
|
1153
|
+
const needsIndex = await shouldReindex(project.paths);
|
|
1154
|
+
if (needsIndex) {
|
|
1155
|
+
if (process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
1156
|
+
console.error(`[DEBUG] Starting indexing: ${project.rootPath}`);
|
|
1157
|
+
}
|
|
1158
|
+
void performIndexing(project);
|
|
1159
|
+
}
|
|
1160
|
+
else {
|
|
1161
|
+
if (process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
1162
|
+
console.error(`[DEBUG] Index found. Ready: ${project.rootPath}`);
|
|
1163
|
+
}
|
|
1164
|
+
project.indexState.status = 'ready';
|
|
1165
|
+
project.indexState.lastIndexed = new Date();
|
|
1166
|
+
}
|
|
1167
|
+
})().finally(() => {
|
|
1168
|
+
project.initPromise = undefined;
|
|
1169
|
+
});
|
|
1170
|
+
await project.initPromise;
|
|
1171
|
+
}
|
|
1172
|
+
function ensureProjectWatcher(project, debounceMs) {
|
|
1173
|
+
if (project.stopWatcher) {
|
|
1174
|
+
touchProject(project.rootPath);
|
|
1175
|
+
return;
|
|
561
1176
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
1177
|
+
project.stopWatcher = startFileWatcher({
|
|
1178
|
+
rootPath: project.rootPath,
|
|
1179
|
+
debounceMs,
|
|
1180
|
+
onChanged: () => {
|
|
1181
|
+
const shouldRunNow = project.autoRefresh.onFileChange(project.indexState.status === 'indexing');
|
|
1182
|
+
if (!shouldRunNow) {
|
|
1183
|
+
if (process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
1184
|
+
console.error(`[file-watcher] Index in progress — queueing auto-refresh: ${project.rootPath}`);
|
|
1185
|
+
}
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
1189
|
+
console.error(`[file-watcher] Changes detected — incremental reindex starting: ${project.rootPath}`);
|
|
1190
|
+
}
|
|
1191
|
+
void performIndexing(project, true);
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
touchProject(project.rootPath);
|
|
1195
|
+
const watchedProjects = getAllProjects().filter((entry) => entry.stopWatcher);
|
|
1196
|
+
if (watchedProjects.length <= MAX_WATCHED_PROJECTS) {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
const evictionCandidates = watchedProjects
|
|
1200
|
+
.filter((entry) => normalizeRootKey(entry.rootPath) !== activeProjectKey)
|
|
1201
|
+
.sort((a, b) => {
|
|
1202
|
+
const accessA = projectAccessOrder.get(normalizeRootKey(a.rootPath)) ?? 0;
|
|
1203
|
+
const accessB = projectAccessOrder.get(normalizeRootKey(b.rootPath)) ?? 0;
|
|
1204
|
+
return accessA - accessB;
|
|
1205
|
+
});
|
|
1206
|
+
const projectToEvict = evictionCandidates[0];
|
|
1207
|
+
if (projectToEvict?.stopWatcher) {
|
|
1208
|
+
projectToEvict.stopWatcher();
|
|
1209
|
+
delete projectToEvict.stopWatcher;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
async function initProject(rootPath, debounceMs, options) {
|
|
1213
|
+
rememberProjectPath(rootPath);
|
|
1214
|
+
const project = getOrCreateProject(rootPath);
|
|
1215
|
+
await ensureProjectInitialized(project);
|
|
1216
|
+
touchProject(project.rootPath);
|
|
1217
|
+
if (options.enableWatcher) {
|
|
1218
|
+
ensureProjectWatcher(project, debounceMs);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
async function main() {
|
|
1222
|
+
if (primaryRootPath) {
|
|
1223
|
+
// Validate bootstrap root path exists and is a directory when explicitly configured.
|
|
1224
|
+
try {
|
|
1225
|
+
const stats = await fs.stat(primaryRootPath);
|
|
1226
|
+
if (!stats.isDirectory()) {
|
|
1227
|
+
console.error(`ERROR: Root path is not a directory: ${primaryRootPath}`);
|
|
1228
|
+
console.error(`Please specify a valid project directory.`);
|
|
1229
|
+
process.exit(1);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
catch (_error) {
|
|
1233
|
+
console.error(`ERROR: Root path does not exist: ${primaryRootPath}`);
|
|
1234
|
+
console.error(`Please specify a valid project directory.`);
|
|
1235
|
+
process.exit(1);
|
|
566
1236
|
}
|
|
567
1237
|
}
|
|
568
1238
|
// Server startup banner (guarded to avoid stderr during MCP STDIO handshake)
|
|
569
1239
|
if (process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
570
1240
|
console.error('[DEBUG] Codebase Context MCP Server');
|
|
571
|
-
console.error(
|
|
1241
|
+
console.error(primaryRootPath
|
|
1242
|
+
? `[DEBUG] Bootstrap root: ${primaryRootPath}`
|
|
1243
|
+
: '[DEBUG] Bootstrap root: <workspace-awaiting>');
|
|
572
1244
|
console.error(`[DEBUG] Analyzers: ${analyzerRegistry
|
|
573
1245
|
.getAll()
|
|
574
1246
|
.map((a) => a.name)
|
|
575
1247
|
.join(', ')}`);
|
|
576
1248
|
}
|
|
577
1249
|
// Check for package.json to confirm it's a project root (guarded to avoid stderr during handshake)
|
|
578
|
-
if (process.env.CODEBASE_CONTEXT_DEBUG) {
|
|
1250
|
+
if (process.env.CODEBASE_CONTEXT_DEBUG && primaryRootPath) {
|
|
579
1251
|
try {
|
|
580
|
-
await fs.access(path.join(
|
|
581
|
-
console.error(`[DEBUG] Project detected: ${path.basename(
|
|
1252
|
+
await fs.access(path.join(primaryRootPath, 'package.json'));
|
|
1253
|
+
console.error(`[DEBUG] Project detected: ${path.basename(primaryRootPath)}`);
|
|
582
1254
|
}
|
|
583
1255
|
catch {
|
|
584
1256
|
console.error(`[DEBUG] WARNING: No package.json found. This may not be a project root.`);
|
|
585
1257
|
}
|
|
586
1258
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
1259
|
+
// Parent death guard — catches SIGKILL, crashes, terminal close on ALL platforms.
|
|
1260
|
+
// process.kill(pid, 0) throws ESRCH when the process no longer exists.
|
|
1261
|
+
const parentPid = process.ppid;
|
|
1262
|
+
if (parentPid > 1) {
|
|
1263
|
+
const parentGuard = setInterval(() => {
|
|
1264
|
+
try {
|
|
1265
|
+
process.kill(parentPid, 0);
|
|
1266
|
+
}
|
|
1267
|
+
catch (err) {
|
|
1268
|
+
// ESRCH = process gone → exit. EPERM = process alive, different UID → ignore.
|
|
1269
|
+
if (err.code === 'ESRCH') {
|
|
1270
|
+
process.exit(0);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}, 5_000);
|
|
1274
|
+
parentGuard.unref();
|
|
598
1275
|
}
|
|
599
1276
|
const transport = new StdioServerTransport();
|
|
600
1277
|
await server.connect(transport);
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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);
|
|
1278
|
+
// Register cleanup before any handler that calls process.exit(), so the
|
|
1279
|
+
// exit listener is always in place when stdin/onclose/signals fire.
|
|
1280
|
+
const stopAllWatchers = () => {
|
|
1281
|
+
for (const project of getAllProjects()) {
|
|
1282
|
+
project.stopWatcher?.();
|
|
621
1283
|
}
|
|
622
|
-
}
|
|
623
|
-
process.once('exit',
|
|
1284
|
+
};
|
|
1285
|
+
process.once('exit', stopAllWatchers);
|
|
624
1286
|
process.once('SIGINT', () => {
|
|
625
|
-
|
|
1287
|
+
stopAllWatchers();
|
|
626
1288
|
process.exit(0);
|
|
627
1289
|
});
|
|
628
1290
|
process.once('SIGTERM', () => {
|
|
629
|
-
|
|
1291
|
+
stopAllWatchers();
|
|
1292
|
+
process.exit(0);
|
|
1293
|
+
});
|
|
1294
|
+
process.once('SIGHUP', () => {
|
|
1295
|
+
stopAllWatchers();
|
|
630
1296
|
process.exit(0);
|
|
631
1297
|
});
|
|
1298
|
+
// Detect stdin pipe closure — the primary signal that the MCP client is gone.
|
|
1299
|
+
// StdioServerTransport only listens for 'data'/'error', never 'end'.
|
|
1300
|
+
process.stdin.on('end', () => process.exit(0));
|
|
1301
|
+
process.stdin.on('close', () => process.exit(0));
|
|
1302
|
+
// Handle graceful MCP protocol-level disconnect.
|
|
1303
|
+
// Fires after SDK internal cleanup when transport.close() is called.
|
|
1304
|
+
server.onclose = () => process.exit(0);
|
|
1305
|
+
if (process.env.CODEBASE_CONTEXT_DEBUG)
|
|
1306
|
+
console.error('[DEBUG] Server ready');
|
|
1307
|
+
await refreshKnownRootsFromClient();
|
|
1308
|
+
// Keep the current single-project auto-select behavior when exactly one startup project is known.
|
|
1309
|
+
const startupRoots = getKnownRootPaths();
|
|
1310
|
+
if (startupRoots.length === 1) {
|
|
1311
|
+
await initProject(startupRoots[0], watcherDebounceMs, { enableWatcher: true });
|
|
1312
|
+
setActiveProject(startupRoots[0]);
|
|
1313
|
+
}
|
|
1314
|
+
// Subscribe to root changes
|
|
1315
|
+
server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
|
|
1316
|
+
try {
|
|
1317
|
+
await refreshKnownRootsFromClient();
|
|
1318
|
+
}
|
|
1319
|
+
catch {
|
|
1320
|
+
/* best-effort */
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
632
1323
|
}
|
|
633
1324
|
// Export server components for programmatic use
|
|
634
|
-
export { server,
|
|
1325
|
+
export { server, refreshKnownRootsFromClient, resolveRootPath, shouldReindex, TOOLS };
|
|
1326
|
+
export { performIndexing };
|
|
635
1327
|
// Only auto-start when run directly as CLI (not when imported as module)
|
|
636
1328
|
// Check if this module is the entry point
|
|
637
1329
|
const isDirectRun = process.argv[1]?.replace(/\\/g, '/').endsWith('index.js') ||
|