@kage-core/kage-graph-mcp 2.5.4 → 2.5.6

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.
Files changed (3) hide show
  1. package/dist/cli.js +16 -0
  2. package/dist/kernel.js +169 -48
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -708,6 +708,22 @@ async function main() {
708
708
  }
709
709
  return;
710
710
  }
711
+ if (command === "gen-plugin-hooks") {
712
+ // Maintainer tool: regenerate plugin/hooks/* from the setupAgent("claude-code") templates
713
+ // so the plugin and the npm install path ship identical hooks (one source of truth).
714
+ const pluginDir = takeArg(args, "--plugin-dir") ?? (0, node_path_1.join)(process.cwd(), "plugin");
715
+ const result = (0, kernel_js_1.generatePluginHooks)(pluginDir);
716
+ if (args.includes("--json")) {
717
+ console.log(JSON.stringify({ plugin_dir: pluginDir, ...result }, null, 2));
718
+ return;
719
+ }
720
+ console.log(`Generated plugin hooks in ${(0, node_path_1.join)(pluginDir, "hooks")}`);
721
+ console.log(` scripts: ${result.scripts.join(", ")}`);
722
+ console.log(` events: ${result.events.join(", ")}`);
723
+ if (result.removed.length)
724
+ console.log(` removed: ${result.removed.join(", ")}`);
725
+ return;
726
+ }
711
727
  if (command === "daemon") {
712
728
  const action = args[1];
713
729
  const projectDir = projectArg(args);
package/dist/kernel.js CHANGED
@@ -102,6 +102,7 @@ exports.compactProject = compactProject;
102
102
  exports.installAgentPolicy = installAgentPolicy;
103
103
  exports.createDenseEmbeddingProvider = createDenseEmbeddingProvider;
104
104
  exports.buildEmbeddingIndex = buildEmbeddingIndex;
105
+ exports.isUngroundedConversationalCapture = isUngroundedConversationalCapture;
105
106
  exports.recall = recall;
106
107
  exports.recallWithEmbeddings = recallWithEmbeddings;
107
108
  exports.queryCodeGraph = queryCodeGraph;
@@ -150,6 +151,7 @@ exports.capture = capture;
150
151
  exports.createPublicCandidate = createPublicCandidate;
151
152
  exports.registryRecommendations = registryRecommendations;
152
153
  exports.setupAgent = setupAgent;
154
+ exports.generatePluginHooks = generatePluginHooks;
153
155
  exports.setupDoctor = setupDoctor;
154
156
  exports.verifyAgentActivation = verifyAgentActivation;
155
157
  exports.observe = observe;
@@ -2133,9 +2135,18 @@ function evaluateMemoryAdmission(projectDir, packet) {
2133
2135
  score -= 10;
2134
2136
  risks.push("too little context");
2135
2137
  }
2138
+ // Ungrounded conversational chatter (a path-less, repo-reference-free user outburst) is not
2139
+ // durable memory regardless of its other signals — keywords like "issue"/"before" can
2140
+ // otherwise score it as a candidate. Hard-block admission so it can never auto-promote.
2141
+ if (isUngroundedConversationalCapture(packet)) {
2142
+ score -= 45;
2143
+ risks.push("ungrounded conversational utterance, not durable knowledge");
2144
+ }
2136
2145
  const bounded = Math.max(0, Math.min(100, score));
2137
2146
  return {
2138
- admit: bounded >= 45 && risks.indexOf("session bookkeeping, not durable knowledge") === -1,
2147
+ admit: bounded >= 45
2148
+ && risks.indexOf("session bookkeeping, not durable knowledge") === -1
2149
+ && risks.indexOf("ungrounded conversational utterance, not durable knowledge") === -1,
2139
2150
  class: bounded >= 72 ? "high_signal" : bounded >= 45 ? "candidate" : "episodic_only",
2140
2151
  score: bounded,
2141
2152
  reasons,
@@ -7004,10 +7015,11 @@ function gcProject(projectDir, options = {}) {
7004
7015
  skipped.push({ id: packet.id, title: packet.title, reason: "already deprecated" });
7005
7016
  continue;
7006
7017
  }
7007
- // Serialized transcript / tool-output / file-content dumps carry no durable knowledge
7008
- // and bloat recall + the graph. Always delete them (deprecating would leave the blob on
7009
- // disk) this also reclaims legacy dumps written before the capture-time guard existed.
7010
- if (isSerializedDumpTitle(packet.title) || isSerializedDumpBody(packet.body)) {
7018
+ // Serialized transcript / tool-output / file-content dumps and ungrounded conversational
7019
+ // chatter (a path-less rant at the assistant) carry no durable knowledge and bloat recall +
7020
+ // the digest. Always delete them (deprecating would leave the blob on disk) — this also
7021
+ // reclaims legacy junk written before the capture-time guards existed.
7022
+ if (isSerializedDumpTitle(packet.title) || isSerializedDumpBody(packet.body) || isUngroundedConversationalCapture(packet)) {
7011
7023
  if (!options.dryRun)
7012
7024
  (0, node_fs_1.unlinkSync)(path);
7013
7025
  deleted.push({ id: packet.id, title: packet.title });
@@ -7970,6 +7982,73 @@ function isSerializedDumpBody(body) {
7970
7982
  return isSerializedDumpTitle(t)
7971
7983
  || /<task-notification\b|<tool-use-id\b|"hookSpecificOutput"|"isImage"\s*:|"noOutputExpected"\s*:|"interrupted"\s*:\s*(true|false)/i.test(t.slice(0, 4000));
7972
7984
  }
7985
+ // Repo grounding signals — does the text point at something concrete in the codebase? These
7986
+ // three predicates are the shared vocabulary for "grounded": observationSignalScore weights
7987
+ // them into its durable-knowledge score, and the ungrounded-utterance guard uses their union
7988
+ // to decide whether a path-less capture references the repo at all. Kept as one source of
7989
+ // truth so the scorer and the guard can never drift on what "grounded" means.
7990
+ function textCitesPath(text) {
7991
+ return /\b[\w.-]+\/[\w./-]+\b/.test(text)
7992
+ || /\b[\w-]+\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|rb|json|ya?ml|toml|md|sh|css|html|sql)\b/i.test(text);
7993
+ }
7994
+ function textHasCodeIdentifier(text) {
7995
+ return /\b[a-z][a-z0-9]*[A-Z]\w*\b/.test(text) // camelCase
7996
+ || /\b[a-z0-9]+_[a-z0-9_]+\b/.test(text) // snake_case
7997
+ || /`[^`]+`/.test(text)
7998
+ || /\b\w+\(\)/.test(text);
7999
+ }
8000
+ function textHasCommand(text) {
8001
+ return /(^|\s)(npm|pnpm|yarn|npx|node|git|cargo|make|pytest|go|tsc|kage)\s+[\w.-]/.test(text);
8002
+ }
8003
+ function hasRepoGroundingSignal(text) {
8004
+ const t = text ?? "";
8005
+ if (!t.trim())
8006
+ return false;
8007
+ return textCitesPath(t) || textHasCodeIdentifier(t) || textHasCommand(t);
8008
+ }
8009
+ // A raw conversational user utterance: a frustrated/rhetorical/imperative chat message aimed
8010
+ // at the agent — e.g. "why are you asking me???!!! it's your job, don't stop before you...".
8011
+ // It is plain prose, so the serialized-dump guard waves it through, yet it is chatter, not
8012
+ // durable repo knowledge. Detection is deliberately narrow — a run of emphatic terminal
8013
+ // punctuation, or one of a curated set of rhetorical / second-person-at-the-assistant
8014
+ // phrases — so it stays clear of normal declarative learnings (which read as statements, not
8015
+ // outbursts addressed to "you").
8016
+ // Phrases that rant AT the assistant ("why are you...", "it's your job", "don't stop"). These
8017
+ // never appear in a curated learning, so they mark chatter even when the outburst name-drops a
8018
+ // platform word (github, pr, x, linkedin) that the loose grounding matcher would otherwise
8019
+ // treat as a repo reference. This is the override that closes the leak where a frustrated
8020
+ // message peppered with nouns slipped through and was captured as approved memory.
8021
+ function looksFrustratedAtAssistant(text) {
8022
+ return /\b(why are you|why did you|why would you|why aren'?t you|are you (kidding|serious|even|really)|it'?s your job|that'?s your job|do your job|don'?t stop|stop asking|stop before you|keep going|hurry up|just do it|figure it out yourself|i (already )?told you)\b/i.test(text ?? "");
8023
+ }
8024
+ function looksLikeRawUserUtterance(text) {
8025
+ const t = (text ?? "").trim();
8026
+ if (!t)
8027
+ return false;
8028
+ // Emphatic or mixed terminal punctuation ("???", "!!", "?!") — common in venting chat,
8029
+ // essentially absent from curated memory prose.
8030
+ if (/[!?]{2,}/.test(t))
8031
+ return true;
8032
+ // Rhetorical questions / second-person-imperative frustration directed at the assistant.
8033
+ return looksFrustratedAtAssistant(t);
8034
+ }
8035
+ // Capture-noise guard for ungrounded chat. A packet trips it when it cites zero repo paths and
8036
+ // either (a) it rants at the assistant — which overrides incidental repo-ish words, since the
8037
+ // grounding matcher false-positives on bare platform names — or (b) it reads as a raw outburst
8038
+ // and carries no repo grounding signal at all. The override + conjunction keep it safe: a real
8039
+ // ungrounded decision or convention is declarative (no "why are you.../don't stop") and usually
8040
+ // names a symbol, file, command, or rule, so it is never caught. Such packets route to pending
8041
+ // (not auto-approved) at capture time and are withheld from recall, like serialized dumps.
8042
+ function isUngroundedConversationalCapture(packet) {
8043
+ if (packet.paths && packet.paths.length > 0)
8044
+ return false;
8045
+ const text = `${packet.title ?? ""}\n${packet.body ?? ""}`;
8046
+ if (looksFrustratedAtAssistant(text))
8047
+ return true;
8048
+ if (hasRepoGroundingSignal(text))
8049
+ return false;
8050
+ return looksLikeRawUserUtterance(text);
8051
+ }
7973
8052
  // Collapse whitespace and hard-cap a value rendered inline in a context block, so one
7974
8053
  // oversized field (e.g. a graph fact whose body is a raw transcript) can never blow up
7975
8054
  // the assembled output — the 270k-char overflow that motivated this guard.
@@ -8055,7 +8134,7 @@ function recallWithVectorScores(projectDir, query, limit = 5, explain = false, i
8055
8134
  ];
8056
8135
  return { packet, score: score_breakdown.final, relevance, why_matched: unique(why).slice(0, 12), score_breakdown };
8057
8136
  })
8058
- .filter((entry) => entry.relevance > 0 && !isSerializedDumpTitle(entry.packet.title))
8137
+ .filter((entry) => entry.relevance > 0 && !isSerializedDumpTitle(entry.packet.title) && !isUngroundedConversationalCapture(entry.packet))
8059
8138
  .sort((a, b) => b.score - a.score || a.packet.title.localeCompare(b.packet.title));
8060
8139
  const scored = diversifyRecallEntries(rankedScored, limit)
8061
8140
  .map(({ relevance, ...entry }) => entry);
@@ -8067,7 +8146,7 @@ function recallWithVectorScores(projectDir, query, limit = 5, explain = false, i
8067
8146
  const { score, why } = pendingLexicalScores.get(packet.id) ?? { score: 0, why: [] };
8068
8147
  return { packet, score, why_matched: why };
8069
8148
  })
8070
- .filter((entry) => entry.score > 0)
8149
+ .filter((entry) => entry.score > 0 && !isUngroundedConversationalCapture(entry.packet))
8071
8150
  .sort((a, b) => b.score - a.score || a.packet.title.localeCompare(b.packet.title))
8072
8151
  .filter((entry) => {
8073
8152
  const key = `${entry.packet.type}:${entry.packet.title.toLowerCase()}:${entry.packet.paths.join(",")}`;
@@ -13711,6 +13790,20 @@ function capture(input) {
13711
13790
  if (missingPaths.length) {
13712
13791
  warnings.push(`Some referenced paths do not exist in this repo: ${missingPaths.join(", ")}`);
13713
13792
  }
13793
+ // Ungrounded conversational chatter — a frustrated/rhetorical user message with no cited repo
13794
+ // paths — is not durable memory. Don't hard-reject (a reviewer may still salvage it); instead
13795
+ // deny it the auto-approve fast path so it lands in the pending inbox, tagged with why. Recall
13796
+ // withholds it regardless of status. Explicit pendingReview captures already route to review.
13797
+ const ungroundedUtterance = isUngroundedConversationalCapture({
13798
+ title: input.title,
13799
+ body: input.body,
13800
+ paths: groundedPaths,
13801
+ });
13802
+ const routeToPending = Boolean(input.pendingReview) || ungroundedUtterance;
13803
+ if (ungroundedUtterance && !input.pendingReview) {
13804
+ warnings.push("Routed to pending review: this reads as an ungrounded conversational message with no cited repo paths, " +
13805
+ "not a durable learning. Cite a repo path or restate it as a reusable insight to approve it.");
13806
+ }
13714
13807
  const createdAt = nowIso();
13715
13808
  // Agent-asserted links to code-graph nodes (PRD `graph_nodes`): the agent recording the
13716
13809
  // memory knows which symbol/route/file the rule is about, so let it declare them instead
@@ -13729,9 +13822,9 @@ function capture(input) {
13729
13822
  scope: "repo",
13730
13823
  visibility: "team",
13731
13824
  sensitivity: "internal",
13732
- status: input.pendingReview ? "pending" : "approved",
13825
+ status: routeToPending ? "pending" : "approved",
13733
13826
  confidence: DEFAULT_CONFIDENCE,
13734
- tags: input.tags ?? [],
13827
+ tags: ungroundedUtterance ? unique([...(input.tags ?? []), "needs-grounding"]) : (input.tags ?? []),
13735
13828
  paths: groundedPaths,
13736
13829
  stack: input.stack ?? [],
13737
13830
  source_refs: [
@@ -13793,7 +13886,7 @@ function capture(input) {
13793
13886
  ...evaluateMemoryQuality(input.projectDir, packet),
13794
13887
  ...(contradictions.length ? { contradicts: contradictions.map((c) => c.packet_id) } : {}),
13795
13888
  };
13796
- const path = writePacket(input.projectDir, packet, input.pendingReview ? "pending" : "packets");
13889
+ const path = writePacket(input.projectDir, packet, routeToPending ? "pending" : "packets");
13797
13890
  recordMemoryAudit(input.projectDir, "capture", [packet], {
13798
13891
  type: packet.type,
13799
13892
  status: packet.status,
@@ -14354,6 +14447,58 @@ exit 0
14354
14447
  setSnippet(paths[agent] || null, universal, [`Merge this MCP stdio config into ${agent}'s MCP settings.`, "Restart the agent after updating config."]);
14355
14448
  return result;
14356
14449
  }
14450
+ // Single source of truth for Claude Code hooks. The plugin's hook scripts and hooks.json are
14451
+ // GENERATED from the exact same setupAgent("claude-code") output the npm install path writes —
14452
+ // so a hook fix lands in both channels and they can't silently drift. The hook scripts call the
14453
+ // kage CLI and never reference their own location, so the same bodies serve both the ~/.claude
14454
+ // install and the plugin; only the wiring's command path is re-targeted to ${CLAUDE_PLUGIN_ROOT}.
14455
+ // Run via `kage gen-plugin-hooks`; a unit test asserts the committed plugin/hooks match this.
14456
+ function generatePluginHooks(pluginDir) {
14457
+ const tmpHome = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "kage-plugin-home-"));
14458
+ const tmpProject = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "kage-plugin-proj-"));
14459
+ try {
14460
+ setupAgent("claude-code", tmpProject, { write: true, homeDir: tmpHome });
14461
+ const srcHookDir = (0, node_path_1.join)(tmpHome, ".claude", "kage", "hooks");
14462
+ const settings = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(tmpHome, ".claude", "settings.json"), "utf8"));
14463
+ const hooksOut = (0, node_path_1.join)(pluginDir, "hooks");
14464
+ (0, node_fs_1.mkdirSync)(hooksOut, { recursive: true });
14465
+ // Copy the generated hook scripts verbatim (path-agnostic — they shell out to `kage`).
14466
+ const scripts = (0, node_fs_1.readdirSync)(srcHookDir).filter((f) => f.endsWith(".sh")).sort();
14467
+ for (const file of scripts) {
14468
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(hooksOut, file), (0, node_fs_1.readFileSync)((0, node_path_1.join)(srcHookDir, file), "utf8"), { mode: 0o755 });
14469
+ }
14470
+ // Re-target the event wiring from the install path's `~/.claude/kage/hooks/` to the plugin's
14471
+ // `${CLAUDE_PLUGIN_ROOT}/hooks/`, preserving events, matchers, and timeouts exactly.
14472
+ const events = Object.keys(settings.hooks ?? {});
14473
+ const pluginHooks = { hooks: {} };
14474
+ for (const event of events) {
14475
+ pluginHooks.hooks[event] = (settings.hooks ?? {})[event].map((entry) => ({
14476
+ ...(entry.matcher !== undefined ? { matcher: entry.matcher } : {}),
14477
+ hooks: entry.hooks.map((h) => {
14478
+ const m = h.command.match(/^bash ~\/\.claude\/kage\/hooks\/(.+)$/);
14479
+ return {
14480
+ type: h.type,
14481
+ command: m ? `bash "\${CLAUDE_PLUGIN_ROOT}/hooks/${m[1]}"` : h.command,
14482
+ ...(h.timeout !== undefined ? { timeout: h.timeout } : {}),
14483
+ };
14484
+ }),
14485
+ }));
14486
+ }
14487
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(hooksOut, "hooks.json"), `${JSON.stringify(pluginHooks, null, 2)}\n`, "utf8");
14488
+ // Drop committed scripts the install path no longer emits (e.g. kage-prompt-context.sh,
14489
+ // now subsumed by observe.sh handling UserPromptSubmit).
14490
+ const removed = [];
14491
+ for (const stale of (0, node_fs_1.readdirSync)(hooksOut).filter((f) => f.endsWith(".sh") && !scripts.includes(f))) {
14492
+ (0, node_fs_1.unlinkSync)((0, node_path_1.join)(hooksOut, stale));
14493
+ removed.push(stale);
14494
+ }
14495
+ return { scripts, removed, events };
14496
+ }
14497
+ finally {
14498
+ (0, node_fs_1.rmSync)(tmpHome, { recursive: true, force: true });
14499
+ (0, node_fs_1.rmSync)(tmpProject, { recursive: true, force: true });
14500
+ }
14501
+ }
14357
14502
  function upsertJsonMcpServer(path, name, server) {
14358
14503
  ensureDir((0, node_path_1.dirname)(path));
14359
14504
  let config = {};
@@ -14756,19 +14901,13 @@ function observationSignalScore(observation) {
14756
14901
  const causalHits = SIGNAL_CAUSAL_MARKERS.filter((marker) => lower.includes(marker)).length;
14757
14902
  if (causalHits > 0)
14758
14903
  score += Math.min(0.35, 0.25 + (causalHits - 1) * 0.05);
14759
- const citesPath = Boolean(observation.path)
14760
- || /\b[\w.-]+\/[\w./-]+\b/.test(prose)
14761
- || /\b[\w-]+\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|rb|json|ya?ml|toml|md|sh|css|html|sql)\b/i.test(prose);
14904
+ const citesPath = Boolean(observation.path) || textCitesPath(prose);
14762
14905
  if (citesPath)
14763
14906
  score += 0.2;
14764
- const codeIdentifier = /\b[a-z][a-z0-9]*[A-Z]\w*\b/.test(prose) // camelCase
14765
- || /\b[a-z0-9]+_[a-z0-9_]+\b/.test(prose) // snake_case
14766
- || /`[^`]+`/.test(prose)
14767
- || /\b\w+\(\)/.test(prose);
14907
+ const codeIdentifier = textHasCodeIdentifier(prose);
14768
14908
  if (codeIdentifier)
14769
14909
  score += 0.15;
14770
- const commandLine = Boolean(observation.command)
14771
- || /(^|\s)(npm|pnpm|yarn|npx|node|git|cargo|make|pytest|go|tsc|kage)\s+[\w.-]/.test(prose);
14910
+ const commandLine = Boolean(observation.command) || textHasCommand(prose);
14772
14911
  if (commandLine)
14773
14912
  score += 0.15;
14774
14913
  const words = combined.split(/\s+/).filter(Boolean).length;
@@ -15488,20 +15627,17 @@ function kageResume(projectDir) {
15488
15627
  updated_at: packetRecency(packet),
15489
15628
  age: humanPacketAge(packetRecency(packet)),
15490
15629
  }));
15491
- const hasSessionContent = Boolean(lastSession || lastChangeMemory || pendingAutoDistilled || reconciliation.unresolved_count);
15492
- const hasContent = hasSessionContent || recentMemory.length > 0;
15630
+ // A new session starts on a fresh task, so SessionStart does NOT replay last session's files,
15631
+ // commands, or a recency-ranked memory list — that was pre-task noise (it fires before the
15632
+ // first prompt, so it can't be task-targeted) and the surface that leaked raw command dumps and
15633
+ // junk packets. Task-relevant memory is pulled on the first prompt via prompt-context; recall
15634
+ // at the moment a file is read via file-context. SessionStart carries only always-on curated
15635
+ // repo facts (the pinned block, below) plus a few actionable open-thread pointers.
15636
+ const hasSessionContent = Boolean(lastChangeMemory || pendingAutoDistilled || reconciliation.unresolved_count);
15637
+ const hasContent = hasSessionContent;
15493
15638
  const lines = [];
15494
- if (hasContent) {
15495
- lines.push("# Previously (Kage)");
15496
- if (lastSession) {
15497
- lines.push(`Last session ${lastSession.session_id} (${lastSession.observations} observation${lastSession.observations === 1 ? "" : "s"}, ended ${lastSession.last_at}).`);
15498
- if (lastSession.paths.length)
15499
- lines.push(`Worked on: ${lastSession.paths.join(", ")}`);
15500
- if (lastSession.commands.length)
15501
- lines.push(`Commands: ${lastSession.commands.join("; ")}`);
15502
- if (lastSession.distilled_titles.length)
15503
- lines.push(`Learned: ${lastSession.distilled_titles.join("; ")}`);
15504
- }
15639
+ if (hasSessionContent) {
15640
+ lines.push("# Open threads (Kage)");
15505
15641
  if (lastChangeMemory) {
15506
15642
  lines.push(`Change memory: ${lastChangeMemory.title} — ${lastChangeMemory.summary}`);
15507
15643
  }
@@ -15514,22 +15650,7 @@ function kageResume(projectDir) {
15514
15650
  lines.push(` - ${item.packet_id}: ${item.title}`);
15515
15651
  }
15516
15652
  }
15517
- // Compact timeline index: one line per recent packet, full detail (summary)
15518
- // only for the newest few, hard-capped to the resume token budget.
15519
- const block = lines.slice(0, 15);
15520
- if (recentPackets.length) {
15521
- block.push("", "## Recent memory");
15522
- recentPackets.forEach((packet, position) => {
15523
- const entry = `[${packet.id.slice(0, 12)}] ${packet.type} ${packet.title} (${humanPacketAge(packetRecency(packet))})`;
15524
- const candidate = [entry];
15525
- if (position < RESUME_TIMELINE_DETAILED && packet.summary) {
15526
- const summary = packet.summary.replace(/\s+/g, " ").trim();
15527
- candidate.push(` ${summary.length > 160 ? `${summary.slice(0, 157)}...` : summary}`);
15528
- }
15529
- if (estimateTokens([...block, ...candidate].join("\n")) <= RESUME_CONTEXT_TOKEN_BUDGET)
15530
- block.push(...candidate);
15531
- });
15532
- }
15653
+ const block = lines.slice(0, 12);
15533
15654
  // Lead the SessionStart injection with the team's pinned, always-on repo memory (the
15534
15655
  // curated high-signal facts), not just the recent-timeline digest — parity with recall's
15535
15656
  // context block, so a new session starts already holding the key knowledge, not only a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "2.5.4",
3
+ "version": "2.5.6",
4
4
  "description": "Team memory for coding agents: captures the decisions, runbooks, and bug fixes that get lost, verified against your code and shared via git. MCP server, zero deps, no account.",
5
5
  "main": "dist/index.js",
6
6
  "files": [