codecortex-ai 0.4.2 → 0.4.4

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 CHANGED
@@ -1,6 +1,9 @@
1
1
  # CodeCortex
2
2
 
3
- > Persistent codebase knowledge layer for AI agents. Your AI shouldn't re-learn your codebase every session.
3
+ Persistent codebase knowledge layer for AI agents. Your AI shouldn't re-learn your codebase every session.
4
+
5
+ > **⚠️ If you're on v0.4.3 or earlier, update now:** `npm install -g codecortex-ai@latest`
6
+ > v0.4.4 adds freshness flags on all MCP responses and `get_edit_briefing` — a pre-edit risk briefing tool.
4
7
 
5
8
  [Website](https://codecortex-ai.vercel.app) · [npm](https://www.npmjs.com/package/codecortex-ai) · [GitHub](https://github.com/rushikeshmore/CodeCortex)
6
9
 
@@ -13,7 +16,7 @@
13
16
  Every AI coding session starts from scratch. When context compacts or a new session begins, the AI re-scans the entire codebase. Same files, same tokens, same wasted time. It's like hiring a new developer every session who has to re-learn everything before writing a single line.
14
17
 
15
18
  **The data backs this up:**
16
- - AI agents increase defect risk by 30% on unfamiliar code ([CodeScene + Lund University, 2025](https://codescene.com/hubfs/whitepapers/AI-Coding-Assistants-and-Code-Quality.pdf))
19
+ - AI agents increase defect risk by 30% on unfamiliar code ([CodeScene + Lund University, 2025](https://codescene.com/hubfs/whitepapers/AI-Ready-Code-How-Code-Health-Determines-AI-Performance.pdf))
17
20
  - Code churn grew 2.5x in the AI era ([GitClear, 211M lines analyzed](https://www.gitclear.com/coding_on_copilot_data_shows_ais_downward_pressure_on_code_quality))
18
21
  - Nobody combines structural + semantic + temporal + decision knowledge in one portable tool
19
22
 
@@ -25,9 +28,11 @@ CodeCortex pre-digests codebases into layered knowledge files and serves them to
25
28
 
26
29
  ## Quick Start
27
30
 
31
+ > **Requires Node 20 or 22.** Node 24 is not yet supported (tree-sitter native bindings need an upstream update).
32
+
28
33
  ```bash
29
- # Install
30
- npm install -g codecortex-ai
34
+ # Install (--legacy-peer-deps needed for tree-sitter peer dep mismatches)
35
+ npm install -g codecortex-ai --legacy-peer-deps
31
36
 
32
37
  # Initialize knowledge for your project
33
38
  cd /path/to/your-project
@@ -93,9 +98,9 @@ Example from a real codebase:
93
98
  - `routes.ts` and `worker.ts` co-changed in 9/12 commits (75%) with **zero imports between them**
94
99
  - Without this knowledge, an AI editing one file would produce a bug 75% of the time
95
100
 
96
- ## MCP Tools (14)
101
+ ## MCP Tools (15)
97
102
 
98
- ### Read Tools (9)
103
+ ### Read Tools (10)
99
104
 
100
105
  | Tool | Description |
101
106
  |------|-------------|
@@ -108,6 +113,9 @@ Example from a real codebase:
108
113
  | `lookup_symbol` | Symbol by name/file/kind |
109
114
  | `get_change_coupling` | What files must I also edit if I touch X? |
110
115
  | `get_hotspots` | Files ranked by risk (churn x coupling) |
116
+ | `get_edit_briefing` | **NEW** — Pre-edit risk briefing: co-change warnings, hidden deps, bug history, importers |
117
+
118
+ All read tools include `_freshness` metadata indicating how up-to-date the knowledge is.
111
119
 
112
120
  ### Write Tools (5)
113
121
 
@@ -148,7 +156,7 @@ vs. raw scan of entire codebase: ~37,800 tokens
148
156
 
149
157
  85-90% token reduction. 7-10x efficiency gain.
150
158
 
151
- ## Supported Languages (28)
159
+ ## Supported Languages (27)
152
160
 
153
161
  | Category | Languages |
154
162
  |----------|-----------|
@@ -172,4 +180,4 @@ vs. raw scan of entire codebase: ~37,800 tokens
172
180
 
173
181
  ## License
174
182
 
175
- MIT
183
+ MIT
@@ -197,6 +197,9 @@ function getModuleDependencies(graph, moduleName) {
197
197
  calls: graph.calls.filter((e) => moduleFiles.has(e.file))
198
198
  };
199
199
  }
200
+ function getFileImporters(graph, file) {
201
+ return graph.imports.filter((e) => e.target.includes(file)).map((e) => e.source);
202
+ }
200
203
  function getMostImportedFiles(graph, limit = 10) {
201
204
  const counts = /* @__PURE__ */ new Map();
202
205
  for (const edge of graph.imports) {
@@ -212,6 +215,9 @@ function enrichCouplingWithImports(graph, coupling) {
212
215
  for (const pair of coupling) {
213
216
  const key = [pair.fileA, pair.fileB].sort().join("|");
214
217
  pair.hasImport = importPairs.has(key);
218
+ if (pair.strength >= 0.7 && !pair.hasImport) {
219
+ pair.warning = `HIDDEN DEPENDENCY \u2014 ${Math.round(pair.strength * 100)}% co-change rate`;
220
+ }
215
221
  }
216
222
  }
217
223
 
@@ -456,11 +462,159 @@ async function getAllCortexFiles(dir) {
456
462
  return files;
457
463
  }
458
464
 
465
+ // src/git/diff.ts
466
+ import simpleGit from "simple-git";
467
+ async function getUncommittedDiff(root) {
468
+ const git = simpleGit(root);
469
+ const diff = await git.diffSummary();
470
+ return {
471
+ filesChanged: diff.files.map((f) => f.file),
472
+ insertions: diff.insertions,
473
+ deletions: diff.deletions,
474
+ summary: `${diff.files.length} files changed, +${diff.insertions} -${diff.deletions}`
475
+ };
476
+ }
477
+ async function getChangedFilesSinceDate(root, sinceDate) {
478
+ const git = simpleGit(root);
479
+ const log = await git.log({ "--since": sinceDate, "--name-only": null });
480
+ const files = /* @__PURE__ */ new Set();
481
+ for (const commit of log.all) {
482
+ const diff = commit.diff;
483
+ if (diff) {
484
+ for (const file of diff.files) {
485
+ files.add(file.file);
486
+ }
487
+ }
488
+ }
489
+ return [...files];
490
+ }
491
+ function mapFilesToModules(files) {
492
+ const moduleMap = /* @__PURE__ */ new Map();
493
+ for (const file of files) {
494
+ const parts = file.split("/");
495
+ let module = "root";
496
+ if (parts[0] === "src" && parts.length >= 3 && parts[1]) {
497
+ module = parts[1];
498
+ } else if (parts[0] === "lib" && parts.length >= 3 && parts[1]) {
499
+ module = parts[1];
500
+ }
501
+ const existing = moduleMap.get(module) || [];
502
+ existing.push(file);
503
+ moduleMap.set(module, existing);
504
+ }
505
+ return moduleMap;
506
+ }
507
+
508
+ // src/git/history.ts
509
+ import simpleGit2 from "simple-git";
510
+ async function getCommitHistory(root, days = 90) {
511
+ const git = simpleGit2(root);
512
+ const since = new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString().split("T")[0];
513
+ const log = await git.log({
514
+ "--since": since,
515
+ "--stat": null,
516
+ maxCount: 500
517
+ });
518
+ return log.all.map((commit) => ({
519
+ hash: commit.hash,
520
+ date: commit.date,
521
+ message: commit.message,
522
+ author: commit.author_name,
523
+ filesChanged: parseStatFiles(commit.diff)
524
+ }));
525
+ }
526
+ function parseStatFiles(diff) {
527
+ if (!diff || !diff.files) return [];
528
+ return diff.files.map((f) => f.file);
529
+ }
530
+ async function isGitRepo(root) {
531
+ const git = simpleGit2(root);
532
+ try {
533
+ await git.status();
534
+ return true;
535
+ } catch {
536
+ return false;
537
+ }
538
+ }
539
+ async function getHeadCommit(root) {
540
+ const git = simpleGit2(root);
541
+ try {
542
+ const log = await git.log({ maxCount: 1 });
543
+ return log.latest?.hash || null;
544
+ } catch {
545
+ return null;
546
+ }
547
+ }
548
+
549
+ // src/core/freshness.ts
550
+ async function computeFreshness(projectRoot) {
551
+ const manifest = await readManifest(projectRoot);
552
+ if (!manifest) return null;
553
+ const lastUpdated = manifest.lastUpdated;
554
+ if (!lastUpdated) return null;
555
+ const isRepo = await isGitRepo(projectRoot);
556
+ let changedFiles = [];
557
+ if (isRepo) {
558
+ try {
559
+ changedFiles = await getChangedFilesSinceDate(projectRoot, lastUpdated);
560
+ changedFiles = changedFiles.filter((f) => {
561
+ const base = f.split("/").pop() ?? "";
562
+ return !base.endsWith(".lock") && base !== "package-lock.json" && base !== "yarn.lock" && base !== "pnpm-lock.yaml" && base !== "CHANGELOG.md" && !f.startsWith(".codecortex/");
563
+ });
564
+ } catch {
565
+ changedFiles = [];
566
+ }
567
+ }
568
+ const now = /* @__PURE__ */ new Date();
569
+ const analyzed = new Date(lastUpdated);
570
+ const daysSinceAnalysis = Math.floor((now.getTime() - analyzed.getTime()) / (1e3 * 60 * 60 * 24));
571
+ const count = changedFiles.length;
572
+ let status;
573
+ let message;
574
+ if (count === 0 && daysSinceAnalysis <= 7) {
575
+ status = "fresh";
576
+ message = "Knowledge is up to date.";
577
+ } else if (count <= 2 && daysSinceAnalysis <= 7) {
578
+ status = "slightly_stale";
579
+ message = `${count} file${count === 1 ? "" : "s"} changed since last analysis. Minor drift.`;
580
+ } else if (count <= 5 || daysSinceAnalysis <= 14) {
581
+ status = "stale";
582
+ message = `${count} file${count === 1 ? "" : "s"} changed since last analysis. Consider running \`codecortex update\`.`;
583
+ } else {
584
+ status = "very_stale";
585
+ message = `${count} files changed over ${daysSinceAnalysis} days since last analysis. Run \`codecortex update\` before trusting this knowledge.`;
586
+ }
587
+ return {
588
+ status,
589
+ lastAnalyzed: lastUpdated,
590
+ daysSinceAnalysis,
591
+ filesChangedSince: count,
592
+ changedFiles: changedFiles.slice(0, 20),
593
+ // Cap to avoid bloating response
594
+ message
595
+ };
596
+ }
597
+
459
598
  // src/mcp/tools/read.ts
460
599
  function textResult(data) {
461
600
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
462
601
  }
602
+ function withFreshness(data, freshness) {
603
+ if (!freshness) return data;
604
+ return { ...data, _freshness: freshness };
605
+ }
463
606
  function registerReadTools(server, projectRoot) {
607
+ let cachedFreshness = null;
608
+ const FRESHNESS_TTL_MS = 6e4;
609
+ async function getFreshness() {
610
+ const now = Date.now();
611
+ if (cachedFreshness && now - cachedFreshness.timestamp < FRESHNESS_TTL_MS) {
612
+ return cachedFreshness.info;
613
+ }
614
+ const info = await computeFreshness(projectRoot);
615
+ cachedFreshness = { info, timestamp: now };
616
+ return info;
617
+ }
464
618
  server.registerTool(
465
619
  "get_project_overview",
466
620
  {
@@ -481,12 +635,13 @@ function registerReadTools(server, projectRoot) {
481
635
  mostImported: getMostImportedFiles(graph, 5)
482
636
  };
483
637
  }
484
- return textResult({
638
+ const freshness = await getFreshness();
639
+ return textResult(withFreshness({
485
640
  constitution,
486
641
  overview,
487
642
  manifest,
488
643
  graphSummary
489
- });
644
+ }, freshness));
490
645
  }
491
646
  );
492
647
  server.registerTool(
@@ -508,7 +663,8 @@ function registerReadTools(server, projectRoot) {
508
663
  if (graph) {
509
664
  deps = getModuleDependencies(graph, name);
510
665
  }
511
- return textResult({ found: true, name, doc, dependencies: deps });
666
+ const freshness = await getFreshness();
667
+ return textResult(withFreshness({ found: true, name, doc, dependencies: deps }, freshness));
512
668
  }
513
669
  );
514
670
  server.registerTool(
@@ -524,12 +680,13 @@ function registerReadTools(server, projectRoot) {
524
680
  }
525
681
  const session = await readSession(projectRoot, latestId);
526
682
  const allSessions = await listSessions(projectRoot);
527
- return textResult({
683
+ const freshness = await getFreshness();
684
+ return textResult(withFreshness({
528
685
  hasSession: true,
529
686
  latest: session,
530
687
  totalSessions: allSessions.length,
531
688
  recentSessionIds: allSessions.slice(0, 5)
532
- });
689
+ }, freshness));
533
690
  }
534
691
  );
535
692
  server.registerTool(
@@ -542,11 +699,12 @@ function registerReadTools(server, projectRoot) {
542
699
  },
543
700
  async ({ query }) => {
544
701
  const results = await searchKnowledge(projectRoot, query);
545
- return textResult({
702
+ const freshness = await getFreshness();
703
+ return textResult(withFreshness({
546
704
  query,
547
705
  totalResults: results.length,
548
706
  results: results.slice(0, 20)
549
- });
707
+ }, freshness));
550
708
  }
551
709
  );
552
710
  server.registerTool(
@@ -568,11 +726,12 @@ function registerReadTools(server, projectRoot) {
568
726
  }
569
727
  }
570
728
  }
571
- return textResult({
729
+ const freshness = await getFreshness();
730
+ return textResult(withFreshness({
572
731
  total: decisions.length,
573
732
  topic: topic || "all",
574
733
  decisions
575
- });
734
+ }, freshness));
576
735
  }
577
736
  );
578
737
  server.registerTool(
@@ -587,16 +746,17 @@ function registerReadTools(server, projectRoot) {
587
746
  async ({ file, module }) => {
588
747
  const graph = await readGraph(projectRoot);
589
748
  if (!graph) return textResult({ found: false, message: "No graph data. Run codecortex init first." });
749
+ const freshness = await getFreshness();
590
750
  if (module) {
591
751
  const deps = getModuleDependencies(graph, module);
592
- return textResult({ module, ...deps });
752
+ return textResult(withFreshness({ module, ...deps }, freshness));
593
753
  }
594
754
  if (file) {
595
755
  const imports = graph.imports.filter((e) => e.source.includes(file) || e.target.includes(file));
596
756
  const calls = graph.calls.filter((e) => e.file.includes(file));
597
- return textResult({ file, imports, calls });
757
+ return textResult(withFreshness({ file, imports, calls }, freshness));
598
758
  }
599
- return textResult(graph);
759
+ return textResult(withFreshness(graph, freshness));
600
760
  }
601
761
  );
602
762
  server.registerTool(
@@ -618,11 +778,12 @@ function registerReadTools(server, projectRoot) {
618
778
  );
619
779
  if (kind) matches = matches.filter((s) => s.kind === kind);
620
780
  if (file) matches = matches.filter((s) => s.file.includes(file));
621
- return textResult({
781
+ const freshness = await getFreshness();
782
+ return textResult(withFreshness({
622
783
  query: { name, kind, file },
623
784
  totalMatches: matches.length,
624
785
  symbols: matches.slice(0, 30)
625
- });
786
+ }, freshness));
626
787
  }
627
788
  );
628
789
  server.registerTool(
@@ -644,12 +805,13 @@ function registerReadTools(server, projectRoot) {
644
805
  (c) => c.fileA.includes(file) || c.fileB.includes(file)
645
806
  );
646
807
  }
647
- return textResult({
808
+ const freshness = await getFreshness();
809
+ return textResult(withFreshness({
648
810
  file: file || "all",
649
811
  minStrength,
650
812
  couplings: coupling,
651
813
  warning: coupling.filter((c) => !c.hasImport).length > 0 ? "HIDDEN DEPENDENCIES FOUND \u2014 some coupled files have NO import between them" : null
652
- });
814
+ }, freshness));
653
815
  }
654
816
  );
655
817
  server.registerTool(
@@ -683,14 +845,104 @@ function registerReadTools(server, projectRoot) {
683
845
  riskMap.set(b.file, entry);
684
846
  }
685
847
  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({
848
+ const freshness = await getFreshness();
849
+ return textResult(withFreshness({
687
850
  period: `${temporal.periodDays} days`,
688
851
  totalCommits: temporal.totalCommits,
689
852
  hotspots: ranked
853
+ }, freshness));
854
+ }
855
+ );
856
+ server.registerTool(
857
+ "get_edit_briefing",
858
+ {
859
+ description: "CALL THIS BEFORE EDITING FILES. Takes a list of files you plan to edit and returns everything you need to know: co-change warnings (files you must also update), risk assessment, who imports these files, relevant patterns, and recent change history. Prevents bugs from hidden dependencies.",
860
+ inputSchema: {
861
+ files: z.array(z.string()).min(1).describe("File paths you plan to edit (relative to project root)")
862
+ }
863
+ },
864
+ async ({ files }) => {
865
+ const temporalContent = await readFile(cortexPath(projectRoot, "temporal.json"));
866
+ if (!temporalContent) return textResult({ found: false, message: "No temporal data. Run codecortex init first." });
867
+ const temporal = JSON.parse(temporalContent);
868
+ const graph = await readGraph(projectRoot);
869
+ const patternsContent = await readFile(cortexPath(projectRoot, "patterns.md"));
870
+ const briefings = files.map((file) => {
871
+ const couplings = temporal.coupling.filter((c) => c.fileA.includes(file) || c.fileB.includes(file)).map((c) => {
872
+ const other = c.fileA.includes(file) ? c.fileB : c.fileA;
873
+ return {
874
+ file: other,
875
+ cochanges: c.cochanges,
876
+ strength: c.strength,
877
+ hasImport: c.hasImport,
878
+ warning: c.warning || null
879
+ };
880
+ }).sort((a, b) => b.strength - a.strength);
881
+ const hotspot = temporal.hotspots.find((h) => h.file.includes(file));
882
+ const bugs = temporal.bugHistory.find((b) => b.file.includes(file));
883
+ const couplingCount = couplings.length;
884
+ const hiddenDeps = couplings.filter((c) => !c.hasImport).length;
885
+ let riskLevel = "LOW";
886
+ const riskScore = (hotspot?.changes || 0) + couplingCount * 2 + (bugs?.fixCommits || 0) * 3 + hiddenDeps * 4;
887
+ if (riskScore >= 20) riskLevel = "CRITICAL";
888
+ else if (riskScore >= 12) riskLevel = "HIGH";
889
+ else if (riskScore >= 6) riskLevel = "MEDIUM";
890
+ let importedBy = [];
891
+ if (graph) {
892
+ importedBy = getFileImporters(graph, file);
893
+ }
894
+ const recentChange = hotspot ? {
895
+ lastChanged: hotspot.lastChanged,
896
+ daysSinceChange: hotspot.daysSinceChange,
897
+ totalChanges: hotspot.changes,
898
+ stability: hotspot.stability
899
+ } : null;
900
+ const bugHistory = bugs ? {
901
+ fixCommits: bugs.fixCommits,
902
+ lessons: bugs.lessons
903
+ } : null;
904
+ return {
905
+ file,
906
+ risk: {
907
+ level: riskLevel,
908
+ score: Math.round(riskScore * 100) / 100,
909
+ reason: buildRiskReason(riskLevel, hotspot, couplingCount, hiddenDeps, bugs)
910
+ },
911
+ cochangeWarnings: couplings,
912
+ importedBy,
913
+ recentChange,
914
+ bugHistory
915
+ };
690
916
  });
917
+ const inputSet = new Set(files);
918
+ const alsoConsider = /* @__PURE__ */ new Set();
919
+ for (const b of briefings) {
920
+ for (const c of b.cochangeWarnings) {
921
+ const coupledFile = c.file;
922
+ const isInInput = files.some((f) => coupledFile.includes(f) || f.includes(coupledFile));
923
+ if (!isInInput && c.strength >= 0.5) {
924
+ alsoConsider.add(`${coupledFile} (${Math.round(c.strength * 100)}% co-change with ${b.file}${c.hasImport ? "" : ", NO import \u2014 hidden dep"})`);
925
+ }
926
+ }
927
+ }
928
+ const freshness = await getFreshness();
929
+ return textResult(withFreshness({
930
+ briefings,
931
+ alsoConsiderEditing: [...alsoConsider],
932
+ patterns: patternsContent || null
933
+ }, freshness));
691
934
  }
692
935
  );
693
936
  }
937
+ function buildRiskReason(level, hotspot, couplings, hiddenDeps, bugs) {
938
+ if (level === "LOW") return "Low change frequency, few couplings.";
939
+ const parts = [];
940
+ if (hotspot && hotspot.changes >= 5) parts.push(`${hotspot.changes} changes (${hotspot.stability})`);
941
+ if (couplings > 0) parts.push(`${couplings} coupled file${couplings === 1 ? "" : "s"}`);
942
+ if (hiddenDeps > 0) parts.push(`${hiddenDeps} hidden dep${hiddenDeps === 1 ? "" : "s"}`);
943
+ if (bugs && bugs.fixCommits > 0) parts.push(`${bugs.fixCommits} bug-fix commit${bugs.fixCommits === 1 ? "" : "s"}`);
944
+ return parts.join(", ") + ".";
945
+ }
694
946
 
695
947
  // src/mcp/tools/write.ts
696
948
  import { z as z3 } from "zod";
@@ -916,7 +1168,7 @@ function registerWriteTools(server, projectRoot) {
916
1168
  function createServer(projectRoot) {
917
1169
  const server = new McpServer({
918
1170
  name: "codecortex",
919
- version: "0.4.2",
1171
+ version: "0.4.4",
920
1172
  description: "Persistent codebase knowledge layer. Pre-digested architecture, symbols, coupling, and patterns served to AI agents."
921
1173
  });
922
1174
  registerReadTools(server, projectRoot);
@@ -956,12 +1208,17 @@ export {
956
1208
  writeModuleDoc,
957
1209
  listModuleDocs,
958
1210
  listDecisions,
1211
+ getCommitHistory,
1212
+ isGitRepo,
1213
+ getHeadCommit,
959
1214
  writeSession,
960
1215
  listSessions,
961
1216
  getLatestSession,
962
1217
  createSession,
963
1218
  searchKnowledge,
1219
+ getUncommittedDiff,
1220
+ mapFilesToModules,
964
1221
  createServer,
965
1222
  startServer
966
1223
  };
967
- //# sourceMappingURL=chunk-PBYRGMCK.js.map
1224
+ //# sourceMappingURL=chunk-4N3T43UG.js.map