codecortex-ai 0.4.3 → 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
@@ -2,8 +2,8 @@
2
2
 
3
3
  Persistent codebase knowledge layer for AI agents. Your AI shouldn't re-learn your codebase every session.
4
4
 
5
- > **⚠️ If you're on v0.3.x or earlier, update now:** `npm install -g codecortex-ai@latest`
6
- > v0.4.2 fixes broken installs, adds auto-update notifications, git hooks, and grouped CLI help.
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.
7
7
 
8
8
  [Website](https://codecortex-ai.vercel.app) · [npm](https://www.npmjs.com/package/codecortex-ai) · [GitHub](https://github.com/rushikeshmore/CodeCortex)
9
9
 
@@ -98,9 +98,9 @@ Example from a real codebase:
98
98
  - `routes.ts` and `worker.ts` co-changed in 9/12 commits (75%) with **zero imports between them**
99
99
  - Without this knowledge, an AI editing one file would produce a bug 75% of the time
100
100
 
101
- ## MCP Tools (14)
101
+ ## MCP Tools (15)
102
102
 
103
- ### Read Tools (9)
103
+ ### Read Tools (10)
104
104
 
105
105
  | Tool | Description |
106
106
  |------|-------------|
@@ -113,6 +113,9 @@ Example from a real codebase:
113
113
  | `lookup_symbol` | Symbol by name/file/kind |
114
114
  | `get_change_coupling` | What files must I also edit if I touch X? |
115
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.
116
119
 
117
120
  ### Write Tools (5)
118
121
 
@@ -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.3",
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-CR6LARZ4.js.map
1224
+ //# sourceMappingURL=chunk-4N3T43UG.js.map