@kage-core/kage-graph-mcp 1.4.0 → 2.0.1

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.
@@ -14,17 +14,21 @@ function notifyDone(shared) {
14
14
  Atomics.notify(done, 0);
15
15
  }
16
16
  const data = node_worker_threads_1.workerData;
17
- try {
18
- const results = data.files.map((file) => (0, kernel_js_1.buildStructuralFileForWorker)(data.projectDir, file, data.knownFiles, data.prior));
19
- writeResult(data.outputPath, { ok: true, results });
20
- }
21
- catch (error) {
22
- writeResult(data.outputPath, {
23
- ok: false,
24
- results: [],
25
- error: error instanceof Error ? `${error.message}\n${error.stack ?? ""}` : String(error),
26
- });
27
- }
28
- finally {
29
- notifyDone(data.shared);
17
+ async function run() {
18
+ try {
19
+ await (0, kernel_js_1.ensureTreeSitterLanguages)((0, kernel_js_1.treeSitterLanguagesForPaths)(data.files));
20
+ const results = data.files.map((file) => (0, kernel_js_1.buildStructuralFileForWorker)(data.projectDir, file, data.knownFiles, data.prior));
21
+ writeResult(data.outputPath, { ok: true, results });
22
+ }
23
+ catch (error) {
24
+ writeResult(data.outputPath, {
25
+ ok: false,
26
+ results: [],
27
+ error: error instanceof Error ? `${error.message}\n${error.stack ?? ""}` : String(error),
28
+ });
29
+ }
30
+ finally {
31
+ notifyDone(data.shared);
32
+ }
30
33
  }
34
+ void run();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "1.4.0",
3
+ "version": "2.0.1",
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": [
@@ -38,12 +38,15 @@
38
38
  "dependencies": {
39
39
  "@modelcontextprotocol/sdk": "^1.10.2",
40
40
  "three": "^0.184.0",
41
- "typescript": "^5.0.0"
41
+ "tree-sitter-wasms": "^0.1.13",
42
+ "typescript": "^5.0.0",
43
+ "web-tree-sitter": "^0.24.7"
42
44
  },
43
45
  "devDependencies": {
44
46
  "@types/node": "^22.0.0"
45
47
  },
46
48
  "engines": {
47
49
  "node": ">=18"
48
- }
49
- }
50
+ },
51
+ "mcpName": "com.kage-core/kage"
52
+ }
package/viewer/console.js CHANGED
@@ -9,9 +9,36 @@
9
9
  suppressed: src("suppressed", root + "suppressed.json"),
10
10
  metrics: src("metrics", "./data/kage/metrics.json"),
11
11
  activity: src("activity", root + "activity.json"),
12
+ value: src("value", root + "value.json"),
12
13
  };
13
14
  var state = { items: [], filter: "all", q: "", metrics: null, graphReady: false, showAll: false };
14
15
 
16
+ // Theme-aware colors: the canvas graph and donut can't use CSS variables directly,
17
+ // so resolve them once (and again when the OS light/dark preference flips).
18
+ var THEME = {};
19
+ function cssVar(name, fallback) {
20
+ var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
21
+ return v || fallback;
22
+ }
23
+ function refreshTheme() {
24
+ THEME = {
25
+ gain: cssVar("--gain", "#0c7a4d"),
26
+ warn: cssVar("--warn", "#9a6b08"),
27
+ code: cssVar("--code", "#155e9c"),
28
+ memory: cssVar("--memory", "#6d49b8"),
29
+ ink: cssVar("--ink", "#1c1e1a"),
30
+ muted: cssVar("--muted", "#5d635b"),
31
+ faint: cssVar("--faint", "#8b9088"),
32
+ paper: cssVar("--paper", "#fffdf9"),
33
+ line: cssVar("--line-strong", "#c9c4b4"),
34
+ };
35
+ }
36
+ refreshTheme();
37
+ if (window.matchMedia) {
38
+ var scheme = window.matchMedia("(prefers-color-scheme: dark)");
39
+ if (scheme.addEventListener) scheme.addEventListener("change", function () { refreshTheme(); renderInsights(state.metrics, state.items); });
40
+ }
41
+
15
42
  function getJSON(p) { return fetch(p).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }); }
16
43
  function el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }
17
44
  function fmt(n) {
@@ -39,7 +66,8 @@
39
66
 
40
67
  // ---- nav ----
41
68
  var META = {
42
- overview: ["kage://overview", "Repository overview", "Whether this repo's agent memory can be trusted at a glance."],
69
+ gains: ["kage://gains", "What Kage saved you", "Tokens, dollars, and bad memories caughtreceipts, not vibes."],
70
+ overview: ["kage://trust", "Memory trust", "Whether this repo's agent memory can be trusted — at a glance."],
43
71
  graph: ["kage://memory-map", "Memory ↔ code map", "Each packet anchored to the files it's grounded in. Hover a node to inspect."],
44
72
  memory: ["kage://memory", "Memory", "Every packet Kage has stored, with health and grounding."],
45
73
  activity: ["kage://activity", "Activity", "What agents actually recalled and captured here, over time."],
@@ -62,15 +90,16 @@
62
90
  });
63
91
 
64
92
  // ---- load ----
65
- Promise.all([getJSON(paths.trust), getJSON(paths.suppressed), getJSON(paths.lifecycle), getJSON(paths.metrics), getJSON(paths.activity)])
66
- .then(function (r) { render(r[0], r[1], r[2], r[3], r[4]); })
67
- .catch(function () { render(null, null, null, null, null); });
93
+ Promise.all([getJSON(paths.trust), getJSON(paths.suppressed), getJSON(paths.lifecycle), getJSON(paths.metrics), getJSON(paths.activity), getJSON(paths.value)])
94
+ .then(function (r) { render(r[0], r[1], r[2], r[3], r[4], r[5]); })
95
+ .catch(function () { render(null, null, null, null, null, null); });
68
96
 
69
- function render(trust, suppressed, lifecycle, metrics, activity) {
97
+ function render(trust, suppressed, lifecycle, metrics, activity, value) {
70
98
  state.items = (lifecycle && lifecycle.items) || [];
71
99
  state.metrics = metrics || {};
72
100
  state.activity = activity || {};
73
101
  document.getElementById("repo").textContent = resolveRepoName(metrics, lifecycle);
102
+ renderGains(value);
74
103
  renderHero(trust);
75
104
  renderTiles(metrics, state.items);
76
105
  renderAttention(state.items, suppressed);
@@ -78,7 +107,115 @@
78
107
  renderInsights(metrics, state.items);
79
108
  renderActivity(activity);
80
109
  var start = (location.hash || "").replace("#", "");
81
- show(META[start] ? start : "overview");
110
+ show(META[start] ? start : "gains");
111
+ }
112
+
113
+ // ---- gains (value ledger receipts) ----
114
+ // value.json is the raw ledger written by recall: { totals, events[] }. Windows are
115
+ // recomputed here with the same rules as `kage gains` (today = local midnight,
116
+ // 7d = rolling, all-time = totals so trimmed events never lose history).
117
+ var DOLLARS_PER_MILLION_TOKENS = 15;
118
+ function dollars(tokens) { return (tokens / 1e6) * DOLLARS_PER_MILLION_TOKENS; }
119
+ function fmtDollars(tokens) {
120
+ var d = dollars(tokens);
121
+ return "$" + (d >= 100 ? Math.round(d) : d.toFixed(2));
122
+ }
123
+ function summarizeWindow(events, cutoff) {
124
+ var w = { tokens_saved: 0, stale_withheld: 0, recalls: 0, caller_answers: 0 };
125
+ events.forEach(function (e) {
126
+ var at = Date.parse(e.at);
127
+ if (!at || at < cutoff) return;
128
+ if (e.kind === "recall_served") { w.recalls += 1; w.tokens_saved += Math.max(0, e.tokens_saved || 0); }
129
+ else if (e.kind === "stale_withheld") w.stale_withheld += 1;
130
+ else if (e.kind === "caller_answered") w.caller_answers += 1;
131
+ });
132
+ return w;
133
+ }
134
+ function renderGains(value) {
135
+ var hero = document.getElementById("gainsHero");
136
+ var tiles = document.getElementById("gainsTiles");
137
+ var timeline = document.getElementById("gainsTimeline");
138
+ if (!hero) return;
139
+ var events = (value && value.events) || [];
140
+ var totals = (value && value.totals) || null;
141
+ var midnight = new Date(); midnight.setHours(0, 0, 0, 0);
142
+ var today = summarizeWindow(events, midnight.getTime());
143
+ var week = summarizeWindow(events, Date.now() - 7 * 86400000);
144
+ var all = totals || summarizeWindow(events, 0);
145
+
146
+ hero.textContent = "";
147
+ var head = el("div", "r-head");
148
+ head.appendChild(el("span", null, "Kage · savings receipt"));
149
+ head.appendChild(el("span", null, new Date().toISOString().slice(0, 10)));
150
+ hero.appendChild(head);
151
+ var hh = el("div", "r-hero");
152
+ var big = el("div", "big");
153
+ big.appendChild(el("b", null, all.tokens_saved ? "0" : "—"));
154
+ big.appendChild(el("span", "unit", "tokens saved, all time"));
155
+ if (all.tokens_saved) big.appendChild(el("span", "dollars", "≈ " + fmtDollars(all.tokens_saved)));
156
+ hh.appendChild(big);
157
+ hh.appendChild(el("p", "sub", all.tokens_saved
158
+ ? "Context your agents did not have to re-read from source, because Kage served grounded memory instead."
159
+ : "No savings recorded yet. Run kage recall or kage_context against this repo and the receipt fills in."));
160
+ hero.appendChild(hh);
161
+ var wins = el("div", "r-windows");
162
+ [["Today", today], ["Last 7 days", week], ["All time", all]].forEach(function (p) {
163
+ var w = el("div", "r-win");
164
+ w.appendChild(el("div", "k", p[0]));
165
+ w.appendChild(el("div", "v", fmt(p[1].tokens_saved) + " tok"));
166
+ w.appendChild(el("div", "d", "≈ " + fmtDollars(p[1].tokens_saved)));
167
+ wins.appendChild(w);
168
+ });
169
+ hero.appendChild(wins);
170
+ var lines = el("div", "r-lines");
171
+ [
172
+ ["Recalls served from memory", fmt(all.recalls), "gain"],
173
+ ["Stale memories caught & withheld", fmt(all.stale_withheld), all.stale_withheld ? "warn" : ""],
174
+ ["Caller questions answered from the graph", fmt(all.caller_answers), ""],
175
+ ].forEach(function (li) {
176
+ var row = el("div", "r-line");
177
+ row.appendChild(el("span", null, li[0]));
178
+ row.appendChild(el("span", "dots"));
179
+ row.appendChild(el("b", li[2], li[1]));
180
+ lines.appendChild(row);
181
+ });
182
+ hero.appendChild(lines);
183
+ hero.appendChild(el("div", "r-foot", "estimated at $" + DOLLARS_PER_MILLION_TOKENS + " per 1M input tokens · ledger: .agent_memory/reports/value.json · verify: kage gains"));
184
+ if (all.tokens_saved) countUp(big.querySelector("b"), all.tokens_saved, 900);
185
+
186
+ if (tiles) {
187
+ tiles.textContent = "";
188
+ [
189
+ { k: "Saved (7 days)", v: fmt(week.tokens_saved), s: "≈ " + fmtDollars(week.tokens_saved) + " of context not re-read", cls: "green" },
190
+ { k: "Recalls served (7d)", v: fmt(week.recalls), s: fmt(all.recalls) + " all-time", cls: "green" },
191
+ { k: "Stale caught", v: fmt(all.stale_withheld), s: all.stale_withheld ? "withheld before they misled an agent" : "nothing withheld yet", cls: all.stale_withheld ? "warn" : "" },
192
+ { k: "Graph answers", v: fmt(all.caller_answers), s: "caller questions answered from the code graph", cls: "code" },
193
+ ].forEach(function (d) { var x = el("div", "tile"); x.appendChild(el("div", "k", d.k)); x.appendChild(el("div", "v " + (d.cls || ""), d.v)); x.appendChild(el("div", "s", d.s)); tiles.appendChild(x); });
194
+ }
195
+
196
+ if (timeline) {
197
+ timeline.textContent = ""; timeline.className = "vfeed";
198
+ if (!events.length) { timeline.appendChild(el("div", "empty", "No value events yet. Each recall, withheld stale memory, and graph answer lands here with a timestamp.")); return; }
199
+ var V_ICON = { recall_served: "✓", stale_withheld: "⊘", caller_answered: "◆" };
200
+ events.slice(-30).reverse().forEach(function (e) {
201
+ var row = el("div", "vev " + (e.kind || ""));
202
+ row.appendChild(el("span", "vi", V_ICON[e.kind] || "•"));
203
+ var mid = el("div", "vt");
204
+ if (e.kind === "recall_served") {
205
+ mid.appendChild(document.createTextNode("Recall served — saved "));
206
+ mid.appendChild(el("b", null, "~" + fmt(Math.max(0, e.tokens_saved || 0)) + " tokens"));
207
+ mid.appendChild(document.createTextNode(" vs reading source"));
208
+ } else if (e.kind === "stale_withheld") {
209
+ mid.appendChild(document.createTextNode("Stale memory withheld"));
210
+ if (e.packet_title) { mid.appendChild(document.createTextNode(" — ")); mid.appendChild(el("b", null, e.packet_title)); }
211
+ } else {
212
+ mid.appendChild(document.createTextNode("Caller question answered from the code graph"));
213
+ }
214
+ row.appendChild(mid);
215
+ row.appendChild(el("span", "when", relTime(e.at)));
216
+ timeline.appendChild(row);
217
+ });
218
+ }
82
219
  }
83
220
 
84
221
  // ---- activity feed (real recorded recalls + captures) ----
@@ -341,15 +478,15 @@
341
478
  var generated = c.generated || 0;
342
479
  var groundedCurrent = items.length - needsReview - generated;
343
480
  var seg = [
344
- { k: "Grounded & current", v: groundedCurrent, col: "#41ff8f" },
345
- { k: "Needs review", v: needsReview, col: "#ffd166" },
346
- { k: "Generated", v: generated, col: "#3a8db0" },
481
+ { k: "Grounded & current", v: groundedCurrent, col: THEME.gain },
482
+ { k: "Needs review", v: needsReview, col: THEME.warn },
483
+ { k: "Generated", v: generated, col: THEME.code },
347
484
  ].filter(function (s) { return s.v > 0; });
348
485
  var total = items.length || 1;
349
486
  var stops = [], acc = 0;
350
487
  seg.forEach(function (s) { var from = acc / total * 360, to = (acc + s.v) / total * 360; stops.push(s.col + " " + from + "deg " + to + "deg"); acc += s.v; });
351
488
  var donut = document.getElementById("donut");
352
- donut.style.background = "conic-gradient(" + (stops.join(", ") || "#41ff8f 0deg 360deg") + ")";
489
+ donut.style.background = "conic-gradient(" + (stops.join(", ") || THEME.gain + " 0deg 360deg") + ")";
353
490
  var pct = Math.round(groundedCurrent / total * 100);
354
491
  // The big % is the health readout, so color it by how healthy it is — a high
355
492
  // grounded share is green, not a warning. (Amber/red only when it's actually low.)
@@ -417,6 +554,14 @@
417
554
 
418
555
  // ---- memory <-> code graph (interactive canvas) ----
419
556
  var G = { nodes: [], edges: [], hover: -1, focus: -1, raf: 0, alpha: 1, filter: "all", view: { s: 1, tx: 0, ty: 0 }, drag: null, pan: null, tween: null };
557
+ // Canvas needs concrete colors; turn a resolved theme hex into rgba at a given alpha.
558
+ function rgba(hex, a) {
559
+ var h = String(hex).replace("#", "");
560
+ if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
561
+ var n = parseInt(h, 16);
562
+ if (isNaN(n)) return hex;
563
+ return "rgba(" + ((n >> 16) & 255) + "," + ((n >> 8) & 255) + "," + (n & 255) + "," + a + ")";
564
+ }
420
565
  function seeded(n) { var x = Math.sin(n * 999.137) * 43758.5453; return x - Math.floor(x); }
421
566
  function clamp(v, a, b) { return v < a ? a : v > b ? b : v; }
422
567
  function nodeR(nd) { return Math.min(13, 4.5 + nd.deg * 1.1); }
@@ -470,8 +615,8 @@
470
615
  G.edges.forEach(function (e) { if (seed[e[0]]) set[e[1]] = 1; if (seed[e[1]]) set[e[0]] = 1; });
471
616
  return set;
472
617
  }
473
- function color(nd) { return nd.kind === "file" ? "#6ad7ff" : (nd.review ? "#ffd166" : "#c49cff"); }
474
- function bodyColor(nd) { return nd.kind === "file" ? "rgba(6,18,22,0.92)" : (nd.review ? "rgba(26,21,7,0.92)" : "rgba(19,12,28,0.92)"); }
618
+ function color(nd) { return nd.kind === "file" ? THEME.code : (nd.review ? THEME.warn : THEME.memory); }
619
+ function bodyColor(nd) { return rgba(THEME.paper, 0.94); }
475
620
  var DIAMOND = { decision: 1, bug_fix: 1, test: 1, gotcha: 1 };
476
621
  function shapePath(x, y, r, nd) {
477
622
  if (nd.kind === "file") { roundRect(x - r * 1.3, y - r * 0.78, r * 2.6, r * 1.56, 3); return; }
@@ -518,7 +663,7 @@
518
663
  ctx.lineWidth = 1 / v.s;
519
664
  G.edges.forEach(function (e) {
520
665
  var on = active && emph[e[0]] && emph[e[1]];
521
- ctx.strokeStyle = on ? "rgba(106,215,255,0.65)" : (active ? "rgba(147,175,160,0.04)" : "rgba(147,175,160,0.13)");
666
+ ctx.strokeStyle = on ? rgba(THEME.code, 0.7) : (active ? rgba(THEME.faint, 0.08) : rgba(THEME.faint, 0.25));
522
667
  ctx.beginPath(); ctx.moveTo(n[e[0]].x, n[e[0]].y); ctx.lineTo(n[e[1]].x, n[e[1]].y); ctx.stroke();
523
668
  });
524
669
  n.forEach(function (nd, i) {
@@ -534,7 +679,7 @@
534
679
  if (nd.kind === "memory") { ctx.globalAlpha = (dim ? 0.14 : 1) * 0.9; ctx.fillStyle = col; ctx.beginPath(); ctx.arc(nd.x, nd.y, Math.max(2.2, r * 0.2), 0, 6.2832); ctx.fill(); }
535
680
  ctx.restore();
536
681
  // halo on hover/focus
537
- if (!dim && strong) { ctx.save(); shapePath(nd.x, nd.y, r + 5, nd); ctx.strokeStyle = i === G.focus ? "#e4f7e9" : col; ctx.lineWidth = 1.8 / v.s; ctx.shadowColor = col; ctx.shadowBlur = 9; ctx.stroke(); ctx.restore(); }
682
+ if (!dim && strong) { ctx.save(); shapePath(nd.x, nd.y, r + 5, nd); ctx.strokeStyle = i === G.focus ? THEME.ink : col; ctx.lineWidth = 1.8 / v.s; ctx.shadowColor = col; ctx.shadowBlur = 9; ctx.stroke(); ctx.restore(); }
538
683
  });
539
684
  // labels in screen space (mono pill, centered below) so they stay crisp at any zoom
540
685
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
@@ -548,9 +693,9 @@
548
693
  if (sx < -60 || sx > W + 60 || sy < -20 || sy > H + 20) return;
549
694
  ctx.font = (strong ? "700 " : "600 ") + "11px ui-monospace, Menlo, monospace";
550
695
  var w = ctx.measureText(nd.label).width, pw = w + 14, ph = 18, lx = sx - pw / 2, ly = sy + r + 7;
551
- ctx.globalAlpha = 0.92; ctx.fillStyle = "rgba(5,8,6,0.92)"; roundRect(lx, ly, pw, ph, 4); ctx.fill();
552
- ctx.lineWidth = 1; ctx.strokeStyle = strong ? "rgba(65,255,143,0.42)" : "rgba(65,255,143,0.12)"; ctx.stroke();
553
- ctx.globalAlpha = 1; ctx.fillStyle = strong ? "#e4f7e9" : "#9cb0a4"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
696
+ ctx.globalAlpha = 0.94; ctx.fillStyle = rgba(THEME.paper, 0.94); roundRect(lx, ly, pw, ph, 4); ctx.fill();
697
+ ctx.lineWidth = 1; ctx.strokeStyle = strong ? rgba(THEME.gain, 0.6) : rgba(THEME.line, 0.8); ctx.stroke();
698
+ ctx.globalAlpha = 1; ctx.fillStyle = strong ? THEME.ink : THEME.muted; ctx.textAlign = "center"; ctx.textBaseline = "middle";
554
699
  ctx.fillText(nd.label, sx, ly + ph / 2);
555
700
  });
556
701
  ctx.textAlign = "left"; ctx.textBaseline = "alphabetic"; ctx.globalAlpha = 1;
@@ -581,10 +726,10 @@
581
726
  if (G.focus < 0) { detail.style.display = "none"; return; }
582
727
  var nd = G.nodes[G.focus], html;
583
728
  if (nd.kind === "file") {
584
- html = "<div class='gd-k' style='color:#6ad7ff'>code file</div><b class='gd-t'>" + escapeHtml(nd.tip) + "</b>" +
729
+ html = "<div class='gd-k k-file'>code file</div><b class='gd-t'>" + escapeHtml(nd.tip) + "</b>" +
585
730
  "<div class='gd-row'>cited by <b>" + nd.deg + "</b> memor" + (nd.deg === 1 ? "y" : "ies") + "</div>";
586
731
  } else {
587
- html = "<div class='gd-k' style='color:#c49cff'>" + escapeHtml(nd.sub) + "</div><b class='gd-t'>" + escapeHtml(nd.tip) + "</b>" +
732
+ html = "<div class='gd-k k-memory'>" + escapeHtml(nd.sub) + "</div><b class='gd-t'>" + escapeHtml(nd.tip) + "</b>" +
588
733
  "<div class='gd-row'>health <b class='" + (nd.health || "") + "'>" + (nd.health || "—") + "</b>" + (nd.uses ? " · used " + nd.uses + "×" : "") + " · " + nd.deg + " file" + (nd.deg === 1 ? "" : "s") + "</div>" +
589
734
  (nd.files && nd.files.length ? "<div class='gd-files'>" + nd.files.map(escapeHtml).join(" · ") + "</div>" : "");
590
735
  }