@kage-core/kage-graph-mcp 1.1.3 → 1.1.5

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
@@ -38,6 +38,7 @@ kage recall "how do I run tests" --project /path/to/repo
38
38
  kage recall "how do I run tests" --project /path/to/repo --explain --json
39
39
  kage quality --project /path/to/repo
40
40
  kage benchmark --project /path/to/repo
41
+ kage benchmark --project /path/to/repo --compare --task "how do I run tests"
41
42
  kage viewer --project /path/to/repo
42
43
  kage daemon start --project /path/to/repo
43
44
  kage observe --project /path/to/repo --event '{"type":"command_result","session_id":"s1","command":"npm test","exit_code":0}'
@@ -125,6 +126,12 @@ and parser coverage, code graph counts, evidence coverage, approved vs pending
125
126
  memory, validation status, estimated tokens saved per recall, duplicate
126
127
  candidates, average memory quality, and a readiness score.
127
128
 
129
+ Use `kage benchmark --compare --task "<task>" --project <repo>` or
130
+ `kage_benchmark_compare` to compare the same task on the same repo with and
131
+ without Kage. It estimates manual full-file rediscovery tokens/steps, compares
132
+ them to compact Kage recall plus code graph context, and returns evidence plus
133
+ caveats for honest marketing proof.
134
+
128
135
  Use `kage refresh --project <repo>` or the `kage_refresh` MCP tool after
129
136
  meaningful file changes. Refresh rebuilds indexes, code graph, memory graph,
130
137
  metrics, and stale-memory metadata. Memory is marked stale when status or
@@ -204,6 +211,7 @@ Local repo tools:
204
211
  - `kage_pr_check`
205
212
  - `kage_quality`
206
213
  - `kage_benchmark`
214
+ - `kage_benchmark_compare`
207
215
  - `kage_setup_agent`
208
216
  - `kage_graph`
209
217
  - `kage_graph_visual`
package/dist/cli.js CHANGED
@@ -31,6 +31,7 @@ Usage:
31
31
  kage metrics --project <dir> [--json]
32
32
  kage quality --project <dir> [--json]
33
33
  kage benchmark --project <dir> [--json]
34
+ kage benchmark --project <dir> --compare --task <task> [--json]
34
35
  kage code-graph --project <dir> [--json]
35
36
  kage code-graph "<query>" --project <dir> [--json]
36
37
  kage graph --project <dir> [--json]
@@ -533,6 +534,46 @@ async function main() {
533
534
  return;
534
535
  }
535
536
  if (command === "benchmark") {
537
+ if (args.includes("--compare")) {
538
+ const result = (0, kernel_js_1.benchmarkTaskComparison)(projectArg(args), takeArg(args, "--task") ?? firstPositional(args) ?? "how do I run tests");
539
+ if (args.includes("--json")) {
540
+ console.log(JSON.stringify(result, null, 2));
541
+ return;
542
+ }
543
+ console.log(`Kage A/B Benchmark: ${result.project_dir}`);
544
+ console.log(`Task: ${result.task}`);
545
+ console.log("");
546
+ console.log("Without Kage:");
547
+ console.log(` Files examined: ${result.baseline_without_kage.files_examined}`);
548
+ console.log(` Full-file tokens: ${result.baseline_without_kage.full_file_tokens}`);
549
+ console.log(` Steps: ${result.baseline_without_kage.steps}`);
550
+ console.log(` Estimated time: ${result.baseline_without_kage.estimated_time_seconds}s`);
551
+ console.log("");
552
+ console.log("With Kage:");
553
+ console.log(` Memory packets: ${result.with_kage.memory_packets_used}`);
554
+ console.log(` Code facts: ${result.with_kage.code_files_returned + result.with_kage.code_symbols_returned + result.with_kage.code_routes_returned + result.with_kage.code_tests_returned}`);
555
+ console.log(` Context tokens: ${result.with_kage.context_tokens}`);
556
+ console.log(` Steps: ${result.with_kage.steps}`);
557
+ console.log(` Estimated time: ${result.with_kage.estimated_time_seconds}s`);
558
+ console.log("");
559
+ console.log("Delta:");
560
+ console.log(` Estimated tokens saved: ${result.delta.estimated_tokens_saved}`);
561
+ console.log(` Context reduction: ${result.delta.context_reduction_percent}%`);
562
+ console.log(` Rediscovery steps saved: ${result.delta.rediscovery_steps_saved}`);
563
+ console.log(` Estimated time saved: ${result.delta.estimated_time_saved_seconds}s`);
564
+ console.log(` Full-file reads avoided: ${result.delta.full_file_reads_avoided}`);
565
+ console.log(` Recall hit: ${result.delta.recall_hit ? "yes" : "no"}`);
566
+ console.log(` Code graph hit: ${result.delta.code_graph_hit ? "yes" : "no"}`);
567
+ console.log("");
568
+ console.log("Baseline files:");
569
+ for (const file of result.evidence.baseline_files.slice(0, 8))
570
+ console.log(` - ${file.path} (${file.tokens} tokens): ${file.why}`);
571
+ console.log("");
572
+ console.log("Kage memory:");
573
+ for (const packet of result.evidence.kage_memory.slice(0, 5))
574
+ console.log(` - ${packet.title} (${packet.type}, score ${packet.score})`);
575
+ return;
576
+ }
536
577
  const result = (0, kernel_js_1.benchmarkProject)(projectArg(args));
537
578
  if (args.includes("--json")) {
538
579
  console.log(JSON.stringify(result, null, 2));
package/dist/index.js CHANGED
@@ -209,6 +209,18 @@ function listTools() {
209
209
  required: ["project_dir"],
210
210
  },
211
211
  },
212
+ {
213
+ name: "kage_benchmark_compare",
214
+ description: "Compare the same task on the same repo with and without Kage. Reports estimated baseline discovery tokens/steps versus Kage recall/code-graph context, with evidence and caveats.",
215
+ inputSchema: {
216
+ type: "object",
217
+ properties: {
218
+ project_dir: { type: "string" },
219
+ task: { type: "string" },
220
+ },
221
+ required: ["project_dir", "task"],
222
+ },
223
+ },
212
224
  {
213
225
  name: "kage_setup_agent",
214
226
  description: "Generate MCP/setup instructions for Codex, Claude Code, Cursor, Windsurf, Gemini CLI, OpenCode, Cline, Goose, Roo Code, Kilo Code, Claude Desktop, Aider, or generic MCP.",
@@ -661,6 +673,12 @@ async function callTool(name, args) {
661
673
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
662
674
  };
663
675
  }
676
+ if (name === "kage_benchmark_compare") {
677
+ const result = (0, kernel_js_1.benchmarkTaskComparison)(String(args?.project_dir ?? ""), String(args?.task ?? ""));
678
+ return {
679
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
680
+ };
681
+ }
664
682
  if (name === "kage_setup_agent") {
665
683
  const result = (0, kernel_js_1.setupAgent)(String(args?.agent ?? ""), String(args?.project_dir ?? ""), { write: Boolean(args?.write) });
666
684
  return {
package/dist/kernel.js CHANGED
@@ -75,6 +75,7 @@ exports.graphMermaid = graphMermaid;
75
75
  exports.kageMetrics = kageMetrics;
76
76
  exports.qualityReport = qualityReport;
77
77
  exports.benchmarkProject = benchmarkProject;
78
+ exports.benchmarkTaskComparison = benchmarkTaskComparison;
78
79
  exports.learn = learn;
79
80
  exports.capture = capture;
80
81
  exports.createPublicCandidate = createPublicCandidate;
@@ -180,11 +181,23 @@ Keep captures concise and future-facing. Do not store raw transcripts.
180
181
 
181
182
  ## End-Of-Task Proposal
182
183
 
183
- Before finishing a task that changed files, call \`kage_propose_from_diff\`.
184
+ After meaningful file changes, call \`kage_refresh\` so indexes, code graph,
185
+ memory graph, metrics, and stale-memory checks are current.
184
186
 
185
- This writes a branch review summary and a repo-local change-memory packet. It
186
- should capture what changed, why it matters, how to verify it, and what future
187
- agents should know. Git or PR review is the repo-level review boundary.
187
+ Before finishing a task that changed files, call \`kage_pr_summarize\` or
188
+ \`kage_propose_from_diff\`, then call \`kage_pr_check\`.
189
+
190
+ \`kage_pr_summarize\` writes a branch review summary and a repo-local
191
+ change-memory packet. \`kage_pr_check\` verifies validation, graph freshness,
192
+ stale packets, and whether repo memory changed with the branch. If the check
193
+ fails, explain the required actions instead of hiding the failure. Git or PR
194
+ review is the repo-level review boundary.
195
+
196
+ ## Package Updates
197
+
198
+ If the user asks to update Kage, run \`kage upgrade\`, then verify setup with
199
+ \`kage setup verify-agent --agent <agent> --project <repo>\`. Tell the user to
200
+ restart the agent when MCP tools need to reload.
188
201
 
189
202
  ## Feedback
190
203
 
@@ -210,7 +223,9 @@ For normal coding tasks:
210
223
  4. \`kage_graph\` for remembered decisions, bugs, workflows, and conventions
211
224
  5. Work on the task
212
225
  6. \`kage_learn\` for concrete learnings
213
- 7. \`kage_propose_from_diff\` before the final response to create repo-local change memory
226
+ 7. \`kage_refresh\` after meaningful file changes
227
+ 8. \`kage_pr_summarize\` or \`kage_propose_from_diff\` before the final response to create repo-local change memory
228
+ 9. \`kage_pr_check\` before final handoff or merge readiness claims
214
229
 
215
230
  For quick factual questions, \`kage_recall\` alone is enough. For status or demo requests, call \`kage_metrics\`.
216
231
  ${AGENTS_POLICY_END}
@@ -3050,6 +3065,110 @@ function benchmarkProject(projectDir) {
3050
3065
  },
3051
3066
  };
3052
3067
  }
3068
+ function baselineDiscoveryFiles(projectDir, task) {
3069
+ const terms = tokenize(task);
3070
+ const graph = buildCodeGraph(projectDir);
3071
+ const candidatePaths = unique([
3072
+ "README.md",
3073
+ "AGENTS.md",
3074
+ "CLAUDE.md",
3075
+ "package.json",
3076
+ ...graph.files.map((file) => file.path),
3077
+ ]).filter((path) => path && !shouldSkipRepoMemoryPath(path));
3078
+ return candidatePaths
3079
+ .map((path) => {
3080
+ const absolute = (0, node_path_1.join)(projectDir, path);
3081
+ if (!(0, node_fs_1.existsSync)(absolute))
3082
+ return null;
3083
+ const stats = (0, node_fs_1.statSync)(absolute);
3084
+ if (!stats.isFile() || stats.size > 240_000)
3085
+ return null;
3086
+ const text = (0, node_fs_1.readFileSync)(absolute, "utf8");
3087
+ const score = scoreText(terms, `${path}\n${text.slice(0, 8000)}`, [path]);
3088
+ const alwaysUseful = ["README.md", "AGENTS.md", "CLAUDE.md", "package.json"].includes(path);
3089
+ if (score <= 0 && !alwaysUseful)
3090
+ return null;
3091
+ return {
3092
+ path,
3093
+ tokens: Math.max(1, Math.ceil(stats.size / 4)),
3094
+ why: score > 0 ? "task terms matched path or file content" : "standard repo orientation file",
3095
+ score: score + (alwaysUseful ? 1 : 0),
3096
+ };
3097
+ })
3098
+ .filter((entry) => Boolean(entry))
3099
+ .sort((a, b) => b.score - a.score || b.tokens - a.tokens || a.path.localeCompare(b.path))
3100
+ .slice(0, 10);
3101
+ }
3102
+ function benchmarkTaskComparison(projectDir, task) {
3103
+ ensureMemoryDirs(projectDir);
3104
+ const query = task.trim() || "how do I run tests";
3105
+ const baselineFiles = baselineDiscoveryFiles(projectDir, query);
3106
+ const baselineTokens = baselineFiles.reduce((sum, file) => sum + file.tokens, 0);
3107
+ const recallResult = recall(projectDir, query, 5, true);
3108
+ const codeResult = queryCodeGraph(projectDir, query, 10);
3109
+ const kageContext = `${recallResult.context_block}\n\n${codeResult.context_block}`;
3110
+ const kageTokens = estimateTokens(kageContext);
3111
+ const codeFactLines = [
3112
+ ...codeResult.routes.map((route) => `[route] ${route.method} ${route.path} in ${route.file_path}:${route.line}`),
3113
+ ...codeResult.symbols.map((symbol) => `[symbol] ${symbol.kind} ${symbol.name} in ${symbol.path}:${symbol.line}`),
3114
+ ...codeResult.tests.map((test) => `[test] ${test.title} in ${test.test_path}:${test.line}`),
3115
+ ...codeResult.files.slice(0, 5).map((file) => `[file] ${file.path} (${file.kind}, ${file.language}, ${file.parser})`),
3116
+ ];
3117
+ const baselineSteps = Math.max(3, baselineFiles.length + 2);
3118
+ const kageSteps = 3;
3119
+ const tokensSaved = Math.max(0, baselineTokens - kageTokens);
3120
+ const contextReduction = baselineTokens > 0 ? percent(tokensSaved, baselineTokens) : 0;
3121
+ const timeSaved = Math.max(0, baselineSteps * 45 - kageSteps * 12);
3122
+ return {
3123
+ schema_version: 1,
3124
+ project_dir: projectDir,
3125
+ task: query,
3126
+ generated_at: nowIso(),
3127
+ baseline_without_kage: {
3128
+ strategy: "manual_repo_discovery_estimate",
3129
+ files_examined: baselineFiles.length,
3130
+ full_file_tokens: baselineTokens,
3131
+ steps: baselineSteps,
3132
+ estimated_time_seconds: baselineSteps * 45,
3133
+ },
3134
+ with_kage: {
3135
+ strategy: "recall_plus_code_graph",
3136
+ recall_results: recallResult.results.length,
3137
+ memory_packets_used: recallResult.results.length,
3138
+ code_files_returned: codeResult.files.length,
3139
+ code_symbols_returned: codeResult.symbols.length,
3140
+ code_routes_returned: codeResult.routes.length,
3141
+ code_tests_returned: codeResult.tests.length,
3142
+ context_tokens: kageTokens,
3143
+ steps: kageSteps,
3144
+ estimated_time_seconds: kageSteps * 12,
3145
+ },
3146
+ delta: {
3147
+ estimated_tokens_saved: tokensSaved,
3148
+ context_reduction_percent: contextReduction,
3149
+ rediscovery_steps_saved: Math.max(0, baselineSteps - kageSteps),
3150
+ estimated_time_saved_seconds: timeSaved,
3151
+ full_file_reads_avoided: Math.max(0, baselineFiles.length - codeResult.files.length),
3152
+ recall_hit: recallResult.results.length > 0,
3153
+ code_graph_hit: codeFactLines.length > 0,
3154
+ },
3155
+ evidence: {
3156
+ baseline_files: baselineFiles.map(({ path, tokens, why }) => ({ path, tokens, why })),
3157
+ kage_memory: recallResult.results.map((entry) => ({
3158
+ id: entry.packet.id,
3159
+ title: entry.packet.title,
3160
+ type: entry.packet.type,
3161
+ score: entry.score,
3162
+ })),
3163
+ kage_code_facts: codeFactLines.slice(0, 12),
3164
+ },
3165
+ caveats: [
3166
+ "Baseline is a deterministic manual-discovery estimate, not a live human or agent timing trace.",
3167
+ "Token savings estimate full-file reads avoided versus compact Kage recall/code-graph context.",
3168
+ "Use this for relative proof on the same repo/task, not cross-repo absolute claims.",
3169
+ ],
3170
+ };
3171
+ }
3053
3172
  function kageMetricsShallow(projectDir) {
3054
3173
  const codeGraph = buildCodeGraph(projectDir);
3055
3174
  const knowledgeGraph = buildKnowledgeGraph(projectDir);
@@ -3414,22 +3533,42 @@ Before making code changes or answering implementation questions:
3414
3533
  3. Call kage_code_graph for file, symbol, route, test, or dependency questions.
3415
3534
  4. Call kage_graph for decisions, bugs, workflows, and conventions.
3416
3535
  When you learn something reusable: kage_learn.
3417
- Before finishing a task that changed files: kage_propose_from_diff.
3536
+ After meaningful file changes: kage_refresh.
3537
+ Before finishing a task that changed files: kage_pr_summarize or kage_propose_from_diff, then kage_pr_check.
3418
3538
  If recalled memory helped: kage_feedback helpful. If wrong or stale: kage_feedback wrong or stale."
3419
3539
  fi
3420
3540
 
3421
3541
  KAGE_MSG="$POLICY" python3 -c "import json,os; print(json.dumps({'systemMessage': os.environ['KAGE_MSG']}))"
3542
+ `;
3543
+ const stopHookScript = `#!/usr/bin/env bash
3544
+ # Kage Stop hook — best-effort repo memory refresh before Claude Code finishes.
3545
+ # Silent if Kage is not initialized in the current project or no git changes exist.
3546
+ set -euo pipefail
3547
+
3548
+ PAYLOAD="$(cat || true)"
3549
+ CWD="$(printf "%s" "$PAYLOAD" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('cwd',''))" 2>/dev/null || echo "")"
3550
+
3551
+ [[ -d "$CWD/.agent_memory" ]] || exit 0
3552
+ command -v kage >/dev/null 2>&1 || exit 0
3553
+
3554
+ if git -C "$CWD" status --porcelain -uall >/dev/null 2>&1 && [[ -n "$(git -C "$CWD" status --porcelain -uall)" ]]; then
3555
+ kage refresh --project "$CWD" --json >/dev/null 2>&1 || true
3556
+ kage pr summarize --project "$CWD" --json >/dev/null 2>&1 || true
3557
+ fi
3558
+
3559
+ exit 0
3422
3560
  `;
3423
3561
  const settingsPath = (0, node_path_1.join)(home, ".claude", "settings.json");
3424
3562
  const hookEntry = {
3425
3563
  hooks: {
3426
3564
  SessionStart: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/session-start.sh", timeout: 5 }] }],
3565
+ Stop: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/stop.sh", timeout: 20 }] }],
3427
3566
  },
3428
3567
  };
3429
3568
  setSnippet(path, JSON.stringify({ mcpServers: { kage: server } }, null, 2), [
3430
3569
  "Add the MCP server to ~/.claude.json, then restart Claude Code.",
3431
3570
  "alwaysLoad: true makes Kage tools immediately visible without requiring ToolSearch.",
3432
- `Also create ${hookDir}/session-start.sh with the hook script and add the SessionStart hook to ~/.claude/settings.json.`,
3571
+ `Also create ${hookDir}/session-start.sh and ${hookDir}/stop.sh with the hook scripts and add SessionStart/Stop hooks to ~/.claude/settings.json.`,
3433
3572
  "Run `kage init --project <repo>` inside each repo to install the ambient memory policy.",
3434
3573
  ], true);
3435
3574
  if (options.write) {
@@ -3437,6 +3576,7 @@ KAGE_MSG="$POLICY" python3 -c "import json,os; print(json.dumps({'systemMessage'
3437
3576
  // Install the ambient session-start hook
3438
3577
  (0, node_fs_1.mkdirSync)(hookDir, { recursive: true });
3439
3578
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(hookDir, "session-start.sh"), hookScript, { mode: 0o755 });
3579
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(hookDir, "stop.sh"), stopHookScript, { mode: 0o755 });
3440
3580
  upsertJsonSettings(settingsPath, hookEntry);
3441
3581
  result.wrote = true;
3442
3582
  }
@@ -3862,11 +4002,25 @@ function distillSession(projectDir, sessionId) {
3862
4002
  function createDiffChangeMemory(projectDir, summary) {
3863
4003
  const branch = summary.branch ?? "detached";
3864
4004
  const head = summary.head ?? "unknown";
3865
- const fingerprint = (0, node_crypto_1.createHash)("sha256")
3866
- .update(`${branch}\n${head}\n${summary.changed_files.join("\n")}\n${summary.diff_stat}`)
3867
- .digest("hex")
3868
- .slice(0, 10);
3869
4005
  const title = `Change memory: ${branch}`;
4006
+ // Remove any stale change-memory packets for this branch so propose_from_diff
4007
+ // replaces rather than accumulates. The stable ID (branch-only, no fingerprint)
4008
+ // makes writePacket idempotent going forward; this sweep handles packets that
4009
+ // were written with the old fingerprint-based ID.
4010
+ const stalePrefix = `workflow-${slugify(title)}-`;
4011
+ const stableId = makePacketId(projectDir, "workflow", title);
4012
+ const stableFileName = `${stalePrefix}${(0, node_crypto_1.createHash)("sha256").update(stableId).digest("hex").slice(0, 8)}.json`;
4013
+ try {
4014
+ const existing = (0, node_fs_1.readdirSync)(packetsDir(projectDir)).filter((name) => name.startsWith(stalePrefix) && name !== stableFileName);
4015
+ for (const name of existing) {
4016
+ const stale = (0, node_path_1.join)(packetsDir(projectDir), name);
4017
+ const stalePacket = readJson(stale);
4018
+ if (stalePacket?.type === "workflow" && stalePacket?.title === title) {
4019
+ (0, node_fs_1.unlinkSync)(stale);
4020
+ }
4021
+ }
4022
+ }
4023
+ catch { /* non-fatal */ }
3870
4024
  const verifyCommands = npmScriptCommands(projectDir)
3871
4025
  .filter((command) => /(test|check|lint|build|type|verify)/i.test(command))
3872
4026
  .slice(0, 8);
@@ -3900,7 +4054,7 @@ function createDiffChangeMemory(projectDir, summary) {
3900
4054
  const now = nowIso();
3901
4055
  const packet = {
3902
4056
  schema_version: exports.PACKET_SCHEMA_VERSION,
3903
- id: makePacketId(projectDir, "workflow", title, fingerprint),
4057
+ id: stableId,
3904
4058
  title,
3905
4059
  summary: `Repo-local context for ${summary.changed_files.length} changed repo path${summary.changed_files.length === 1 ? "" : "s"} on ${branch}.`,
3906
4060
  body,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
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": [