@kage-core/kage-graph-mcp 1.1.24 → 1.1.25

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/dist/kernel.js CHANGED
@@ -460,6 +460,12 @@ function estimateTokens(text) {
460
460
  function packetText(packet) {
461
461
  return `${packet.title}\n${packet.summary}\n${packet.body}\n${packet.type}\n${packet.tags.join(" ")}\n${packet.paths.join(" ")}`;
462
462
  }
463
+ function isGeneratedChangeMemory(packet) {
464
+ return packet.type === "workflow"
465
+ && packet.tags.includes("change-memory")
466
+ && packet.tags.includes("diff-proposal")
467
+ && packet.source_refs.some((ref) => ref.kind === "git_diff");
468
+ }
463
469
  function tokenSet(text) {
464
470
  return new Set(tokenize(text).filter((term) => term.length > 2));
465
471
  }
@@ -476,6 +482,7 @@ function duplicateCandidates(projectDir, packet, threshold = 0.58) {
476
482
  const current = tokenSet(packetText(packet));
477
483
  return [...loadApprovedPackets(projectDir), ...loadPendingPackets(projectDir)]
478
484
  .filter((candidate) => candidate.id !== packet.id)
485
+ .filter((candidate) => !(isGeneratedChangeMemory(packet) && isGeneratedChangeMemory(candidate)))
479
486
  .map((candidate) => ({ packet: candidate, score: jaccard(current, tokenSet(packetText(candidate))) }))
480
487
  .filter((entry) => entry.score >= threshold)
481
488
  .sort((a, b) => b.score - a.score || a.packet.title.localeCompare(b.packet.title))
@@ -888,6 +895,30 @@ function readGit(projectDir, args) {
888
895
  return null;
889
896
  }
890
897
  }
898
+ function safeStat(path) {
899
+ try {
900
+ return (0, node_fs_1.statSync)(path);
901
+ }
902
+ catch {
903
+ return null;
904
+ }
905
+ }
906
+ function safeLstat(path) {
907
+ try {
908
+ return (0, node_fs_1.lstatSync)(path);
909
+ }
910
+ catch {
911
+ return null;
912
+ }
913
+ }
914
+ function safeReadText(path) {
915
+ try {
916
+ return (0, node_fs_1.readFileSync)(path, "utf8");
917
+ }
918
+ catch {
919
+ return null;
920
+ }
921
+ }
891
922
  function gitBranch(projectDir) {
892
923
  return readGit(projectDir, ["branch", "--show-current"]) || readGit(projectDir, ["rev-parse", "--short", "HEAD"]);
893
924
  }
@@ -901,6 +932,12 @@ function gitMergeBase(projectDir) {
901
932
  return readGit(projectDir, ["merge-base", "HEAD", "origin/main"])
902
933
  || readGit(projectDir, ["merge-base", "HEAD", "origin/master"]);
903
934
  }
935
+ function gitProjectPrefix(projectDir) {
936
+ const prefix = readGit(projectDir, ["rev-parse", "--show-prefix"]);
937
+ if (prefix === null)
938
+ return null;
939
+ return prefix.replace(/\\/g, "/").replace(/\/+$/, "");
940
+ }
904
941
  // Directories that are never meaningful in change-memory packets.
905
942
  // These are typically generated, vendored, or ephemeral — any project can
906
943
  // accumulate thousands of files here that bury real signal.
@@ -934,12 +971,26 @@ function isNoisePath(filePath) {
934
971
  return false;
935
972
  return NOISE_PATH_PREFIXES.some((prefix) => filePath.startsWith(prefix));
936
973
  }
937
- function parsePorcelainStatus(status) {
974
+ function gitPathToProjectRelative(projectDir, path) {
975
+ const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "");
976
+ const projectPrefix = gitProjectPrefix(projectDir);
977
+ if (projectPrefix === null || projectPrefix === "")
978
+ return normalized;
979
+ if (normalized === projectPrefix)
980
+ return "";
981
+ const prefix = `${projectPrefix}/`;
982
+ if (!normalized.startsWith(prefix)) {
983
+ return (0, node_fs_1.existsSync)((0, node_path_1.join)(projectDir, normalized)) ? normalized : null;
984
+ }
985
+ return normalized.slice(prefix.length);
986
+ }
987
+ function parsePorcelainStatus(projectDir, status) {
938
988
  return unique(status
939
989
  .split(/\r?\n/)
940
990
  .map(parsePorcelainPath)
941
991
  .map((path) => path.replace(/^.* -> /, ""))
942
- .filter(Boolean)
992
+ .map((path) => gitPathToProjectRelative(projectDir, path))
993
+ .filter((path) => Boolean(path))
943
994
  .filter((path) => !shouldSkipRepoMemoryPath(path))).sort();
944
995
  }
945
996
  function parsePorcelainPath(line) {
@@ -948,13 +999,14 @@ function parsePorcelainPath(line) {
948
999
  }
949
1000
  function branchDiffStat(projectDir, changedFiles) {
950
1001
  const diffStats = [
951
- readGit(projectDir, ["diff", "--stat"]),
952
- readGit(projectDir, ["diff", "--cached", "--stat"]),
1002
+ readGit(projectDir, ["diff", "--stat", "--relative"]),
1003
+ readGit(projectDir, ["diff", "--cached", "--stat", "--relative"]),
953
1004
  ].filter(Boolean).join("\n").trim();
954
1005
  const untracked = new Set((readGit(projectDir, ["ls-files", "--others", "--exclude-standard"]) ?? "")
955
1006
  .split(/\r?\n/)
956
1007
  .map((path) => path.trim())
957
- .filter(Boolean)
1008
+ .map((path) => gitPathToProjectRelative(projectDir, path))
1009
+ .filter((path) => Boolean(path))
958
1010
  .filter((path) => changedFiles.includes(path)));
959
1011
  const untrackedStats = [...untracked]
960
1012
  .filter((file) => !diffStats.includes(file))
@@ -1016,8 +1068,9 @@ function createRepoOverviewPacket(projectDir) {
1016
1068
  if (deps.stripe)
1017
1069
  tags.push("stripe");
1018
1070
  }
1019
- if ((0, node_fs_1.existsSync)(readmePath)) {
1020
- const readme = (0, node_fs_1.readFileSync)(readmePath, "utf8").slice(0, 1000);
1071
+ const readmeText = (0, node_fs_1.existsSync)(readmePath) ? safeReadText(readmePath) : null;
1072
+ if (readmeText) {
1073
+ const readme = readmeText.slice(0, 1000);
1021
1074
  bodyParts.push(`README excerpt:\n${readme}`);
1022
1075
  }
1023
1076
  const createdAt = nowIso();
@@ -1038,7 +1091,7 @@ function createRepoOverviewPacket(projectDir) {
1038
1091
  stack,
1039
1092
  source_refs: [
1040
1093
  ...((0, node_fs_1.existsSync)(packagePath) ? [{ kind: "file", path: "package.json" }] : []),
1041
- ...((0, node_fs_1.existsSync)(readmePath) ? [{ kind: "file", path: "README.md" }] : []),
1094
+ ...(readmeText ? [{ kind: "file", path: "README.md" }] : []),
1042
1095
  ],
1043
1096
  context: {
1044
1097
  fact: "Generated repo overview summarizes package metadata and the README as a navigation aid for agent startup.",
@@ -1748,7 +1801,20 @@ function scanStructuralFiles(projectDir) {
1748
1801
  ignore("kageignore");
1749
1802
  continue;
1750
1803
  }
1751
- const stats = (0, node_fs_1.statSync)(absolutePath);
1804
+ const linkStats = safeLstat(absolutePath);
1805
+ if (!linkStats) {
1806
+ ignore("unreadable_path");
1807
+ continue;
1808
+ }
1809
+ if (linkStats.isSymbolicLink()) {
1810
+ ignore("symlink");
1811
+ continue;
1812
+ }
1813
+ const stats = safeStat(absolutePath);
1814
+ if (!stats) {
1815
+ ignore("unreadable_path");
1816
+ continue;
1817
+ }
1752
1818
  if (stats.isDirectory()) {
1753
1819
  visit(absolutePath);
1754
1820
  continue;
@@ -5282,10 +5348,14 @@ function baselineDiscoveryFiles(projectDir, task) {
5282
5348
  const absolute = (0, node_path_1.join)(projectDir, path);
5283
5349
  if (!(0, node_fs_1.existsSync)(absolute))
5284
5350
  return null;
5285
- const stats = (0, node_fs_1.statSync)(absolute);
5351
+ const stats = safeStat(absolute);
5352
+ if (!stats)
5353
+ return null;
5286
5354
  if (!stats.isFile() || stats.size > 240_000)
5287
5355
  return null;
5288
- const text = (0, node_fs_1.readFileSync)(absolute, "utf8");
5356
+ const text = safeReadText(absolute);
5357
+ if (text === null)
5358
+ return null;
5289
5359
  const score = scoreText(terms, `${path}\n${text.slice(0, 8000)}`, [path]);
5290
5360
  const alwaysUseful = ["README.md", "AGENTS.md", "CLAUDE.md", "package.json"].includes(path);
5291
5361
  if (score <= 0 && !alwaysUseful)
@@ -6421,7 +6491,7 @@ function proposeFromDiff(projectDir) {
6421
6491
  const status = readGit(projectDir, ["status", "--porcelain", "-uall"]);
6422
6492
  if (status === null)
6423
6493
  return { ok: false, changedFiles: [], errors: ["Not a git repository or git is unavailable."] };
6424
- const changedFiles = parsePorcelainStatus(status);
6494
+ const changedFiles = parsePorcelainStatus(projectDir, status);
6425
6495
  if (changedFiles.length === 0)
6426
6496
  return { ok: false, changedFiles: [], errors: ["No changed files found."] };
6427
6497
  const stat = branchDiffStat(projectDir, changedFiles);
@@ -6470,7 +6540,7 @@ function buildBranchOverlay(projectDir) {
6470
6540
  branch: gitBranch(projectDir),
6471
6541
  head: gitHead(projectDir),
6472
6542
  merge_base: gitMergeBase(projectDir),
6473
- changed_files: parsePorcelainStatus(status),
6543
+ changed_files: parsePorcelainStatus(projectDir, status),
6474
6544
  pending_packet_ids: loadPendingPackets(projectDir).map((packet) => packet.id).sort(),
6475
6545
  generated_at: nowIso(),
6476
6546
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "1.1.24",
3
+ "version": "1.1.25",
4
4
  "description": "Local-first repo memory, code graph, and recall MCP server for coding agents",
5
5
  "main": "dist/index.js",
6
6
  "files": [
package/viewer/index.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Kage Memory Terminal</title>
7
- <link rel="stylesheet" href="./styles.css?v=17">
7
+ <link rel="stylesheet" href="./styles.css?v=19">
8
8
  </head>
9
9
  <body>
10
10
  <header class="app-header">
@@ -13,6 +13,12 @@
13
13
  <h1>Memory Terminal</h1>
14
14
  <p id="graphSummary">Load memory, code, and metrics graphs to inspect what agents know and why.</p>
15
15
  </div>
16
+ <nav class="site-links" aria-label="Kage site links">
17
+ <a href="../">Home</a>
18
+ <a href="../guide.html">Docs</a>
19
+ <a href="../releases.html">Releases</a>
20
+ <a href="https://github.com/kage-core/Kage">GitHub</a>
21
+ </nav>
16
22
  <div id="statusStrip" class="status-strip" aria-label="Graph health"></div>
17
23
  <div id="autoLoadStatus" class="autoload-status">auto-load: waiting</div>
18
24
  <label class="file-picker">
@@ -157,6 +163,6 @@
157
163
  </section>
158
164
  </main>
159
165
 
160
- <script src="./app.js?v=17"></script>
166
+ <script src="./app.js?v=19"></script>
161
167
  </body>
162
168
  </html>
package/viewer/styles.css CHANGED
@@ -40,7 +40,10 @@ h2 { color: var(--terminal); font-size: 13px; letter-spacing: 0.04em; text-trans
40
40
 
41
41
  .app-header {
42
42
  display: grid;
43
- grid-template-columns: minmax(0, 1fr) auto auto auto;
43
+ grid-template-columns: minmax(360px, 1fr) auto auto;
44
+ grid-template-areas:
45
+ "brand links picker"
46
+ "status status autoload";
44
47
  gap: 16px;
45
48
  align-items: center;
46
49
  padding: 14px 18px;
@@ -52,7 +55,11 @@ h2 { color: var(--terminal); font-size: 13px; letter-spacing: 0.04em; text-trans
52
55
  z-index: 10;
53
56
  }
54
57
 
55
- .brand-block { min-width: 0; }
58
+ .brand-block {
59
+ grid-area: brand;
60
+ min-width: 280px;
61
+ max-width: 620px;
62
+ }
56
63
  .eyebrow {
57
64
  display: inline-flex;
58
65
  margin-bottom: 5px;
@@ -72,14 +79,42 @@ h2 { color: var(--terminal); font-size: 13px; letter-spacing: 0.04em; text-trans
72
79
  overflow-wrap: anywhere;
73
80
  }
74
81
 
82
+ .site-links {
83
+ grid-area: links;
84
+ display: flex;
85
+ flex-wrap: wrap;
86
+ gap: 10px;
87
+ justify-content: flex-end;
88
+ }
89
+ .site-links a {
90
+ display: inline-flex;
91
+ align-items: center;
92
+ min-height: 28px;
93
+ padding: 4px 9px;
94
+ border: 1px solid var(--line);
95
+ border-radius: 4px;
96
+ background: rgba(13, 25, 19, 0.86);
97
+ color: var(--terminal-dim);
98
+ font-size: 11px;
99
+ font-weight: 780;
100
+ text-decoration: none;
101
+ white-space: nowrap;
102
+ }
103
+ .site-links a:hover {
104
+ border-color: var(--line-strong);
105
+ color: var(--terminal-strong);
106
+ }
107
+
75
108
  .status-strip {
109
+ grid-area: status;
76
110
  display: flex;
77
111
  flex-wrap: wrap;
78
112
  gap: 8px;
79
- justify-content: flex-end;
113
+ justify-content: flex-start;
80
114
  }
81
115
 
82
116
  .autoload-status {
117
+ grid-area: autoload;
83
118
  display: inline-flex;
84
119
  align-items: center;
85
120
  min-height: 28px;
@@ -131,6 +166,7 @@ h2 { color: var(--terminal); font-size: 13px; letter-spacing: 0.04em; text-trans
131
166
  font-weight: 780;
132
167
  box-shadow: inset 0 0 0 1px rgba(65, 255, 143, 0.10);
133
168
  }
169
+ .file-picker { grid-area: picker; }
134
170
  .file-picker:hover, button:hover { background: #0d1f15; }
135
171
  .file-picker input {
136
172
  position: absolute;
@@ -702,6 +738,12 @@ input:focus, select:focus, button:focus, .file-picker:focus-within {
702
738
  @media (max-width: 1120px) {
703
739
  .app-header {
704
740
  grid-template-columns: 1fr;
741
+ grid-template-areas:
742
+ "brand"
743
+ "links"
744
+ "status"
745
+ "autoload"
746
+ "picker";
705
747
  align-items: stretch;
706
748
  padding: 12px 14px;
707
749
  }