@kage-core/kage-graph-mcp 2.3.0 → 2.3.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.
package/dist/cli.js CHANGED
@@ -32,7 +32,7 @@ const FULL_USAGE = `Kage — full command reference
32
32
 
33
33
  Usage:
34
34
  kage index --project <dir>
35
- kage scan --project <dir> [--json]
35
+ kage scan --project <dir> [--json] [--scorecard [--out <file>]]
36
36
  kage demo [--project <dir>]
37
37
  kage install [--project <dir>] [--agents a,b] [--no-agents] [--json]
38
38
  kage init --project <dir> [--with-policy]
@@ -79,6 +79,7 @@ Usage:
79
79
  kage lineage --project <dir> [--json]
80
80
  kage supersede --project <dir> --packet <old-id> --replacement <new-id> [--reason <text>] [--json]
81
81
  kage conflicts --project <dir> [--json]
82
+ kage skills --project <dir> [--dir <path>] [--dry-run] [--json]
82
83
  kage contributors --project <dir> [--json]
83
84
  kage profile --project <dir> [--json]
84
85
  kage xray --project <dir> [--json]
@@ -318,15 +319,30 @@ async function main() {
318
319
  console.log(JSON.stringify(result, null, 2));
319
320
  return;
320
321
  }
322
+ if (args.includes("--scorecard")) {
323
+ // A shareable artifact, not terminal output: write the SVG (or Markdown if
324
+ // --out ends in .md) and echo the Markdown table so it's pasteable now.
325
+ const out = takeArg(args, "--out");
326
+ const asMarkdown = out ? out.toLowerCase().endsWith(".md") : false;
327
+ const outPath = out ?? (0, node_path_1.join)(scanTarget, "kage-scorecard.svg");
328
+ (0, node_fs_1.writeFileSync)(outPath, asMarkdown ? (0, kernel_js_1.truthScorecardMarkdown)(result) : (0, kernel_js_1.truthScorecardSvg)(result), "utf8");
329
+ console.log(`Scorecard written to ${outPath}`);
330
+ console.log("Share it: embed the SVG in a README, screenshot it, or post it.\n");
331
+ console.log((0, kernel_js_1.truthScorecardMarkdown)(result));
332
+ return;
333
+ }
321
334
  console.log(`Kage Truth Report — ${result.project_dir}`);
322
335
  console.log(`Scanned ${result.totals.files_scanned} files, ${result.totals.symbols_scanned} symbols${result.totals.docs_scanned ? `, ${result.totals.docs_scanned} doc file(s)` : ""}\n`);
323
- console.log(` ${result.headline}\n`);
336
+ console.log(result.headline ? ` ${result.headline}\n` : "");
324
337
  const sections = [
325
- { kind: "duplicate_cluster", heading: "DUPLICATE IMPLEMENTATIONS", count: result.totals.duplicate_clusters },
326
- { kind: "ghost_export", heading: "GHOST KNOWLEDGEexported, never called", count: result.totals.ghost_exports },
327
- { kind: "bus_factor", heading: "BUS FACTOR 1 one head holds it all", count: result.totals.bus_factor_files },
328
- { kind: "knowledge_void", heading: "KNOWLEDGE VOIDhigh churn, zero memory", count: result.totals.knowledge_voids },
329
- { kind: "doc_lie", heading: "DOC LIESthe README vs reality", count: result.totals.doc_lies },
338
+ { kind: "knowledge_void", heading: "KNOWLEDGE VOID — high churn, zero memory", clean: "no undocumented hot files", count: result.totals.knowledge_voids },
339
+ { kind: "untested_hot", heading: "UNTESTED HOT PATH depended on, no test covers it", clean: "every hot path has a test", count: result.totals.untested_hot_paths },
340
+ { kind: "complexity_hotspot", heading: "COMPLEXITY HOTSPOTbig file, many dependents", clean: "no oversized hub files", count: result.totals.complexity_hotspots },
341
+ { kind: "debt_marker", heading: "KNOWN DEBTTODO / FIXME / HACK left in code", clean: "no debt markers in code", count: result.totals.debt_markers },
342
+ { kind: "bus_factor", heading: "BUS FACTOR 1 one head holds it all", clean: "no single-owner hot files", count: result.totals.bus_factor_files },
343
+ { kind: "duplicate_cluster", heading: "DUPLICATE IMPLEMENTATIONS", clean: "no duplicate implementations", count: result.totals.duplicate_clusters },
344
+ { kind: "ghost_export", heading: "GHOST KNOWLEDGE — exported, never called", clean: "no dead exports", count: result.totals.ghost_exports },
345
+ { kind: "doc_lie", heading: "DOC LIES — the README vs reality", clean: "docs match the code", count: result.totals.doc_lies },
330
346
  ];
331
347
  for (const section of sections) {
332
348
  const items = result.findings.filter((finding) => finding.kind === section.kind);
@@ -341,6 +357,14 @@ async function main() {
341
357
  }
342
358
  console.log("");
343
359
  }
360
+ // Reframe the categories that came back clean as reassurance, not emptiness.
361
+ // (doc checks only run when docs exist, so skip that line when none scanned.)
362
+ const cleanLabels = sections
363
+ .filter((section) => section.count === 0 && !(section.kind === "doc_lie" && !result.totals.docs_scanned))
364
+ .map((section) => section.clean);
365
+ if (result.findings.length && cleanLabels.length) {
366
+ console.log(`Clean: ${cleanLabels.join(" · ")}\n`);
367
+ }
344
368
  if (!result.findings.length) {
345
369
  const small = result.totals.files_scanned < 30;
346
370
  const noGit = result.warnings.some((warning) => warning.includes("Git history"));
@@ -466,6 +490,11 @@ async function main() {
466
490
  : null;
467
491
  const detected = requested ?? probes.filter((p) => p.paths.some((path) => (0, node_fs_1.existsSync)(path))).map((p) => p.agent);
468
492
  const init = (0, kernel_js_1.initProject)(project, { policy: false });
493
+ // Always write the repo policy (AGENTS.md + CLAUDE.md) — it is what instructs
494
+ // agents to use Kage and it travels with the repo, so teammates who clone are
495
+ // covered even before they wire their own agent. Decoupled from agent detection:
496
+ // a machine where no agent is auto-detected must still commit the policy.
497
+ const policy = (0, kernel_js_1.installAgentPolicy)(project);
469
498
  const wired = [];
470
499
  if (!skipAgents) {
471
500
  for (const agent of detected) {
@@ -487,6 +516,7 @@ async function main() {
487
516
  console.log(`Kage installed in ${init.index.projectDir}\n`);
488
517
  console.log(" Memory .agent_memory/ created — packets are plain files, reviewable in git");
489
518
  console.log(` Indexes ${init.index.indexes.length} built (code graph, recall, structure)`);
519
+ console.log(` Policy AGENTS.md + CLAUDE.md ${policy.created ? "written" : policy.updated ? "updated" : "current"} — commit these so every teammate's agent uses Kage`);
490
520
  if (skipAgents) {
491
521
  console.log(" Agents skipped (--no-agents)");
492
522
  }
@@ -501,13 +531,27 @@ async function main() {
501
531
  console.log(` Agents ${w.agent} ✗ ${w.error ?? "print-only; run kage setup " + w.agent + " --project . --write"}`);
502
532
  }
503
533
  }
504
- console.log("\nNext:");
505
- console.log(" restart your agent memory now recalls automatically at session start");
506
- console.log(" kage scan --project . 60-second Truth Report on this repo");
507
- console.log(" kage viewer --project . local dashboard (gains, packets, graph)");
508
- console.log("\nVersion control: commit .agent_memory/packets/, ignore .agent_memory/indexes/ and reports/.");
509
- console.log("Enable the packet merge driver once per clone:");
510
- console.log(` ${kernel_js_1.PACKET_MERGE_DRIVER_CONFIG}`);
534
+ // Do the version-control housekeeping instead of telling the user to. Both are
535
+ // idempotent and safe to skip if this isn't a git repo.
536
+ let vcDone = false;
537
+ try {
538
+ const gitignorePath = (0, node_path_1.join)(project, ".gitignore");
539
+ const want = [".agent_memory/indexes/", ".agent_memory/reports/"];
540
+ const current = (0, node_fs_1.existsSync)(gitignorePath) ? (0, node_fs_1.readFileSync)(gitignorePath, "utf8") : "";
541
+ const missing = want.filter((line) => !current.split("\n").some((l) => l.trim() === line));
542
+ if (missing.length) {
543
+ const prefix = current && !current.endsWith("\n") ? "\n" : "";
544
+ (0, node_fs_1.writeFileSync)(gitignorePath, `${current}${prefix}${current ? "" : "# Kage: regenerated, not committed\n"}${missing.join("\n")}\n`, "utf8");
545
+ }
546
+ (0, node_child_process_1.execFileSync)("git", ["-C", project, "config", "merge.kage-packet.driver", "npx -y @kage-core/kage-graph-mcp merge-packet %A %O %B"], { stdio: "ignore" });
547
+ vcDone = true;
548
+ }
549
+ catch { /* not a git repo or no git — fall back to a hint */ }
550
+ console.log(` Git ${vcDone ? ".gitignore + packet merge driver configured" : "skipped (not a git repo)"}`);
551
+ console.log("\nNext: restart your agent — Kage then recalls automatically every session.");
552
+ console.log(" kage scan a 60-second Truth Report on this repo");
553
+ if (!vcDone)
554
+ console.log(`\nWhen this becomes a git repo, run once: ${kernel_js_1.PACKET_MERGE_DRIVER_CONFIG}`);
511
555
  if (!init.validation.ok)
512
556
  process.exit(2);
513
557
  return;
@@ -1306,7 +1350,13 @@ async function main() {
1306
1350
  const plural = (count, singular, pluralForm) => (count === 1 ? singular : pluralForm);
1307
1351
  const week = summary.last_7d;
1308
1352
  if (!summary.all_time.recalls && !summary.all_time.stale_withheld && !summary.all_time.stale_caught && !summary.all_time.caller_answers) {
1309
- console.log("No value events recorded yet. Run kage recall (or let your agent call kage_context) and check back.");
1353
+ console.log("No value events recorded yet this ledger fills up as your agent works.");
1354
+ console.log("Every recall logs the tokens it saved (by not re-reading cited files) and every");
1355
+ console.log("stale memory it withheld. Come back after a session and you'll see a receipt here.\n");
1356
+ console.log("Start now:");
1357
+ console.log(" kage scan --project . a 60-second Truth Report on this repo");
1358
+ console.log(" kage scan --project . --scorecard a shareable scorecard you can post");
1359
+ console.log(" then just work — your agent captures and recalls, verified against this code.");
1310
1360
  return;
1311
1361
  }
1312
1362
  console.log(`This week Kage saved you ~${(0, kernel_js_1.formatTokenCount)(week.tokens_saved)} tokens (~$${week.estimated_dollars.toFixed(2)}), ` +
@@ -1429,6 +1479,36 @@ async function main() {
1429
1479
  process.exit(2);
1430
1480
  return;
1431
1481
  }
1482
+ if (command === "skills" || command === "skills-build") {
1483
+ const result = (0, kernel_js_1.generateSkills)(projectArg(args), {
1484
+ dryRun: args.includes("--dry-run"),
1485
+ dir: takeArg(args, "--dir"),
1486
+ });
1487
+ if (args.includes("--json")) {
1488
+ console.log(JSON.stringify(result, null, 2));
1489
+ }
1490
+ else {
1491
+ const verb = result.dry_run ? "Would generate" : "Generated";
1492
+ console.log(`Kage skills: ${verb} ${result.generated.length} skill file(s) in ${result.dir}/ from verified memory`);
1493
+ for (const skill of result.generated)
1494
+ console.log(` ${result.dry_run ? "·" : "✓"} ${skill.path} (${skill.type})`);
1495
+ if (result.skipped.length)
1496
+ console.log(` skipped ${result.skipped.length} packet(s) not skill-worthy or not grounded`);
1497
+ if (result.errors.length)
1498
+ console.log(` errors: ${result.errors.join("; ")}`);
1499
+ if (!result.dry_run && result.generated.length) {
1500
+ if (result.git_ignored) {
1501
+ console.log(`\n⚠ ${result.dir}/ is git-ignored, so these skills won't reach your team. Un-ignore it (or run with --dir <tracked-path>) and commit them.`);
1502
+ }
1503
+ else {
1504
+ console.log(`\nThese are plain files in ${result.dir}/ — commit them so every teammate's agent loads the same skills.`);
1505
+ }
1506
+ }
1507
+ }
1508
+ if (!result.ok)
1509
+ process.exit(2);
1510
+ return;
1511
+ }
1432
1512
  if (command === "reconcile" || command === "memory-reconcile" || command === "memory-reconciliation") {
1433
1513
  const result = (0, kernel_js_1.kageMemoryReconciliation)(projectArg(args), {
1434
1514
  sessionId: takeArg(args, "--session"),
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.CORE_TOOLS = void 0;
4
5
  exports.listTools = listTools;
5
6
  exports.callTool = callTool;
6
7
  const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
@@ -84,14 +85,35 @@ const KAGE_WORKFLOW_TEXT = "Kage memory workflow (this tool performs no action;
84
85
  "4) After meaningful file changes, call kage_refresh so indexes, graphs, and stale-memory checks stay current. " +
85
86
  "5) Before finishing a branch, call kage_pr_summarize then kage_pr_check. " +
86
87
  "Recall receipts show estimated tokens saved versus rediscovery; report memory quality with kage_feedback (helpful/wrong/stale).";
88
+ // Agent-facing core: the verbs an agent actually uses in the loop (recall,
89
+ // capture, stay-honest, refresh, codify). Everything else is operator/diagnostic
90
+ // and must not bloat the model's default tool list — it stays reachable in full
91
+ // mode (KAGE_TOOLS=full) or via the CLI. Keeping the default small enough that
92
+ // the client always-loads it removes the per-call ToolSearch round-trip.
93
+ exports.CORE_TOOLS = new Set([
94
+ "kage_context",
95
+ "kage_learn",
96
+ "kage_supersede",
97
+ "kage_feedback",
98
+ "kage_pr_check",
99
+ "kage_refresh",
100
+ "kage_skills",
101
+ // Promoted after the agent-trajectory eval showed a real agent reaches for
102
+ // these on natural tasks (risk before a change, listing decisions, tracing a
103
+ // dependency path, searching the repo's own docs).
104
+ "kage_risk",
105
+ "kage_decisions",
106
+ "kage_dependency_path",
107
+ "kage_docs_search",
108
+ ]);
87
109
  function listTools() {
88
- return [
110
+ const all = [
89
111
  {
90
112
  // Combined entry-point tool: validate + recall + code_graph + graph in one call.
91
113
  // Agents should load this schema first (one ToolSearch) instead of loading four
92
114
  // separate deferred schemas. Cuts session start from 4 schema loads to 1.
93
115
  name: "kage_context",
94
- description: "Primary kage entry point. Validates memory health, recalls relevant packets, and queries both the code graph and knowledge graph — all in one call. Call this at the start of every task instead of calling kage_validate, kage_recall, kage_code_graph, and kage_graph separately.",
116
+ description: "Primary kage entry point. Validates memory health, recalls relevant packets, and queries both the code graph and knowledge graph — all in one call. Call this at the start of every task; it answers caller/usage questions from the code graph too, so you rarely need a separate graph tool.",
95
117
  inputSchema: {
96
118
  type: "object",
97
119
  properties: {
@@ -193,20 +215,6 @@ function listTools() {
193
215
  required: ["project_dir"],
194
216
  },
195
217
  },
196
- {
197
- name: "kage_code_graph",
198
- description: "Query the source-derived codebase graph: files, symbols, imports, calls, routes, tests, package scripts. This is generated from code, not learned memory.",
199
- inputSchema: {
200
- type: "object",
201
- properties: {
202
- project_dir: { type: "string" },
203
- query: { type: "string" },
204
- limit: { type: "number" },
205
- json: { type: "boolean" },
206
- },
207
- required: ["project_dir"],
208
- },
209
- },
210
218
  {
211
219
  name: "kage_risk",
212
220
  description: "Assess modification risk for files using Kage's code graph plus local git history: dependents, impact surface, churn, ownership, co-change partners, and test gaps. Use before editing hotspot or shared files.",
@@ -627,6 +635,19 @@ function listTools() {
627
635
  required: ["project_dir"],
628
636
  },
629
637
  },
638
+ {
639
+ name: "kage_skills",
640
+ description: "Codify durable, verified repo memory (runbooks, workflows, actionable decisions) into git-native SKILL.md files under .claude/skills/ that every teammate's agent auto-loads. Only grounded, non-stale packets become skills. Pass dry_run to preview without writing. dir overrides the output directory.",
641
+ inputSchema: {
642
+ type: "object",
643
+ properties: {
644
+ project_dir: { type: "string" },
645
+ dry_run: { type: "boolean" },
646
+ dir: { type: "string" },
647
+ },
648
+ required: ["project_dir"],
649
+ },
650
+ },
630
651
  {
631
652
  name: "kage_benchmark",
632
653
  description: "Return Kage proof metrics, or set mode=memory_quality / memory_scale for synthetic memory retrieval benchmarks.",
@@ -918,6 +939,9 @@ function listTools() {
918
939
  },
919
940
  },
920
941
  ];
942
+ if (process.env.KAGE_TOOLS === "full" || process.env.KAGE_ALL_TOOLS === "1")
943
+ return all;
944
+ return all.filter((tool) => exports.CORE_TOOLS.has(tool.name));
921
945
  }
922
946
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
923
947
  tools: listTools(),
@@ -1082,8 +1106,14 @@ async function callTool(name, args) {
1082
1106
  if (docsSection)
1083
1107
  result.context_block = `${result.context_block}\n\n${docsSection}`;
1084
1108
  }
1109
+ // Visible receipt: in text mode, surface what this recall saved so the agent
1110
+ // can relay it. Value is otherwise invisible; an unseen win is a churned user.
1111
+ const receipt = result.value_receipt;
1112
+ const gainsLine = receipt && (receipt.tokens_saved > 0 || receipt.stale_withheld > 0)
1113
+ ? `\n\nGains: ~${(0, kernel_js_1.formatTokenCount)(receipt.tokens_saved)} tokens saved by this recall${receipt.stale_withheld ? ` · stale memories withheld: ${receipt.stale_withheld}` : ""}`
1114
+ : "";
1085
1115
  return {
1086
- content: [{ type: "text", text: args?.json || args?.explain ? JSON.stringify(result, null, 2) : result.context_block }],
1116
+ content: [{ type: "text", text: args?.json || args?.explain ? JSON.stringify(result, null, 2) : `${result.context_block}${gainsLine}` }],
1087
1117
  };
1088
1118
  }
1089
1119
  if (name === "kage_docs_search") {
@@ -1105,20 +1135,6 @@ async function callTool(name, args) {
1105
1135
  isError: !result.ok,
1106
1136
  };
1107
1137
  }
1108
- if (name === "kage_code_graph") {
1109
- const projectDir = String(args?.project_dir ?? "");
1110
- const query = typeof args?.query === "string" ? args.query : "";
1111
- if (query) {
1112
- const result = (0, kernel_js_1.queryCodeGraph)(projectDir, query, Number(args?.limit ?? 10));
1113
- return {
1114
- content: [{ type: "text", text: args?.json ? JSON.stringify(result, null, 2) : result.context_block }],
1115
- };
1116
- }
1117
- const result = (0, kernel_js_1.buildCodeGraph)(projectDir);
1118
- return {
1119
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1120
- };
1121
- }
1122
1138
  if (name === "kage_risk") {
1123
1139
  const result = (0, kernel_js_1.kageRisk)(String(args?.project_dir ?? ""), arrayArg(args?.targets), arrayArg(args?.changed_files));
1124
1140
  return {
@@ -1266,6 +1282,15 @@ async function callTool(name, args) {
1266
1282
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1267
1283
  };
1268
1284
  }
1285
+ if (name === "kage_skills") {
1286
+ const result = (0, kernel_js_1.generateSkills)(String(args?.project_dir ?? ""), {
1287
+ dryRun: args?.dry_run === true,
1288
+ dir: typeof args?.dir === "string" ? args.dir : undefined,
1289
+ });
1290
+ return {
1291
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1292
+ };
1293
+ }
1269
1294
  if (name === "kage_module_health") {
1270
1295
  const result = (0, kernel_js_1.kageModuleHealth)(String(args?.project_dir ?? ""));
1271
1296
  return {