@kage-core/kage-graph-mcp 1.1.19 → 1.1.20

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
@@ -9,18 +9,16 @@ This package exposes two surfaces:
9
9
 
10
10
  ## Latest Release
11
11
 
12
- `1.1.18` publishes the end-to-end performance pass:
13
-
14
- - read-only commands reuse current graph artifacts instead of rebuilding them
15
- when inputs are fresh.
16
- - MCP sessions keep an in-process graph cache, so repeated agent calls do not
17
- keep reparsing the same graph JSON.
18
- - `kage refresh` reports lightweight freshness metrics and leaves deep
19
- benchmark/quality work to explicit `kage metrics` and `kage benchmark` calls.
20
- - recall builds graph lookup maps once per query instead of scanning all graph
21
- entities and edges for every memory packet.
22
- - `kage init` remains a packet-only bootstrap path; full graph generation stays
23
- with `kage refresh` and `kage index`.
12
+ `1.1.20` publishes the large-repo indexing pass:
13
+
14
+ - repeated `kage refresh` calls reuse unchanged code graph artifacts by source
15
+ stat fingerprint, with `kage refresh --full` available for intentional clean
16
+ rebuilds.
17
+ - `kage code-index` prefers SCIP via `scip-typescript` plus the `scip` CLI when
18
+ those tools are installed, then falls back to Kage's built-in LSP-compatible
19
+ symbol index.
20
+ - read-only commands and MCP sessions reuse current graph artifacts instead of
21
+ rebuilding them when inputs are fresh.
24
22
 
25
23
  `1.1.17` publishes content-based graph freshness:
26
24
 
@@ -116,6 +114,7 @@ kage policy --project /path/to/repo
116
114
  kage doctor --project /path/to/repo
117
115
  kage index --project /path/to/repo
118
116
  kage refresh --project /path/to/repo
117
+ kage refresh --project /path/to/repo --full
119
118
  kage branch --project /path/to/repo
120
119
  kage code-index --project /path/to/repo
121
120
  kage code-graph --project /path/to/repo
@@ -206,11 +205,13 @@ generic extraction in metrics and file parser coverage. This keeps installation
206
205
  light while allowing teams to plug in the strongest indexer available for their
207
206
  language stack.
208
207
 
209
- `kage code-index --project <repo>` writes `.agent_memory/code_index/lsp-symbols.json`
210
- in an LSP document-symbol-compatible shape using Kage's local parser. The CI,
208
+ `kage code-index --project <repo>` now tries the best external indexer first for
209
+ the common JS/TS case: if `scip-typescript` and the `scip` CLI are on the repo or
210
+ shell path, it writes `.agent_memory/code_index/scip.json` from the generated
211
+ SCIP index. If those tools are unavailable, it writes
212
+ `.agent_memory/code_index/lsp-symbols.json` using Kage's local parser. The CI,
211
213
  PR, and sync workflows run it before refresh so the code graph has a committed
212
- precise-index slot while teams can still replace or augment it with SCIP/LSIF
213
- artifacts from their preferred toolchain.
214
+ precise-index slot without making first-run setup depend on external binaries.
214
215
 
215
216
  The memory graph follows the same product direction as temporal context graph
216
217
  systems such as Graphiti: immutable ingestion episodes, derived entities and
@@ -258,7 +259,9 @@ Use `kage refresh --project <repo>` or the `kage_refresh` MCP tool after
258
259
  meaningful file/content changes. Refresh rebuilds indexes, code graph, memory
259
260
  graph, metrics, and stale-memory metadata. Memory is marked stale when status or
260
261
  feedback says it is stale, its TTL expires, or grounded paths disappear. Pushes
261
- and empty/same-tree commits do not need another refresh.
262
+ and empty/same-tree commits do not need another refresh. Use `--full` or
263
+ `kage_refresh` with `full: true` only when you intentionally want to bypass
264
+ unchanged-graph reuse and rebuild the code graph from scratch.
262
265
 
263
266
  Use `kage gc --project <repo> --dry-run` to preview stale packet cleanup.
264
267
  `kage gc --project <repo>` marks stale repo packets deprecated, rebuilds
package/dist/cli.js CHANGED
@@ -24,7 +24,7 @@ Usage:
24
24
  kage daemon status --project <dir> [--json]
25
25
  kage daemon doctor --project <dir> [--json]
26
26
  kage viewer --project <dir> [--port 3113]
27
- kage refresh --project <dir> [--json]
27
+ kage refresh --project <dir> [--full] [--json]
28
28
  kage gc --project <dir> [--dry-run] [--force] [--json]
29
29
  kage pr summarize --project <dir> [--json]
30
30
  kage pr check --project <dir> [--json]
@@ -335,7 +335,7 @@ async function main() {
335
335
  return;
336
336
  }
337
337
  if (command === "refresh") {
338
- const result = (0, kernel_js_1.refreshProject)(projectArg(args));
338
+ const result = (0, kernel_js_1.refreshProject)(projectArg(args), { full: args.includes("--full") });
339
339
  if (args.includes("--json")) {
340
340
  console.log(JSON.stringify(result, null, 2));
341
341
  if (!result.ok)
@@ -506,7 +506,7 @@ async function main() {
506
506
  return;
507
507
  }
508
508
  if (command === "code-index") {
509
- const result = (0, kernel_js_1.writeLspSymbolIndex)(projectArg(args));
509
+ const result = (0, kernel_js_1.writeCodeIndex)(projectArg(args));
510
510
  if (args.includes("--json")) {
511
511
  console.log(JSON.stringify(result, null, 2));
512
512
  return;
@@ -516,6 +516,11 @@ async function main() {
516
516
  console.log(`Path: ${result.path}`);
517
517
  console.log(`Documents: ${result.documents}`);
518
518
  console.log(`Symbols: ${result.symbols}`);
519
+ if (result.warnings.length) {
520
+ console.log("\nWarnings:");
521
+ for (const warning of result.warnings)
522
+ console.log(` - ${warning}`);
523
+ }
519
524
  if (result.errors.length) {
520
525
  console.log("\nErrors:");
521
526
  for (const error of result.errors)
package/dist/index.js CHANGED
@@ -153,6 +153,7 @@ function listTools() {
153
153
  type: "object",
154
154
  properties: {
155
155
  project_dir: { type: "string" },
156
+ full: { type: "boolean", description: "Force a full code graph rebuild instead of reusing unchanged graph artifacts." },
156
157
  },
157
158
  required: ["project_dir"],
158
159
  },
@@ -173,7 +174,7 @@ function listTools() {
173
174
  },
174
175
  {
175
176
  name: "kage_code_index",
176
- description: "Write .agent_memory/code_index/lsp-symbols.json, an LSP-compatible symbol artifact consumed by the code graph for higher parser coverage.",
177
+ description: "Write external code index artifacts consumed by the code graph. Prefers SCIP when scip-typescript and scip are installed, then falls back to the built-in LSP-compatible symbol index.",
177
178
  inputSchema: {
178
179
  type: "object",
179
180
  properties: {
@@ -725,7 +726,7 @@ async function callTool(name, args) {
725
726
  };
726
727
  }
727
728
  if (name === "kage_code_index") {
728
- const result = (0, kernel_js_1.writeLspSymbolIndex)(String(args?.project_dir ?? ""));
729
+ const result = (0, kernel_js_1.writeCodeIndex)(String(args?.project_dir ?? ""));
729
730
  return {
730
731
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
731
732
  isError: !result.ok,
@@ -752,7 +753,7 @@ async function callTool(name, args) {
752
753
  };
753
754
  }
754
755
  if (name === "kage_refresh") {
755
- const result = (0, kernel_js_1.refreshProject)(String(args?.project_dir ?? ""));
756
+ const result = (0, kernel_js_1.refreshProject)(String(args?.project_dir ?? ""), { full: Boolean(args?.full) });
756
757
  return {
757
758
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
758
759
  isError: !result.ok,
package/dist/kernel.js CHANGED
@@ -64,6 +64,7 @@ exports.ensureMemoryDirs = ensureMemoryDirs;
64
64
  exports.loadApprovedPackets = loadApprovedPackets;
65
65
  exports.loadPendingPackets = loadPendingPackets;
66
66
  exports.writeLspSymbolIndex = writeLspSymbolIndex;
67
+ exports.writeCodeIndex = writeCodeIndex;
67
68
  exports.buildCodeGraph = buildCodeGraph;
68
69
  exports.buildKnowledgeGraph = buildKnowledgeGraph;
69
70
  exports.buildIndexes = buildIndexes;
@@ -2177,6 +2178,8 @@ function externalSymbol(projectDir, parser, input) {
2177
2178
  }
2178
2179
  function parseKageExternalIndex(projectDir, parser, path) {
2179
2180
  const raw = readJson(path);
2181
+ if (Array.isArray(raw.documents))
2182
+ return parseScipJsonObject(projectDir, raw);
2180
2183
  const symbols = Array.isArray(raw.symbols)
2181
2184
  ? raw.symbols.flatMap((item) => isRecord(item) ? [externalSymbol(projectDir, parser, item)].filter(Boolean) : [])
2182
2185
  : [];
@@ -2208,6 +2211,62 @@ function parseKageExternalIndex(projectDir, parser, path) {
2208
2211
  : [];
2209
2212
  return { symbols, imports, calls };
2210
2213
  }
2214
+ function scipSymbolName(symbol) {
2215
+ const local = symbol.trim().split(/\s+/).at(-1) ?? symbol;
2216
+ const segment = local.split(/[\/#.`:]/).filter(Boolean).at(-1) ?? local;
2217
+ return segment.replace(/\(\)?$/, "").replace(/\.$/, "") || symbol;
2218
+ }
2219
+ function scipRangeLine(input) {
2220
+ if (Array.isArray(input) && typeof input[0] === "number")
2221
+ return Math.max(1, input[0] + 1);
2222
+ if (isRecord(input) && isRecord(input.start) && typeof input.start.line === "number")
2223
+ return Math.max(1, input.start.line + 1);
2224
+ return 1;
2225
+ }
2226
+ function parseScipJsonObject(projectDir, raw) {
2227
+ const symbols = [];
2228
+ const calls = [];
2229
+ const symbolInfo = new Map();
2230
+ const docs = Array.isArray(raw.documents) ? raw.documents : [];
2231
+ for (const doc of docs) {
2232
+ if (!isRecord(doc))
2233
+ continue;
2234
+ const rel = String(doc.relativePath ?? doc.relative_path ?? doc.path ?? doc.uri ?? "").replace(/^file:\/\//, "").replace(/\\/g, "/");
2235
+ if (!rel)
2236
+ continue;
2237
+ for (const item of Array.isArray(doc.symbols) ? doc.symbols : []) {
2238
+ if (isRecord(item) && typeof item.symbol === "string")
2239
+ symbolInfo.set(item.symbol, item);
2240
+ }
2241
+ for (const occurrence of Array.isArray(doc.occurrences) ? doc.occurrences : []) {
2242
+ if (!isRecord(occurrence) || typeof occurrence.symbol !== "string")
2243
+ continue;
2244
+ const role = Number(occurrence.symbolRoles ?? occurrence.symbol_roles ?? 0);
2245
+ const line = scipRangeLine(occurrence.range);
2246
+ const name = scipSymbolName(occurrence.symbol);
2247
+ if (!name || name === "local")
2248
+ continue;
2249
+ if ((role & 1) === 1) {
2250
+ const info = symbolInfo.get(occurrence.symbol) ?? {};
2251
+ const detail = Array.isArray(info.documentation) ? info.documentation.map(String).find(Boolean) : undefined;
2252
+ const symbol = externalSymbol(projectDir, "scip", {
2253
+ path: rel,
2254
+ name,
2255
+ kind: occurrence.syntaxKind ?? occurrence.syntax_kind ?? info.kind,
2256
+ line,
2257
+ signature: detail ?? name,
2258
+ exported: !occurrence.symbol.startsWith("local "),
2259
+ });
2260
+ if (symbol && !symbols.some((candidate) => candidate.id === symbol.id))
2261
+ symbols.push(symbol);
2262
+ }
2263
+ else {
2264
+ calls.push({ from_symbol: null, to_symbol: name, path: rel, line });
2265
+ }
2266
+ }
2267
+ }
2268
+ return { symbols, imports: [], calls };
2269
+ }
2211
2270
  function parseLspDocumentSymbols(projectDir, path) {
2212
2271
  const raw = readJson(path);
2213
2272
  const docs = Array.isArray(raw) ? raw : isRecord(raw) && Array.isArray(raw.documents) ? raw.documents : [];
@@ -2280,9 +2339,119 @@ function writeLspSymbolIndex(projectDir) {
2280
2339
  parser: "lsp",
2281
2340
  documents: documents.length,
2282
2341
  symbols: symbolCount,
2342
+ warnings: [],
2283
2343
  errors,
2284
2344
  };
2285
2345
  }
2346
+ function executableOnPath(projectDir, command) {
2347
+ const local = (0, node_path_1.join)(projectDir, "node_modules", ".bin", command);
2348
+ if ((0, node_fs_1.existsSync)(local))
2349
+ return local;
2350
+ const localCmd = `${local}.cmd`;
2351
+ if ((0, node_fs_1.existsSync)(localCmd))
2352
+ return localCmd;
2353
+ for (const entry of (process.env.PATH ?? "").split(node_path_1.delimiter).filter(Boolean)) {
2354
+ const candidate = (0, node_path_1.join)(entry, command);
2355
+ if ((0, node_fs_1.existsSync)(candidate))
2356
+ return candidate;
2357
+ const cmdCandidate = `${candidate}.cmd`;
2358
+ if ((0, node_fs_1.existsSync)(cmdCandidate))
2359
+ return cmdCandidate;
2360
+ }
2361
+ return null;
2362
+ }
2363
+ function hasTypeScriptCode(projectDir) {
2364
+ return listCodeFiles(projectDir).some((path) => TS_AST_EXTENSIONS.has(extensionOf(path)));
2365
+ }
2366
+ function scipCliJson(scipCli, scipPath, projectDir) {
2367
+ try {
2368
+ return (0, node_child_process_1.execFileSync)(scipCli, ["print", "--json", scipPath], {
2369
+ cwd: projectDir,
2370
+ encoding: "utf8",
2371
+ stdio: ["ignore", "pipe", "pipe"],
2372
+ });
2373
+ }
2374
+ catch {
2375
+ return (0, node_child_process_1.execFileSync)(scipCli, ["print", scipPath, "--json"], {
2376
+ cwd: projectDir,
2377
+ encoding: "utf8",
2378
+ stdio: ["ignore", "pipe", "pipe"],
2379
+ });
2380
+ }
2381
+ }
2382
+ function writeScipTypescriptIndex(projectDir) {
2383
+ if (!hasTypeScriptCode(projectDir))
2384
+ return null;
2385
+ const scipTypescript = executableOnPath(projectDir, "scip-typescript");
2386
+ if (!scipTypescript)
2387
+ return null;
2388
+ const scipCli = executableOnPath(projectDir, "scip");
2389
+ const outDir = (0, node_path_1.join)(memoryRoot(projectDir), "code_index");
2390
+ ensureDir(outDir);
2391
+ const scipPath = (0, node_path_1.join)(outDir, "index.scip");
2392
+ const outPath = (0, node_path_1.join)(outDir, "scip.json");
2393
+ const warnings = [];
2394
+ const errors = [];
2395
+ try {
2396
+ const args = ["index"];
2397
+ if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(projectDir, "tsconfig.json")))
2398
+ args.push("--infer-tsconfig");
2399
+ (0, node_child_process_1.execFileSync)(scipTypescript, args, { cwd: projectDir, stdio: ["ignore", "pipe", "pipe"] });
2400
+ const generatedScipPath = (0, node_path_1.join)(projectDir, "index.scip");
2401
+ if ((0, node_fs_1.existsSync)(generatedScipPath))
2402
+ (0, node_fs_1.renameSync)(generatedScipPath, scipPath);
2403
+ if (!(0, node_fs_1.existsSync)(scipPath))
2404
+ throw new Error("scip-typescript completed but did not write index.scip");
2405
+ }
2406
+ catch (error) {
2407
+ errors.push(`scip-typescript failed: ${error instanceof Error ? error.message : String(error)}`);
2408
+ return { ok: false, project_dir: projectDir, path: scipPath, parser: "scip", documents: 0, symbols: 0, warnings, errors };
2409
+ }
2410
+ if (!scipCli) {
2411
+ warnings.push("scip-typescript wrote index.scip, but the scip CLI is not installed so Kage could not convert it into graph facts.");
2412
+ return { ok: false, project_dir: projectDir, path: scipPath, parser: "scip", documents: 0, symbols: 0, warnings, errors };
2413
+ }
2414
+ try {
2415
+ const raw = JSON.parse(scipCliJson(scipCli, scipPath, projectDir));
2416
+ const facts = parseScipJsonObject(projectDir, raw);
2417
+ writeJson(outPath, {
2418
+ schema_version: 1,
2419
+ generator: "scip-typescript",
2420
+ generated_at: nowIso(),
2421
+ source_artifact: (0, node_path_1.relative)(projectDir, scipPath).replace(/\\/g, "/"),
2422
+ symbols: facts.symbols,
2423
+ imports: facts.imports,
2424
+ calls: facts.calls,
2425
+ });
2426
+ return {
2427
+ ok: true,
2428
+ project_dir: projectDir,
2429
+ path: outPath,
2430
+ parser: "scip",
2431
+ documents: Array.isArray(raw.documents) ? raw.documents.length : 0,
2432
+ symbols: facts.symbols.length,
2433
+ warnings,
2434
+ errors,
2435
+ };
2436
+ }
2437
+ catch (error) {
2438
+ errors.push(`scip conversion failed: ${error instanceof Error ? error.message : String(error)}`);
2439
+ return { ok: false, project_dir: projectDir, path: scipPath, parser: "scip", documents: 0, symbols: 0, warnings, errors };
2440
+ }
2441
+ }
2442
+ function writeCodeIndex(projectDir) {
2443
+ const scip = writeScipTypescriptIndex(projectDir);
2444
+ if (scip?.ok)
2445
+ return scip;
2446
+ const lsp = writeLspSymbolIndex(projectDir);
2447
+ return {
2448
+ ...lsp,
2449
+ warnings: [
2450
+ ...(scip ? [...scip.warnings, ...scip.errors] : ["scip-typescript not found; used built-in LSP-compatible fallback."]),
2451
+ ...lsp.warnings,
2452
+ ],
2453
+ };
2454
+ }
2286
2455
  function parseLsif(projectDir, path) {
2287
2456
  const docs = new Map();
2288
2457
  const ranges = new Map();
@@ -2380,7 +2549,7 @@ function extractPackages(projectDir) {
2380
2549
  }
2381
2550
  return packages.sort((a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name));
2382
2551
  }
2383
- function buildCodeGraph(projectDir) {
2552
+ function buildCodeGraph(projectDir, options = {}) {
2384
2553
  ensureMemoryDirs(projectDir);
2385
2554
  const branch = gitBranch(projectDir);
2386
2555
  const head = gitHead(projectDir);
@@ -2389,7 +2558,7 @@ function buildCodeGraph(projectDir) {
2389
2558
  const selection = codeIndexSelection(projectDir);
2390
2559
  const absoluteFiles = selection.files;
2391
2560
  const fingerprint = codeGraphStatFingerprint(projectDir, absoluteFiles);
2392
- const cachedGraph = readCachedCodeGraph(projectDir, fingerprint);
2561
+ const cachedGraph = options.force ? null : readCachedCodeGraph(projectDir, fingerprint);
2393
2562
  if (cachedGraph) {
2394
2563
  selection.manifest.cache = { hits: absoluteFiles.length, misses: 0 };
2395
2564
  selection.manifest.fingerprint = fingerprint;
@@ -2983,9 +3152,9 @@ function currentOrBuildGraphs(projectDir) {
2983
3152
  }
2984
3153
  return buildGraphIndexes(projectDir);
2985
3154
  }
2986
- function buildGraphIndexes(projectDir) {
3155
+ function buildGraphIndexes(projectDir, options = {}) {
2987
3156
  const written = buildPacketIndexes(projectDir);
2988
- const codeGraph = buildCodeGraph(projectDir);
3157
+ const codeGraph = buildCodeGraph(projectDir, { force: options.forceCodeGraph });
2989
3158
  const knowledgeGraph = buildKnowledgeGraph(projectDir, codeGraph);
2990
3159
  const graphIndexPath = (0, node_path_1.join)(indexesDir(projectDir), "graph.json");
2991
3160
  const codeGraphIndexPath = (0, node_path_1.join)(indexesDir(projectDir), "code-graph.json");
@@ -3040,7 +3209,7 @@ function indexProjectDetailed(projectDir, options = {}) {
3040
3209
  const structure = createRepoStructurePacket(projectDir);
3041
3210
  if (structure)
3042
3211
  upsertGeneratedPacket(projectDir, structure);
3043
- const built = options.graphs === false ? null : buildGraphIndexes(projectDir);
3212
+ const built = options.graphs === false ? null : buildGraphIndexes(projectDir, { forceCodeGraph: options.full });
3044
3213
  const indexes = built?.indexes ?? buildPacketIndexes(projectDir);
3045
3214
  return {
3046
3215
  result: {
@@ -3114,15 +3283,15 @@ function refreshPacketStaleness(projectDir) {
3114
3283
  }
3115
3284
  return { findings, updated };
3116
3285
  }
3117
- function refreshProject(projectDir) {
3118
- const detailedIndex = indexProjectDetailed(projectDir);
3286
+ function refreshProject(projectDir, options = {}) {
3287
+ const detailedIndex = indexProjectDetailed(projectDir, { full: options.full });
3119
3288
  const index = detailedIndex.result;
3120
3289
  let codeGraph = detailedIndex.codeGraph;
3121
3290
  let knowledgeGraph = detailedIndex.knowledgeGraph;
3122
3291
  const stale = refreshPacketStaleness(projectDir);
3123
3292
  let indexes = index.indexes;
3124
3293
  if (stale.updated > 0) {
3125
- const rebuilt = buildGraphIndexes(projectDir);
3294
+ const rebuilt = buildGraphIndexes(projectDir, { forceCodeGraph: options.full });
3126
3295
  codeGraph = rebuilt.codeGraph;
3127
3296
  knowledgeGraph = rebuilt.knowledgeGraph;
3128
3297
  indexes = rebuilt.indexes.map((path) => (0, node_path_1.relative)(projectDir, path));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "1.1.19",
3
+ "version": "1.1.20",
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": [