codecortex-ai 0.1.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.
@@ -0,0 +1,962 @@
1
+ // src/mcp/server.ts
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+
5
+ // src/mcp/tools/read.ts
6
+ import { z } from "zod";
7
+
8
+ // src/utils/files.ts
9
+ import { readFile as fsRead, writeFile as fsWrite, mkdir, readdir, stat } from "fs/promises";
10
+ import { existsSync, createWriteStream } from "fs";
11
+ import { join, dirname } from "path";
12
+ async function readFile(path) {
13
+ try {
14
+ return await fsRead(path, "utf-8");
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ async function writeFile(path, content) {
20
+ await ensureDir(dirname(path));
21
+ await fsWrite(path, content, "utf-8");
22
+ }
23
+ async function ensureDir(dir) {
24
+ if (!existsSync(dir)) {
25
+ await mkdir(dir, { recursive: true });
26
+ }
27
+ }
28
+ async function listFiles(dir, extension) {
29
+ if (!existsSync(dir)) return [];
30
+ const entries = await readdir(dir, { withFileTypes: true });
31
+ const files = [];
32
+ for (const entry of entries) {
33
+ if (entry.isFile()) {
34
+ if (!extension || entry.name.endsWith(extension)) {
35
+ files.push(join(dir, entry.name));
36
+ }
37
+ }
38
+ }
39
+ return files;
40
+ }
41
+ function cortexPath(projectRoot, ...segments) {
42
+ return join(projectRoot, ".codecortex", ...segments);
43
+ }
44
+ async function writeJsonStream(path, obj, arrayKey) {
45
+ await ensureDir(dirname(path));
46
+ const arr = obj[arrayKey];
47
+ const stream = createWriteStream(path);
48
+ stream.setMaxListeners(0);
49
+ let streamError = null;
50
+ stream.on("error", (err) => {
51
+ streamError = err;
52
+ });
53
+ function waitDrain() {
54
+ return new Promise((resolve) => stream.once("drain", resolve));
55
+ }
56
+ const header = {};
57
+ for (const [k, v] of Object.entries(obj)) {
58
+ if (k !== arrayKey) header[k] = v;
59
+ }
60
+ const headerJson = JSON.stringify(header);
61
+ stream.write(headerJson.slice(0, -1) + `,"${arrayKey}":[`);
62
+ const BATCH_SIZE = 1e3;
63
+ for (let i = 0; i < arr.length; i += BATCH_SIZE) {
64
+ if (streamError) throw streamError;
65
+ const end = Math.min(i + BATCH_SIZE, arr.length);
66
+ const chunks = [];
67
+ for (let j = i; j < end; j++) {
68
+ chunks.push((j > 0 ? "," : "") + JSON.stringify(arr[j]));
69
+ }
70
+ if (!stream.write(chunks.join(""))) {
71
+ await waitDrain();
72
+ }
73
+ }
74
+ stream.write("]}");
75
+ return new Promise((resolve, reject) => {
76
+ if (streamError) {
77
+ reject(streamError);
78
+ return;
79
+ }
80
+ stream.on("finish", resolve);
81
+ stream.on("error", reject);
82
+ stream.end();
83
+ });
84
+ }
85
+
86
+ // src/utils/yaml.ts
87
+ import { parse, stringify } from "yaml";
88
+ function parseYaml(content) {
89
+ return parse(content);
90
+ }
91
+ function stringifyYaml(data) {
92
+ return stringify(data, { lineWidth: 120 });
93
+ }
94
+
95
+ // src/core/manifest.ts
96
+ async function readManifest(projectRoot) {
97
+ const content = await readFile(cortexPath(projectRoot, "cortex.yaml"));
98
+ if (!content) return null;
99
+ return parseYaml(content);
100
+ }
101
+ async function writeManifest(projectRoot, manifest) {
102
+ const content = stringifyYaml(manifest);
103
+ await writeFile(cortexPath(projectRoot, "cortex.yaml"), content);
104
+ }
105
+ function createManifest(opts) {
106
+ const now = (/* @__PURE__ */ new Date()).toISOString();
107
+ return {
108
+ version: "1.0.0",
109
+ project: opts.project,
110
+ root: opts.root,
111
+ generated: now,
112
+ lastUpdated: now,
113
+ languages: opts.languages,
114
+ totalFiles: opts.totalFiles,
115
+ totalSymbols: opts.totalSymbols,
116
+ totalModules: opts.totalModules,
117
+ tiers: {
118
+ hot: ["cortex.yaml", "constitution.md", "overview.md", "graph.json", "symbols.json", "temporal.json"],
119
+ warm: ["modules/"],
120
+ cold: ["decisions/", "sessions/", "patterns.md"]
121
+ }
122
+ };
123
+ }
124
+ async function updateManifest(projectRoot, updates) {
125
+ const manifest = await readManifest(projectRoot);
126
+ if (!manifest) return null;
127
+ const updated = {
128
+ ...manifest,
129
+ ...updates,
130
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
131
+ };
132
+ await writeManifest(projectRoot, updated);
133
+ return updated;
134
+ }
135
+
136
+ // src/core/graph.ts
137
+ import { createWriteStream as createWriteStream2 } from "fs";
138
+ import { dirname as dirname2 } from "path";
139
+ async function readGraph(projectRoot) {
140
+ const content = await readFile(cortexPath(projectRoot, "graph.json"));
141
+ if (!content) return null;
142
+ return JSON.parse(content);
143
+ }
144
+ async function writeGraph(projectRoot, graph) {
145
+ const path = cortexPath(projectRoot, "graph.json");
146
+ await ensureDir(dirname2(path));
147
+ const stream = createWriteStream2(path);
148
+ stream.setMaxListeners(0);
149
+ function waitDrain() {
150
+ return new Promise((resolve) => stream.once("drain", resolve));
151
+ }
152
+ async function writeArray(arr) {
153
+ stream.write("[");
154
+ const BATCH = 1e3;
155
+ for (let i = 0; i < arr.length; i += BATCH) {
156
+ const end = Math.min(i + BATCH, arr.length);
157
+ const chunks = [];
158
+ for (let j = i; j < end; j++) {
159
+ chunks.push((j > 0 ? "," : "") + JSON.stringify(arr[j]));
160
+ }
161
+ if (!stream.write(chunks.join(""))) await waitDrain();
162
+ }
163
+ stream.write("]");
164
+ }
165
+ stream.write(`{"generated":${JSON.stringify(graph.generated)},"modules":`);
166
+ await writeArray(graph.modules);
167
+ stream.write(',"imports":');
168
+ await writeArray(graph.imports);
169
+ stream.write(',"calls":');
170
+ await writeArray(graph.calls);
171
+ stream.write(`,"entryPoints":${JSON.stringify(graph.entryPoints)}`);
172
+ stream.write(`,"externalDeps":${JSON.stringify(graph.externalDeps)}`);
173
+ stream.write("}");
174
+ return new Promise((resolve, reject) => {
175
+ stream.on("finish", resolve);
176
+ stream.on("error", reject);
177
+ stream.end();
178
+ });
179
+ }
180
+ function buildGraph(opts) {
181
+ return {
182
+ generated: (/* @__PURE__ */ new Date()).toISOString(),
183
+ modules: opts.modules,
184
+ imports: opts.imports,
185
+ calls: opts.calls,
186
+ entryPoints: opts.entryPoints,
187
+ externalDeps: opts.externalDeps
188
+ };
189
+ }
190
+ function getModuleDependencies(graph, moduleName) {
191
+ const moduleFiles = new Set(
192
+ graph.modules.find((m) => m.name === moduleName)?.files || []
193
+ );
194
+ return {
195
+ imports: graph.imports.filter((e) => moduleFiles.has(e.source)),
196
+ importedBy: graph.imports.filter((e) => moduleFiles.has(e.target)),
197
+ calls: graph.calls.filter((e) => moduleFiles.has(e.file))
198
+ };
199
+ }
200
+ function getMostImportedFiles(graph, limit = 10) {
201
+ const counts = /* @__PURE__ */ new Map();
202
+ for (const edge of graph.imports) {
203
+ counts.set(edge.target, (counts.get(edge.target) || 0) + 1);
204
+ }
205
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit).map(([file, importCount]) => ({ file, importCount }));
206
+ }
207
+ function enrichCouplingWithImports(graph, coupling) {
208
+ const importPairs = /* @__PURE__ */ new Set();
209
+ for (const edge of graph.imports) {
210
+ importPairs.add([edge.source, edge.target].sort().join("|"));
211
+ }
212
+ for (const pair of coupling) {
213
+ const key = [pair.fileA, pair.fileB].sort().join("|");
214
+ pair.hasImport = importPairs.has(key);
215
+ }
216
+ }
217
+
218
+ // src/utils/markdown.ts
219
+ function generateModuleDoc(analysis) {
220
+ const lines = [
221
+ `# Module: ${analysis.name}`,
222
+ "",
223
+ `## Purpose`,
224
+ analysis.purpose,
225
+ "",
226
+ `## Data Flow`,
227
+ analysis.dataFlow,
228
+ "",
229
+ `## Public API`,
230
+ ...analysis.publicApi.map((api) => `- \`${api}\``),
231
+ "",
232
+ `## Dependencies`,
233
+ ...analysis.dependencies.map((dep) => `- ${dep}`)
234
+ ];
235
+ if (analysis.gotchas.length > 0) {
236
+ lines.push("", `## Gotchas`, ...analysis.gotchas.map((g) => `- ${g}`));
237
+ }
238
+ if (analysis.temporalSignals) {
239
+ const t = analysis.temporalSignals;
240
+ lines.push(
241
+ "",
242
+ `## Temporal Signals`,
243
+ `- **Churn:** ${t.churn}`,
244
+ `- **Coupled with:** ${t.coupledWith.join(", ") || "none"}`,
245
+ `- **Stability:** ${t.stability}`
246
+ );
247
+ if (t.bugHistory.length > 0) {
248
+ lines.push(`- **Bug history:** ${t.bugHistory.join("; ")}`);
249
+ }
250
+ lines.push(`- **Last changed:** ${t.lastChanged}`);
251
+ }
252
+ return lines.join("\n") + "\n";
253
+ }
254
+ function generateDecisionDoc(decision) {
255
+ const lines = [
256
+ `# Decision: ${decision.title}`,
257
+ "",
258
+ `**Date:** ${decision.date}`,
259
+ `**Status:** ${decision.status}`,
260
+ "",
261
+ `## Context`,
262
+ decision.context,
263
+ "",
264
+ `## Decision`,
265
+ decision.decision
266
+ ];
267
+ if (decision.alternatives.length > 0) {
268
+ lines.push("", `## Alternatives Considered`, ...decision.alternatives.map((a) => `- ${a}`));
269
+ }
270
+ if (decision.consequences.length > 0) {
271
+ lines.push("", `## Consequences`, ...decision.consequences.map((c) => `- ${c}`));
272
+ }
273
+ return lines.join("\n") + "\n";
274
+ }
275
+ function generatePatternEntry(pattern) {
276
+ const lines = [
277
+ `### ${pattern.name}`,
278
+ "",
279
+ pattern.description,
280
+ "",
281
+ "```",
282
+ pattern.example,
283
+ "```"
284
+ ];
285
+ if (pattern.files.length > 0) {
286
+ lines.push("", `Files: ${pattern.files.map((f) => `\`${f}\``).join(", ")}`);
287
+ }
288
+ return lines.join("\n");
289
+ }
290
+
291
+ // src/core/modules.ts
292
+ async function readModuleDoc(projectRoot, moduleName) {
293
+ return readFile(cortexPath(projectRoot, "modules", `${moduleName}.md`));
294
+ }
295
+ async function writeModuleDoc(projectRoot, analysis) {
296
+ const dir = cortexPath(projectRoot, "modules");
297
+ await ensureDir(dir);
298
+ const content = generateModuleDoc(analysis);
299
+ await writeFile(cortexPath(projectRoot, "modules", `${analysis.name}.md`), content);
300
+ }
301
+ async function listModuleDocs(projectRoot) {
302
+ const dir = cortexPath(projectRoot, "modules");
303
+ const files = await listFiles(dir, ".md");
304
+ return files.map((f) => {
305
+ const name = f.split("/").pop() || "";
306
+ return name.replace(".md", "");
307
+ });
308
+ }
309
+ function buildAnalysisPrompt(moduleName, sourceFiles) {
310
+ const fileList = sourceFiles.map((f) => `### ${f.path}
311
+ \`\`\`
312
+ ${f.content}
313
+ \`\`\``).join("\n\n");
314
+ return `Analyze the "${moduleName}" module. Return a JSON object with this exact structure:
315
+
316
+ {
317
+ "name": "${moduleName}",
318
+ "purpose": "One paragraph describing what this module does and why it exists",
319
+ "dataFlow": "How data flows through this module \u2014 inputs, transformations, outputs",
320
+ "publicApi": ["list", "of", "exported", "functions", "and", "types"],
321
+ "gotchas": ["Things that are surprising or could cause bugs"],
322
+ "dependencies": ["What this module depends on and why"]
323
+ }
324
+
325
+ Source files:
326
+
327
+ ${fileList}`;
328
+ }
329
+
330
+ // src/core/sessions.ts
331
+ async function writeSession(projectRoot, session) {
332
+ const dir = cortexPath(projectRoot, "sessions");
333
+ await ensureDir(dir);
334
+ const lines = [
335
+ `# Session: ${session.date}`,
336
+ "",
337
+ `**ID:** ${session.id}`,
338
+ session.previousSession ? `**Previous:** ${session.previousSession}` : null,
339
+ "",
340
+ `## Summary`,
341
+ session.summary,
342
+ "",
343
+ `## Files Changed`,
344
+ ...session.filesChanged.map((f) => `- \`${f}\``),
345
+ "",
346
+ `## Modules Affected`,
347
+ ...session.modulesAffected.map((m) => `- ${m}`)
348
+ ].filter(Boolean);
349
+ if (session.decisionsRecorded.length > 0) {
350
+ lines.push("", `## Decisions Recorded`, ...session.decisionsRecorded.map((d) => `- ${d}`));
351
+ }
352
+ await writeFile(cortexPath(projectRoot, "sessions", `${session.id}.md`), lines.join("\n") + "\n");
353
+ }
354
+ async function readSession(projectRoot, id) {
355
+ return readFile(cortexPath(projectRoot, "sessions", `${id}.md`));
356
+ }
357
+ async function listSessions(projectRoot) {
358
+ const dir = cortexPath(projectRoot, "sessions");
359
+ const files = await listFiles(dir, ".md");
360
+ return files.map((f) => (f.split("/").pop() || "").replace(".md", "")).sort().reverse();
361
+ }
362
+ async function getLatestSession(projectRoot) {
363
+ const sessions = await listSessions(projectRoot);
364
+ return sessions[0] ?? null;
365
+ }
366
+ function createSession(opts) {
367
+ const now = /* @__PURE__ */ new Date();
368
+ const id = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
369
+ return {
370
+ id,
371
+ date: now.toISOString(),
372
+ previousSession: opts.previousSession,
373
+ filesChanged: opts.filesChanged,
374
+ modulesAffected: opts.modulesAffected,
375
+ decisionsRecorded: opts.decisionsRecorded || [],
376
+ summary: opts.summary
377
+ };
378
+ }
379
+
380
+ // src/core/decisions.ts
381
+ async function readDecision(projectRoot, id) {
382
+ return readFile(cortexPath(projectRoot, "decisions", `${id}.md`));
383
+ }
384
+ async function writeDecision(projectRoot, decision) {
385
+ const dir = cortexPath(projectRoot, "decisions");
386
+ await ensureDir(dir);
387
+ const content = generateDecisionDoc(decision);
388
+ await writeFile(cortexPath(projectRoot, "decisions", `${decision.id}.md`), content);
389
+ }
390
+ async function listDecisions(projectRoot) {
391
+ const dir = cortexPath(projectRoot, "decisions");
392
+ const files = await listFiles(dir, ".md");
393
+ return files.map((f) => {
394
+ const name = f.split("/").pop() || "";
395
+ return name.replace(".md", "");
396
+ });
397
+ }
398
+ function createDecision(input) {
399
+ const id = input.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
400
+ return {
401
+ id,
402
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0] ?? "",
403
+ title: input.title,
404
+ context: input.context,
405
+ decision: input.decision,
406
+ alternatives: input.alternatives || [],
407
+ consequences: input.consequences || [],
408
+ status: "accepted"
409
+ };
410
+ }
411
+
412
+ // src/core/search.ts
413
+ import { readdir as readdir2 } from "fs/promises";
414
+ import { join as join2 } from "path";
415
+ import { existsSync as existsSync2 } from "fs";
416
+ async function searchKnowledge(projectRoot, query) {
417
+ const cortexRoot = cortexPath(projectRoot);
418
+ if (!existsSync2(cortexRoot)) return [];
419
+ const results = [];
420
+ const queryLower = query.toLowerCase();
421
+ const allFiles = await getAllCortexFiles(cortexRoot);
422
+ for (const filePath of allFiles) {
423
+ const content = await readFile(filePath);
424
+ if (!content) continue;
425
+ const lines = content.split("\n");
426
+ for (let i = 0; i < lines.length; i++) {
427
+ const line = lines[i];
428
+ if (line?.toLowerCase().includes(queryLower)) {
429
+ const start = Math.max(0, i - 2);
430
+ const end = Math.min(lines.length - 1, i + 2);
431
+ const context = lines.slice(start, end + 1).join("\n");
432
+ results.push({
433
+ file: filePath.replace(cortexRoot + "/", ""),
434
+ line: i + 1,
435
+ content: line.trim(),
436
+ context
437
+ });
438
+ }
439
+ }
440
+ }
441
+ return results;
442
+ }
443
+ async function getAllCortexFiles(dir) {
444
+ const files = [];
445
+ if (!existsSync2(dir)) return files;
446
+ const entries = await readdir2(dir, { withFileTypes: true });
447
+ for (const entry of entries) {
448
+ const fullPath = join2(dir, entry.name);
449
+ if (entry.isDirectory()) {
450
+ const subFiles = await getAllCortexFiles(fullPath);
451
+ files.push(...subFiles);
452
+ } else if (entry.isFile() && (entry.name.endsWith(".md") || entry.name.endsWith(".json") || entry.name.endsWith(".yaml"))) {
453
+ files.push(fullPath);
454
+ }
455
+ }
456
+ return files;
457
+ }
458
+
459
+ // src/mcp/tools/read.ts
460
+ function textResult(data) {
461
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
462
+ }
463
+ function registerReadTools(server, projectRoot) {
464
+ server.registerTool(
465
+ "get_project_overview",
466
+ {
467
+ description: "Get the full project overview: constitution (architecture, risk map, available knowledge), overview narrative, and dependency graph summary. This is the starting point for understanding any codebase. Always call this first.",
468
+ inputSchema: {}
469
+ },
470
+ async () => {
471
+ const constitution = await readFile(cortexPath(projectRoot, "constitution.md"));
472
+ const overview = await readFile(cortexPath(projectRoot, "overview.md"));
473
+ const manifest = await readManifest(projectRoot);
474
+ const graph = await readGraph(projectRoot);
475
+ let graphSummary = null;
476
+ if (graph) {
477
+ graphSummary = {
478
+ modules: graph.modules.length,
479
+ imports: graph.imports.length,
480
+ entryPoints: graph.entryPoints,
481
+ mostImported: getMostImportedFiles(graph, 5)
482
+ };
483
+ }
484
+ return textResult({
485
+ constitution,
486
+ overview,
487
+ manifest,
488
+ graphSummary
489
+ });
490
+ }
491
+ );
492
+ server.registerTool(
493
+ "get_module_context",
494
+ {
495
+ description: "Get deep context for a specific module: purpose, data flow, public API, gotchas, dependencies, and temporal signals (churn, coupling, bug history). Use after get_project_overview when you need to work on a specific module.",
496
+ inputSchema: {
497
+ name: z.string().describe('Module name (e.g., "scoring", "api", "indexer")')
498
+ }
499
+ },
500
+ async ({ name }) => {
501
+ const doc = await readModuleDoc(projectRoot, name);
502
+ if (!doc) {
503
+ const available = await listModuleDocs(projectRoot);
504
+ return textResult({ found: false, name, available, message: `Module "${name}" not found. Available modules: ${available.join(", ")}` });
505
+ }
506
+ const graph = await readGraph(projectRoot);
507
+ let deps = null;
508
+ if (graph) {
509
+ deps = getModuleDependencies(graph, name);
510
+ }
511
+ return textResult({ found: true, name, doc, dependencies: deps });
512
+ }
513
+ );
514
+ server.registerTool(
515
+ "get_session_briefing",
516
+ {
517
+ description: "Get a briefing of what changed since the last session. Shows files changed, modules affected, and decisions recorded. Use at the start of a new session to catch up.",
518
+ inputSchema: {}
519
+ },
520
+ async () => {
521
+ const latestId = await getLatestSession(projectRoot);
522
+ if (!latestId) {
523
+ return textResult({ hasSession: false, message: "No previous sessions recorded." });
524
+ }
525
+ const session = await readSession(projectRoot, latestId);
526
+ const allSessions = await listSessions(projectRoot);
527
+ return textResult({
528
+ hasSession: true,
529
+ latest: session,
530
+ totalSessions: allSessions.length,
531
+ recentSessionIds: allSessions.slice(0, 5)
532
+ });
533
+ }
534
+ );
535
+ server.registerTool(
536
+ "search_knowledge",
537
+ {
538
+ description: "Search across all CodeCortex knowledge files (modules, decisions, patterns, sessions, constitution) for a keyword or phrase. Returns matching lines with context.",
539
+ inputSchema: {
540
+ query: z.string().describe("Search term or phrase")
541
+ }
542
+ },
543
+ async ({ query }) => {
544
+ const results = await searchKnowledge(projectRoot, query);
545
+ return textResult({
546
+ query,
547
+ totalResults: results.length,
548
+ results: results.slice(0, 20)
549
+ });
550
+ }
551
+ );
552
+ server.registerTool(
553
+ "get_decision_history",
554
+ {
555
+ description: "Get architectural decision records. Shows WHY the codebase is built the way it is. Filter by topic keyword.",
556
+ inputSchema: {
557
+ topic: z.string().optional().describe("Optional keyword to filter decisions")
558
+ }
559
+ },
560
+ async ({ topic }) => {
561
+ const ids = await listDecisions(projectRoot);
562
+ const decisions = [];
563
+ for (const id of ids) {
564
+ const content = await readDecision(projectRoot, id);
565
+ if (content) {
566
+ if (!topic || content.toLowerCase().includes(topic.toLowerCase())) {
567
+ decisions.push(content);
568
+ }
569
+ }
570
+ }
571
+ return textResult({
572
+ total: decisions.length,
573
+ topic: topic || "all",
574
+ decisions
575
+ });
576
+ }
577
+ );
578
+ server.registerTool(
579
+ "get_dependency_graph",
580
+ {
581
+ description: "Get the import/export dependency graph. Shows which files import which, external dependencies, and entry points. Optionally filter to a specific file or module.",
582
+ inputSchema: {
583
+ file: z.string().optional().describe("Filter to edges involving this file path"),
584
+ module: z.string().optional().describe("Filter to edges involving this module name")
585
+ }
586
+ },
587
+ async ({ file, module }) => {
588
+ const graph = await readGraph(projectRoot);
589
+ if (!graph) return textResult({ found: false, message: "No graph data. Run codecortex init first." });
590
+ if (module) {
591
+ const deps = getModuleDependencies(graph, module);
592
+ return textResult({ module, ...deps });
593
+ }
594
+ if (file) {
595
+ const imports = graph.imports.filter((e) => e.source.includes(file) || e.target.includes(file));
596
+ const calls = graph.calls.filter((e) => e.file.includes(file));
597
+ return textResult({ file, imports, calls });
598
+ }
599
+ return textResult(graph);
600
+ }
601
+ );
602
+ server.registerTool(
603
+ "lookup_symbol",
604
+ {
605
+ description: "Look up a symbol (function, class, type, interface, const) by name. Returns file path, line numbers, signature, and whether it's exported. Use to find where something is defined.",
606
+ inputSchema: {
607
+ name: z.string().describe("Symbol name to search for"),
608
+ kind: z.enum(["function", "class", "interface", "type", "const", "enum", "method", "property", "variable"]).optional().describe("Filter by symbol kind"),
609
+ file: z.string().optional().describe("Filter by file path (partial match)")
610
+ }
611
+ },
612
+ async ({ name, kind, file }) => {
613
+ const content = await readFile(cortexPath(projectRoot, "symbols.json"));
614
+ if (!content) return textResult({ found: false, message: "No symbol index. Run codecortex init first." });
615
+ const index = JSON.parse(content);
616
+ let matches = index.symbols.filter(
617
+ (s) => s.name.toLowerCase().includes(name.toLowerCase())
618
+ );
619
+ if (kind) matches = matches.filter((s) => s.kind === kind);
620
+ if (file) matches = matches.filter((s) => s.file.includes(file));
621
+ return textResult({
622
+ query: { name, kind, file },
623
+ totalMatches: matches.length,
624
+ symbols: matches.slice(0, 30)
625
+ });
626
+ }
627
+ );
628
+ server.registerTool(
629
+ "get_change_coupling",
630
+ {
631
+ description: "CRITICAL: Call this BEFORE editing any file. Shows files that historically change together. If file A is coupled with file B, editing A without B will likely cause a bug. Hidden couplings (no import between files) are especially dangerous.",
632
+ inputSchema: {
633
+ file: z.string().optional().describe("Show coupling for this specific file"),
634
+ minStrength: z.number().min(0).max(1).default(0.3).describe("Minimum coupling strength (0-1). Default 0.3.")
635
+ }
636
+ },
637
+ async ({ file, minStrength }) => {
638
+ const content = await readFile(cortexPath(projectRoot, "temporal.json"));
639
+ if (!content) return textResult({ found: false, message: "No temporal data. Run codecortex init first." });
640
+ const temporal = JSON.parse(content);
641
+ let coupling = temporal.coupling.filter((c) => c.strength >= minStrength);
642
+ if (file) {
643
+ coupling = coupling.filter(
644
+ (c) => c.fileA.includes(file) || c.fileB.includes(file)
645
+ );
646
+ }
647
+ return textResult({
648
+ file: file || "all",
649
+ minStrength,
650
+ couplings: coupling,
651
+ warning: coupling.filter((c) => !c.hasImport).length > 0 ? "HIDDEN DEPENDENCIES FOUND \u2014 some coupled files have NO import between them" : null
652
+ });
653
+ }
654
+ );
655
+ server.registerTool(
656
+ "get_hotspots",
657
+ {
658
+ description: "Get files ranked by risk: change frequency (churn), coupling count, and bug history. Volatile files with many couplings need extra care when editing.",
659
+ inputSchema: {
660
+ limit: z.number().int().min(1).max(50).default(10).describe("Number of files to return")
661
+ }
662
+ },
663
+ async ({ limit }) => {
664
+ const content = await readFile(cortexPath(projectRoot, "temporal.json"));
665
+ if (!content) return textResult({ found: false, message: "No temporal data. Run codecortex init first." });
666
+ const temporal = JSON.parse(content);
667
+ const riskMap = /* @__PURE__ */ new Map();
668
+ for (const h of temporal.hotspots) {
669
+ riskMap.set(h.file, { churn: h.changes, couplings: 0, bugs: 0, risk: h.changes });
670
+ }
671
+ for (const c of temporal.coupling) {
672
+ for (const f of [c.fileA, c.fileB]) {
673
+ const entry = riskMap.get(f) || { churn: 0, couplings: 0, bugs: 0, risk: 0 };
674
+ entry.couplings++;
675
+ entry.risk += c.strength * 2;
676
+ riskMap.set(f, entry);
677
+ }
678
+ }
679
+ for (const b of temporal.bugHistory) {
680
+ const entry = riskMap.get(b.file) || { churn: 0, couplings: 0, bugs: 0, risk: 0 };
681
+ entry.bugs = b.fixCommits;
682
+ entry.risk += b.fixCommits * 3;
683
+ riskMap.set(b.file, entry);
684
+ }
685
+ const ranked = [...riskMap.entries()].sort((a, b) => b[1].risk - a[1].risk).slice(0, limit).map(([file, data]) => ({ file, ...data, risk: Math.round(data.risk * 100) / 100 }));
686
+ return textResult({
687
+ period: `${temporal.periodDays} days`,
688
+ totalCommits: temporal.totalCommits,
689
+ hotspots: ranked
690
+ });
691
+ }
692
+ );
693
+ }
694
+
695
+ // src/mcp/tools/write.ts
696
+ import { z as z3 } from "zod";
697
+
698
+ // src/types/schema.ts
699
+ import { z as z2 } from "zod";
700
+ var ModuleAnalysisSchema = z2.object({
701
+ name: z2.string(),
702
+ purpose: z2.string(),
703
+ dataFlow: z2.string(),
704
+ publicApi: z2.array(z2.string()),
705
+ gotchas: z2.array(z2.string()),
706
+ dependencies: z2.array(z2.string())
707
+ });
708
+ var DecisionInputSchema = z2.object({
709
+ title: z2.string(),
710
+ context: z2.string(),
711
+ decision: z2.string(),
712
+ alternatives: z2.array(z2.string()).default([]),
713
+ consequences: z2.array(z2.string()).default([])
714
+ });
715
+ var PatternInputSchema = z2.object({
716
+ name: z2.string(),
717
+ description: z2.string(),
718
+ example: z2.string(),
719
+ files: z2.array(z2.string()).default([])
720
+ });
721
+ var FeedbackInputSchema = z2.object({
722
+ file: z2.string(),
723
+ issue: z2.string(),
724
+ reporter: z2.string().default("agent")
725
+ });
726
+
727
+ // src/core/patterns.ts
728
+ async function readPatterns(projectRoot) {
729
+ return readFile(cortexPath(projectRoot, "patterns.md"));
730
+ }
731
+ async function addPattern(projectRoot, pattern) {
732
+ const existing = await readPatterns(projectRoot) || "# Coding Patterns\n";
733
+ const nameRegex = new RegExp(`### ${escapeRegex(pattern.name)}`, "i");
734
+ if (nameRegex.test(existing)) {
735
+ const entry2 = generatePatternEntry(pattern);
736
+ const sectionRegex = new RegExp(
737
+ `### ${escapeRegex(pattern.name)}[\\s\\S]*?(?=### |$)`,
738
+ "i"
739
+ );
740
+ const updated = existing.replace(sectionRegex, entry2 + "\n\n");
741
+ await writeFile(cortexPath(projectRoot, "patterns.md"), updated);
742
+ return "updated";
743
+ }
744
+ const entry = generatePatternEntry(pattern);
745
+ const content = existing.trimEnd() + "\n\n" + entry + "\n";
746
+ await writeFile(cortexPath(projectRoot, "patterns.md"), content);
747
+ return "added";
748
+ }
749
+ function escapeRegex(str) {
750
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
751
+ }
752
+
753
+ // src/mcp/tools/write.ts
754
+ import { readFile as readFile2 } from "fs/promises";
755
+ import { join as join3 } from "path";
756
+ function textResult2(data) {
757
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
758
+ }
759
+ function registerWriteTools(server, projectRoot) {
760
+ server.registerTool(
761
+ "analyze_module",
762
+ {
763
+ description: "Prepares a module for analysis. Returns the source files and a structured prompt. You should read the source files, analyze them, then call save_module_analysis with the result.",
764
+ inputSchema: {
765
+ name: z3.string().describe('Module name (e.g., "scoring", "api")')
766
+ }
767
+ },
768
+ async ({ name }) => {
769
+ const graph = await readGraph(projectRoot);
770
+ if (!graph) {
771
+ return textResult2({ error: "No graph data. Run codecortex init first." });
772
+ }
773
+ const module = graph.modules.find((m) => m.name === name);
774
+ if (!module) {
775
+ const available = graph.modules.map((m) => m.name);
776
+ return textResult2({ error: `Module "${name}" not found`, available });
777
+ }
778
+ const sourceFiles = [];
779
+ for (const filePath of module.files) {
780
+ try {
781
+ const content = await readFile2(join3(projectRoot, filePath), "utf-8");
782
+ sourceFiles.push({ path: filePath, content });
783
+ } catch {
784
+ }
785
+ }
786
+ const prompt = buildAnalysisPrompt(name, sourceFiles);
787
+ return textResult2({
788
+ module: name,
789
+ files: module.files,
790
+ prompt,
791
+ instruction: "Analyze the source files above and call save_module_analysis with the JSON result."
792
+ });
793
+ }
794
+ );
795
+ server.registerTool(
796
+ "save_module_analysis",
797
+ {
798
+ description: "Save the result of a module analysis. Provide the structured analysis (purpose, dataFlow, publicApi, gotchas, dependencies) and it will be persisted to modules/*.md.",
799
+ inputSchema: {
800
+ analysis: ModuleAnalysisSchema.describe("The structured module analysis")
801
+ }
802
+ },
803
+ async ({ analysis }) => {
804
+ const moduleAnalysis = {
805
+ ...analysis
806
+ };
807
+ const temporalContent = await readFile(cortexPath(projectRoot, "temporal.json"));
808
+ if (temporalContent) {
809
+ const temporal = JSON.parse(temporalContent);
810
+ const hotspot = temporal.hotspots?.find(
811
+ (h) => h.file.includes(`/${analysis.name}/`) || h.file.includes(`${analysis.name}.`)
812
+ );
813
+ const couplings = temporal.coupling?.filter(
814
+ (c) => c.fileA.includes(`/${analysis.name}/`) || c.fileB.includes(`/${analysis.name}/`)
815
+ ) || [];
816
+ const bugs = temporal.bugHistory?.filter(
817
+ (b) => b.file.includes(`/${analysis.name}/`)
818
+ ) || [];
819
+ if (hotspot || couplings.length > 0 || bugs.length > 0) {
820
+ moduleAnalysis.temporalSignals = {
821
+ churn: hotspot ? `${hotspot.changes} changes (${hotspot.stability})` : "unknown",
822
+ coupledWith: couplings.map((c) => {
823
+ const other = c.fileA.includes(`/${analysis.name}/`) ? c.fileB : c.fileA;
824
+ return `${other} (${c.cochanges} co-changes)`;
825
+ }),
826
+ stability: hotspot?.stability || "unknown",
827
+ bugHistory: bugs.flatMap((b) => b.lessons),
828
+ lastChanged: hotspot?.lastChanged || "unknown"
829
+ };
830
+ }
831
+ }
832
+ await writeModuleDoc(projectRoot, moduleAnalysis);
833
+ return textResult2({
834
+ saved: true,
835
+ module: analysis.name,
836
+ path: `.codecortex/modules/${analysis.name}.md`
837
+ });
838
+ }
839
+ );
840
+ server.registerTool(
841
+ "record_decision",
842
+ {
843
+ description: "Record an architectural decision. Documents WHY something is built a certain way, what alternatives were considered, and consequences. Use whenever a non-obvious technical choice is made.",
844
+ inputSchema: {
845
+ title: z3.string().describe('Decision title (e.g., "Use tree-sitter for parsing")'),
846
+ context: z3.string().describe("What situation led to this decision"),
847
+ decision: z3.string().describe("What was decided"),
848
+ alternatives: z3.array(z3.string()).default([]).describe("What other options were considered"),
849
+ consequences: z3.array(z3.string()).default([]).describe("Expected consequences of this decision")
850
+ }
851
+ },
852
+ async ({ title, context, decision, alternatives, consequences }) => {
853
+ const record = createDecision({ title, context, decision, alternatives, consequences });
854
+ await writeDecision(projectRoot, record);
855
+ return textResult2({
856
+ recorded: true,
857
+ id: record.id,
858
+ path: `.codecortex/decisions/${record.id}.md`
859
+ });
860
+ }
861
+ );
862
+ server.registerTool(
863
+ "update_patterns",
864
+ {
865
+ description: 'Add or update a coding pattern. Patterns document HOW code is written in this project (naming conventions, error handling, testing approaches). Returns "added", "updated", or "noop".',
866
+ inputSchema: {
867
+ name: z3.string().describe('Pattern name (e.g., "Error handling in API routes")'),
868
+ description: z3.string().describe("What the pattern is and when to use it"),
869
+ example: z3.string().describe("Code example showing the pattern"),
870
+ files: z3.array(z3.string()).default([]).describe("Files where this pattern is used")
871
+ }
872
+ },
873
+ async ({ name, description, example, files }) => {
874
+ const result = await addPattern(projectRoot, { name, description, example, files });
875
+ return textResult2({
876
+ action: result,
877
+ pattern: name,
878
+ path: ".codecortex/patterns.md"
879
+ });
880
+ }
881
+ );
882
+ server.registerTool(
883
+ "report_feedback",
884
+ {
885
+ description: "Report incorrect or outdated knowledge. If you discover that a module doc, decision, or pattern is wrong, report it here. The feedback will be stored and used in the next analysis cycle.",
886
+ inputSchema: {
887
+ file: z3.string().describe('Which knowledge file is incorrect (e.g., "modules/scoring.md")'),
888
+ issue: z3.string().describe("What is wrong or outdated"),
889
+ reporter: z3.string().default("agent").describe("Who is reporting (default: agent)")
890
+ }
891
+ },
892
+ async ({ file, issue, reporter }) => {
893
+ const dir = cortexPath(projectRoot, "feedback");
894
+ await ensureDir(dir);
895
+ const entry = {
896
+ date: (/* @__PURE__ */ new Date()).toISOString(),
897
+ file,
898
+ issue,
899
+ reporter
900
+ };
901
+ const feedbackPath = cortexPath(projectRoot, "feedback", "log.json");
902
+ const existing = await readFile(feedbackPath);
903
+ const entries = existing ? JSON.parse(existing) : [];
904
+ entries.push(entry);
905
+ await writeFile(feedbackPath, JSON.stringify(entries, null, 2));
906
+ return textResult2({
907
+ recorded: true,
908
+ totalFeedback: entries.length,
909
+ message: "Feedback recorded. Will be addressed in next codecortex update."
910
+ });
911
+ }
912
+ );
913
+ }
914
+
915
+ // src/mcp/server.ts
916
+ function createServer(projectRoot) {
917
+ const server = new McpServer({
918
+ name: "codecortex",
919
+ version: "0.1.0",
920
+ description: "Persistent codebase knowledge layer. Pre-digested architecture, symbols, coupling, and patterns served to AI agents."
921
+ });
922
+ registerReadTools(server, projectRoot);
923
+ registerWriteTools(server, projectRoot);
924
+ return server;
925
+ }
926
+ async function startServer(projectRoot) {
927
+ const server = createServer(projectRoot);
928
+ const transport = new StdioServerTransport();
929
+ await server.connect(transport);
930
+ console.error(`CodeCortex MCP server running on stdio (root: ${projectRoot})`);
931
+ }
932
+ if (import.meta.url === `file://${process.argv[1]}`) {
933
+ const root = process.cwd();
934
+ startServer(root).catch((err) => {
935
+ console.error("Fatal error:", err);
936
+ process.exit(1);
937
+ });
938
+ }
939
+
940
+ export {
941
+ readFile,
942
+ writeFile,
943
+ ensureDir,
944
+ cortexPath,
945
+ writeJsonStream,
946
+ readManifest,
947
+ writeManifest,
948
+ createManifest,
949
+ updateManifest,
950
+ writeGraph,
951
+ buildGraph,
952
+ enrichCouplingWithImports,
953
+ listModuleDocs,
954
+ listDecisions,
955
+ writeSession,
956
+ listSessions,
957
+ getLatestSession,
958
+ createSession,
959
+ createServer,
960
+ startServer
961
+ };
962
+ //# sourceMappingURL=chunk-F4WTE7R3.js.map