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

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
@@ -27,6 +27,7 @@ kage init --project /path/to/repo
27
27
  kage policy --project /path/to/repo
28
28
  kage doctor --project /path/to/repo
29
29
  kage index --project /path/to/repo
30
+ kage refresh --project /path/to/repo
30
31
  kage branch --project /path/to/repo
31
32
  kage code-graph --project /path/to/repo
32
33
  kage code-graph "createApp routes tests" --project /path/to/repo
@@ -45,6 +46,8 @@ kage learn --project /path/to/repo --learning "Decision: use kage_learn for actu
45
46
  kage feedback --project /path/to/repo --packet <approved-packet-id> --kind stale
46
47
  kage capture --project /path/to/repo --type runbook --title "Webhook tests" --body "Run pnpm test:api -- webhooks."
47
48
  kage propose --project /path/to/repo --from-diff
49
+ kage pr summarize --project /path/to/repo
50
+ kage pr check --project /path/to/repo
48
51
  kage review-artifact --project /path/to/repo
49
52
  kage registry --project /path/to/repo
50
53
  kage marketplace --project /path/to/repo
@@ -57,6 +60,7 @@ kage layered-recall "how do I run tests" --project /path/to/repo --org acme --gl
57
60
  kage global build --project /path/to/repo --org acme
58
61
  kage review --project /path/to/repo
59
62
  kage validate --project /path/to/repo
63
+ kage upgrade --dry-run
60
64
  ```
61
65
 
62
66
  `kage init` is the first-run command. It creates `.agent_memory/`, migrates
@@ -121,6 +125,16 @@ and parser coverage, code graph counts, evidence coverage, approved vs pending
121
125
  memory, validation status, estimated tokens saved per recall, duplicate
122
126
  candidates, average memory quality, and a readiness score.
123
127
 
128
+ Use `kage refresh --project <repo>` or the `kage_refresh` MCP tool after
129
+ meaningful file changes. Refresh rebuilds indexes, code graph, memory graph,
130
+ metrics, and stale-memory metadata. Memory is marked stale when status or
131
+ feedback says it is stale, its TTL expires, or grounded paths disappear.
132
+
133
+ Use `kage pr summarize --project <repo>` / `kage_pr_summarize` before handoff to
134
+ write branch review metadata and repo-local change memory from the git diff.
135
+ Use `kage pr check --project <repo>` / `kage_pr_check` before merge to verify
136
+ validation, graph freshness, stale packets, and memory packet changes.
137
+
124
138
  Review artifacts include memory quality reasons, risks, duplicate candidates,
125
139
  and estimated token savings for legacy pending/quarantine packets and promotion
126
140
  review.
@@ -185,6 +199,9 @@ Local repo tools:
185
199
  - `kage_recall`
186
200
  - `kage_code_graph`
187
201
  - `kage_metrics`
202
+ - `kage_refresh`
203
+ - `kage_pr_summarize`
204
+ - `kage_pr_check`
188
205
  - `kage_quality`
189
206
  - `kage_benchmark`
190
207
  - `kage_setup_agent`
@@ -267,8 +284,10 @@ Before code changes or repo-specific answers:
267
284
  2. Call `kage_recall` with the user task as the query.
268
285
  3. Call `kage_graph` with the user task as the query.
269
286
  4. Capture reusable learnings with `kage_learn` or `kage_capture`.
270
- 5. Before finishing changed-file tasks, call `kage_propose_from_diff`.
271
- 6. Never publish or promote org/global memory automatically.
287
+ 5. After meaningful file changes, call `kage_refresh`.
288
+ 6. Before finishing changed-file tasks, call `kage_propose_from_diff` or `kage_pr_summarize`.
289
+ 7. Before merge, call `kage_pr_check`.
290
+ 8. Never publish or promote org/global memory automatically.
272
291
  ```
273
292
 
274
293
  Run `kage setup verify-agent --agent codex --project <repo>` after setup. The
package/dist/cli.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
+ const node_child_process_1 = require("node:child_process");
4
5
  const promises_1 = require("node:readline/promises");
5
6
  const node_process_1 = require("node:process");
6
7
  const daemon_js_1 = require("./daemon.js");
@@ -22,6 +23,10 @@ Usage:
22
23
  kage daemon status --project <dir> [--json]
23
24
  kage daemon doctor --project <dir> [--json]
24
25
  kage viewer --project <dir> [--port 3113]
26
+ kage refresh --project <dir> [--json]
27
+ kage pr summarize --project <dir> [--json]
28
+ kage pr check --project <dir> [--json]
29
+ kage upgrade [--dry-run]
25
30
  kage branch --project <dir> [--json]
26
31
  kage metrics --project <dir> [--json]
27
32
  kage quality --project <dir> [--json]
@@ -296,6 +301,97 @@ async function main() {
296
301
  await (0, daemon_js_1.startViewer)(projectArg(args), { port: numberArg(args, "--port", 3113) });
297
302
  return;
298
303
  }
304
+ if (command === "refresh") {
305
+ const result = (0, kernel_js_1.refreshProject)(projectArg(args));
306
+ if (args.includes("--json")) {
307
+ console.log(JSON.stringify(result, null, 2));
308
+ if (!result.ok)
309
+ process.exit(2);
310
+ return;
311
+ }
312
+ console.log(`Refreshed ${result.project_dir}`);
313
+ console.log(`Packets indexed: ${result.index.packets}`);
314
+ console.log(`Packet metadata updated: ${result.updated_packets}`);
315
+ console.log(`Code graph: ${result.code_graph.files} files, ${result.code_graph.symbols} symbols, ${result.code_graph.imports} imports, ${result.code_graph.calls} calls`);
316
+ console.log(`Memory graph: ${result.memory_graph.entities} entities, ${result.memory_graph.edges} edges, ${result.memory_graph.episodes} episodes`);
317
+ console.log(`Stale packets: ${result.stale_packets.length}`);
318
+ for (const packet of result.stale_packets.slice(0, 8)) {
319
+ console.log(` - ${packet.title} (${packet.id}): ${packet.reasons.join("; ")}`);
320
+ }
321
+ console.log(result.validation.ok ? "Validation: passed" : "Validation: failed");
322
+ if (result.validation.errors.length)
323
+ console.log(`Errors:\n${result.validation.errors.map((error) => ` - ${error}`).join("\n")}`);
324
+ if (result.validation.warnings.length)
325
+ console.log(`Warnings:\n${result.validation.warnings.map((warning) => ` - ${warning}`).join("\n")}`);
326
+ console.log(`Next actions:\n${result.next_actions.map((action) => ` - ${action}`).join("\n")}`);
327
+ if (!result.ok)
328
+ process.exit(2);
329
+ return;
330
+ }
331
+ if (command === "pr") {
332
+ const action = args[1];
333
+ if (action === "summarize") {
334
+ const result = (0, kernel_js_1.prSummarize)(projectArg(args));
335
+ if (args.includes("--json")) {
336
+ console.log(JSON.stringify(result, null, 2));
337
+ if (!result.ok)
338
+ process.exit(2);
339
+ return;
340
+ }
341
+ console.log(`PR summary for ${result.project_dir}`);
342
+ console.log(`Branch: ${result.branch ?? "(detached)"}`);
343
+ console.log(`Changed files: ${result.changed_files.join(", ") || "(none)"}`);
344
+ if (result.diff_memory_packet_id)
345
+ console.log(`Repo memory: ${result.diff_memory_packet_id}`);
346
+ if (result.branch_summary_path)
347
+ console.log(`Branch summary: ${result.branch_summary_path}`);
348
+ if (result.review_artifact_path)
349
+ console.log(`Review artifact: ${result.review_artifact_path}`);
350
+ if (result.warnings.length)
351
+ console.log(`Warnings:\n${result.warnings.map((warning) => ` - ${warning}`).join("\n")}`);
352
+ if (result.errors.length)
353
+ console.log(`Errors:\n${result.errors.map((error) => ` - ${error}`).join("\n")}`);
354
+ if (!result.ok)
355
+ process.exit(2);
356
+ return;
357
+ }
358
+ if (action === "check") {
359
+ const result = (0, kernel_js_1.prCheck)(projectArg(args));
360
+ if (args.includes("--json")) {
361
+ console.log(JSON.stringify(result, null, 2));
362
+ if (!result.ok)
363
+ process.exit(2);
364
+ return;
365
+ }
366
+ console.log(`PR memory check for ${result.project_dir}`);
367
+ console.log(`Branch: ${result.branch ?? "(detached)"}`);
368
+ console.log(`Changed files: ${result.changed_files.length}`);
369
+ console.log(`Memory packet changes: ${result.memory_packet_changes.length}`);
370
+ console.log(`Code graph current: ${result.code_graph_current ? "yes" : "no"}`);
371
+ console.log(`Memory graph current: ${result.memory_graph_current ? "yes" : "no"}`);
372
+ console.log(`Stale packets: ${result.stale_packets.length}`);
373
+ if (result.warnings.length)
374
+ console.log(`Warnings:\n${result.warnings.map((warning) => ` - ${warning}`).join("\n")}`);
375
+ if (result.errors.length)
376
+ console.log(`Errors:\n${result.errors.map((error) => ` - ${error}`).join("\n")}`);
377
+ console.log(`Required actions:\n${result.required_actions.map((action) => ` - ${action}`).join("\n")}`);
378
+ if (!result.ok)
379
+ process.exit(2);
380
+ return;
381
+ }
382
+ usage();
383
+ }
384
+ if (command === "upgrade") {
385
+ const commandLine = "npm install -g @kage-core/kage-graph-mcp@latest";
386
+ if (args.includes("--dry-run")) {
387
+ console.log(commandLine);
388
+ return;
389
+ }
390
+ console.log(`Running: ${commandLine}`);
391
+ (0, node_child_process_1.execFileSync)("npm", ["install", "-g", "@kage-core/kage-graph-mcp@latest"], { stdio: "inherit" });
392
+ console.log("Kage upgraded. Restart Codex or Claude Code so the MCP process reloads the new package.");
393
+ return;
394
+ }
299
395
  if (command === "graph") {
300
396
  const query = firstPositional(args);
301
397
  if (query) {
package/dist/index.js CHANGED
@@ -154,6 +154,39 @@ function listTools() {
154
154
  required: ["project_dir"],
155
155
  },
156
156
  },
157
+ {
158
+ name: "kage_refresh",
159
+ description: "Rebuild repo indexes, code graph, memory graph, metrics, and stale-memory metadata. Agents should run this after meaningful file changes and before PR checks.",
160
+ inputSchema: {
161
+ type: "object",
162
+ properties: {
163
+ project_dir: { type: "string" },
164
+ },
165
+ required: ["project_dir"],
166
+ },
167
+ },
168
+ {
169
+ name: "kage_pr_summarize",
170
+ description: "Create a PR/branch memory summary from local git diff metadata and write repo-local change memory. Use when a branch is ready to hand off.",
171
+ inputSchema: {
172
+ type: "object",
173
+ properties: {
174
+ project_dir: { type: "string" },
175
+ },
176
+ required: ["project_dir"],
177
+ },
178
+ },
179
+ {
180
+ name: "kage_pr_check",
181
+ description: "Check whether repo memory, code graph, memory graph, and stale-memory state are ready for merge.",
182
+ inputSchema: {
183
+ type: "object",
184
+ properties: {
185
+ project_dir: { type: "string" },
186
+ },
187
+ required: ["project_dir"],
188
+ },
189
+ },
157
190
  {
158
191
  name: "kage_quality",
159
192
  description: "Return memory quality metrics: useful memory ratio, duplicate burden, stale/wrong feedback, evidence coverage, path grounding, and review queue size.",
@@ -595,6 +628,27 @@ async function callTool(name, args) {
595
628
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
596
629
  };
597
630
  }
631
+ if (name === "kage_refresh") {
632
+ const result = (0, kernel_js_1.refreshProject)(String(args?.project_dir ?? ""));
633
+ return {
634
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
635
+ isError: !result.ok,
636
+ };
637
+ }
638
+ if (name === "kage_pr_summarize") {
639
+ const result = (0, kernel_js_1.prSummarize)(String(args?.project_dir ?? ""));
640
+ return {
641
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
642
+ isError: !result.ok,
643
+ };
644
+ }
645
+ if (name === "kage_pr_check") {
646
+ const result = (0, kernel_js_1.prCheck)(String(args?.project_dir ?? ""));
647
+ return {
648
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
649
+ isError: !result.ok,
650
+ };
651
+ }
598
652
  if (name === "kage_quality") {
599
653
  const result = (0, kernel_js_1.qualityReport)(String(args?.project_dir ?? ""));
600
654
  return {
package/dist/kernel.js CHANGED
@@ -66,6 +66,7 @@ exports.buildCodeGraph = buildCodeGraph;
66
66
  exports.buildKnowledgeGraph = buildKnowledgeGraph;
67
67
  exports.buildIndexes = buildIndexes;
68
68
  exports.indexProject = indexProject;
69
+ exports.refreshProject = refreshProject;
69
70
  exports.installAgentPolicy = installAgentPolicy;
70
71
  exports.recall = recall;
71
72
  exports.queryCodeGraph = queryCodeGraph;
@@ -86,6 +87,8 @@ exports.distillSession = distillSession;
86
87
  exports.proposeFromDiff = proposeFromDiff;
87
88
  exports.buildBranchOverlay = buildBranchOverlay;
88
89
  exports.createReviewArtifact = createReviewArtifact;
90
+ exports.prSummarize = prSummarize;
91
+ exports.prCheck = prCheck;
89
92
  exports.exportPublicBundle = exportPublicBundle;
90
93
  exports.orgStatus = orgStatus;
91
94
  exports.orgUploadPacket = orgUploadPacket;
@@ -447,12 +450,41 @@ function packetFeedbackScore(packet) {
447
450
  const quality = packet.quality;
448
451
  return Number(quality.votes_up ?? 0) * 2 - Number(quality.votes_down ?? 0) * 3 - Number(quality.reports_stale ?? 0) * 4;
449
452
  }
453
+ function meaningfulMemoryPath(path) {
454
+ return path !== "root" && path !== "." && !isNoisePath(path);
455
+ }
456
+ function staleMemoryReasons(projectDir, packet) {
457
+ const reasons = [];
458
+ const quality = packet.quality;
459
+ const freshness = packet.freshness;
460
+ if (packet.status === "deprecated" || packet.status === "superseded") {
461
+ reasons.push(`packet status is ${packet.status}`);
462
+ }
463
+ if (Number(quality.reports_stale ?? 0) > 0) {
464
+ reasons.push("user or agent reported this memory stale");
465
+ }
466
+ const ttlDays = Number(freshness.ttl_days ?? freshness.ttlDays ?? 0);
467
+ const verifiedAt = Date.parse(String(freshness.last_verified_at ?? packet.updated_at ?? packet.created_at));
468
+ if (Number.isFinite(ttlDays) && ttlDays > 0 && Number.isFinite(verifiedAt)) {
469
+ const ageDays = (Date.now() - verifiedAt) / (1000 * 60 * 60 * 24);
470
+ if (ageDays > ttlDays)
471
+ reasons.push(`freshness ttl expired (${Math.floor(ageDays)}d old, ttl ${ttlDays}d)`);
472
+ }
473
+ const paths = packet.paths.filter(meaningfulMemoryPath);
474
+ const missingPaths = paths.filter((path) => !(0, node_fs_1.existsSync)((0, node_path_1.join)(projectDir, path)));
475
+ if (paths.length > 0 && missingPaths.length === paths.length) {
476
+ reasons.push(`all referenced paths are missing: ${missingPaths.slice(0, 4).join(", ")}`);
477
+ }
478
+ else if (missingPaths.length > 0) {
479
+ reasons.push(`some referenced paths are missing: ${missingPaths.slice(0, 4).join(", ")}`);
480
+ }
481
+ return unique(reasons);
482
+ }
450
483
  function classifyPacket(projectDir, packet) {
451
484
  const quality = evaluateMemoryQuality(projectDir, packet);
452
485
  const score = Number(quality.score);
453
486
  const duplicates = quality.duplicate_candidates;
454
- const q = packet.quality;
455
- if (Number(q.reports_stale ?? 0) > 0 || packet.status === "deprecated" || packet.status === "superseded")
487
+ if (staleMemoryReasons(projectDir, packet).length)
456
488
  return "stale";
457
489
  if (duplicates.length)
458
490
  return "duplicate";
@@ -522,11 +554,17 @@ function evaluateMemoryQuality(projectDir, packet) {
522
554
  score -= 18;
523
555
  risks.push("possible duplicate memory");
524
556
  }
557
+ const staleReasons = staleMemoryReasons(projectDir, packet);
558
+ if (staleReasons.length) {
559
+ score -= 22;
560
+ risks.push(...staleReasons);
561
+ }
525
562
  return {
526
563
  score: Math.max(0, Math.min(100, score)),
527
564
  reasons,
528
565
  risks,
529
566
  duplicate_candidates: duplicates,
567
+ stale_reasons: staleReasons,
530
568
  estimated_tokens_saved: Math.max(40, estimateTokens(packet.body) * 2),
531
569
  };
532
570
  }
@@ -769,6 +807,17 @@ function loadPacketsFromDir(dir) {
769
807
  .sort()
770
808
  .map((name) => readJson((0, node_path_1.join)(dir, name)));
771
809
  }
810
+ function loadPacketEntriesFromDir(dir) {
811
+ if (!(0, node_fs_1.existsSync)(dir))
812
+ return [];
813
+ return (0, node_fs_1.readdirSync)(dir)
814
+ .filter((name) => name.endsWith(".json"))
815
+ .sort()
816
+ .map((name) => {
817
+ const path = (0, node_path_1.join)(dir, name);
818
+ return { path, packet: readJson(path) };
819
+ });
820
+ }
772
821
  function loadApprovedPackets(projectDir) {
773
822
  return loadPacketsFromDir(packetsDir(projectDir)).filter((packet) => packet.status === "approved");
774
823
  }
@@ -837,14 +886,15 @@ function isNoisePath(filePath) {
837
886
  function parsePorcelainStatus(status) {
838
887
  return unique(status
839
888
  .split(/\r?\n/)
840
- .map((line) => {
841
- const raw = line.length > 2 && line[2] === " " ? line.slice(3) : line.slice(2);
842
- return raw.trim();
843
- })
889
+ .map(parsePorcelainPath)
844
890
  .map((path) => path.replace(/^.* -> /, ""))
845
891
  .filter(Boolean)
846
892
  .filter((path) => !shouldSkipRepoMemoryPath(path))).sort();
847
893
  }
894
+ function parsePorcelainPath(line) {
895
+ const raw = line.length > 2 && line[2] === " " ? line.slice(3) : line.slice(2);
896
+ return raw.trim();
897
+ }
848
898
  function shouldSkipRepoMemoryPath(relativePath) {
849
899
  return isNoisePath(relativePath) || shouldSkipCodePath(relativePath);
850
900
  }
@@ -2342,6 +2392,104 @@ function indexProject(projectDir) {
2342
2392
  policyPath: (0, node_path_1.relative)(projectDir, policy.path),
2343
2393
  };
2344
2394
  }
2395
+ function staleSuggestedAction(reasons) {
2396
+ if (reasons.some((reason) => reason.includes("status is")))
2397
+ return "mark_stale";
2398
+ if (reasons.some((reason) => reason.includes("missing")))
2399
+ return "update";
2400
+ if (reasons.some((reason) => reason.includes("reported")))
2401
+ return "supersede";
2402
+ return "verify";
2403
+ }
2404
+ function staleFinding(packet, reasons) {
2405
+ return {
2406
+ id: packet.id,
2407
+ title: packet.title,
2408
+ type: packet.type,
2409
+ status: packet.status,
2410
+ paths: packet.paths,
2411
+ reasons,
2412
+ suggested_action: staleSuggestedAction(reasons),
2413
+ };
2414
+ }
2415
+ function refreshPacketStaleness(projectDir) {
2416
+ const findings = [];
2417
+ let updated = 0;
2418
+ for (const entry of loadPacketEntriesFromDir(packetsDir(projectDir))) {
2419
+ const reasons = staleMemoryReasons(projectDir, entry.packet);
2420
+ const oldQuality = entry.packet.quality;
2421
+ const oldFreshness = entry.packet.freshness;
2422
+ let nextQuality;
2423
+ if (reasons.length) {
2424
+ const finding = staleFinding(entry.packet, reasons);
2425
+ findings.push(finding);
2426
+ nextQuality = {
2427
+ ...oldQuality,
2428
+ stale: true,
2429
+ stale_reasons: reasons,
2430
+ suggested_action: finding.suggested_action,
2431
+ };
2432
+ }
2433
+ else {
2434
+ const { stale: _stale, stale_reasons: _staleReasons, suggested_action: _suggestedAction, ...rest } = oldQuality;
2435
+ nextQuality = rest;
2436
+ }
2437
+ const nextFreshness = oldFreshness;
2438
+ const changed = JSON.stringify(oldQuality) !== JSON.stringify(nextQuality)
2439
+ || JSON.stringify(oldFreshness) !== JSON.stringify(nextFreshness);
2440
+ if (changed) {
2441
+ writeJson(entry.path, {
2442
+ ...entry.packet,
2443
+ freshness: nextFreshness,
2444
+ quality: nextQuality,
2445
+ updated_at: nowIso(),
2446
+ });
2447
+ updated += 1;
2448
+ }
2449
+ }
2450
+ return { findings, updated };
2451
+ }
2452
+ function refreshProject(projectDir) {
2453
+ const index = indexProject(projectDir);
2454
+ const stale = refreshPacketStaleness(projectDir);
2455
+ const indexes = stale.updated > 0 ? buildIndexes(projectDir).map((path) => (0, node_path_1.relative)(projectDir, path)) : index.indexes;
2456
+ const validation = validateProject(projectDir);
2457
+ const metrics = kageMetrics(projectDir);
2458
+ const nextActions = [];
2459
+ if (stale.findings.length)
2460
+ nextActions.push("Update, verify, or supersede stale repo memories before relying on them.");
2461
+ if (!validation.ok)
2462
+ nextActions.push("Fix validation errors before merging or sharing memory.");
2463
+ if (validation.warnings.length)
2464
+ nextActions.push("Review validation warnings for grounding, indexes, or generated artifacts.");
2465
+ if (!nextActions.length)
2466
+ nextActions.push("Repo memory, code graph, and indexes are current.");
2467
+ return {
2468
+ ok: validation.ok,
2469
+ project_dir: projectDir,
2470
+ generated_at: nowIso(),
2471
+ index,
2472
+ validation,
2473
+ metrics,
2474
+ stale_packets: stale.findings,
2475
+ updated_packets: stale.updated,
2476
+ indexes,
2477
+ code_graph: {
2478
+ files: metrics.code_graph.files,
2479
+ symbols: metrics.code_graph.symbols,
2480
+ imports: metrics.code_graph.imports,
2481
+ calls: metrics.code_graph.calls,
2482
+ routes: metrics.code_graph.routes,
2483
+ tests: metrics.code_graph.tests,
2484
+ },
2485
+ memory_graph: {
2486
+ entities: metrics.memory_graph.entities,
2487
+ edges: metrics.memory_graph.edges,
2488
+ episodes: metrics.memory_graph.episodes,
2489
+ },
2490
+ next_actions: nextActions,
2491
+ };
2492
+ }
2345
2493
  function installAgentPolicy(projectDir) {
2346
2494
  const agentsPath = (0, node_path_1.join)(projectDir, "AGENTS.md");
2347
2495
  const claudePath = (0, node_path_1.join)(projectDir, "CLAUDE.md");
@@ -3847,7 +3995,7 @@ function proposeFromDiff(projectDir) {
3847
3995
  }
3848
3996
  function buildBranchOverlay(projectDir) {
3849
3997
  ensureMemoryDirs(projectDir);
3850
- const status = readGit(projectDir, ["status", "--porcelain"]) ?? "";
3998
+ const status = readGit(projectDir, ["status", "--porcelain", "-uall"]) ?? "";
3851
3999
  const overlay = {
3852
4000
  schema_version: 1,
3853
4001
  project_dir: projectDir,
@@ -3925,6 +4073,91 @@ function createReviewArtifact(projectDir) {
3925
4073
  (0, node_fs_1.writeFileSync)(path, `${lines.join("\n").trim()}\n`, "utf8");
3926
4074
  return { path, pending: pending.length };
3927
4075
  }
4076
+ function graphIsCurrent(projectDir, relativePath, head) {
4077
+ const path = (0, node_path_1.join)(projectDir, relativePath);
4078
+ if (!(0, node_fs_1.existsSync)(path))
4079
+ return false;
4080
+ if (!head)
4081
+ return true;
4082
+ try {
4083
+ const graph = readJson(path);
4084
+ return graph.repo_state?.head === head;
4085
+ }
4086
+ catch {
4087
+ return false;
4088
+ }
4089
+ }
4090
+ function prSummarize(projectDir) {
4091
+ ensureMemoryDirs(projectDir);
4092
+ const proposal = proposeFromDiff(projectDir);
4093
+ const artifact = createReviewArtifact(projectDir);
4094
+ const validation = validateProject(projectDir);
4095
+ const warnings = [...validation.warnings];
4096
+ if (!proposal.ok)
4097
+ warnings.push(...proposal.errors);
4098
+ return {
4099
+ ok: proposal.ok && validation.ok,
4100
+ project_dir: projectDir,
4101
+ branch: gitBranch(projectDir),
4102
+ head: gitHead(projectDir),
4103
+ changed_files: proposal.changedFiles,
4104
+ diff_memory_packet_id: proposal.packet?.id,
4105
+ diff_memory_packet_path: proposal.packetPath,
4106
+ branch_summary_path: proposal.path,
4107
+ review_artifact_path: artifact.path,
4108
+ validation,
4109
+ errors: validation.errors,
4110
+ warnings,
4111
+ };
4112
+ }
4113
+ function prCheck(projectDir) {
4114
+ ensureMemoryDirs(projectDir);
4115
+ const overlay = buildBranchOverlay(projectDir);
4116
+ const rawStatus = readGit(projectDir, ["status", "--porcelain", "-uall"]) ?? "";
4117
+ const validation = validateProject(projectDir);
4118
+ const stalePackets = loadPacketsFromDir(packetsDir(projectDir))
4119
+ .map((packet) => ({ packet, reasons: staleMemoryReasons(projectDir, packet) }))
4120
+ .filter((entry) => entry.reasons.length)
4121
+ .map((entry) => staleFinding(entry.packet, entry.reasons));
4122
+ const memoryPacketChanges = unique(rawStatus
4123
+ .split(/\r?\n/)
4124
+ .map(parsePorcelainPath)
4125
+ .map((path) => path.replace(/^.* -> /, ""))
4126
+ .filter((path) => path.startsWith(".agent_memory/packets/") && path.endsWith(".json"))).sort();
4127
+ const codeGraphCurrent = graphIsCurrent(projectDir, ".agent_memory/code_graph/graph.json", overlay.head);
4128
+ const memoryGraphCurrent = graphIsCurrent(projectDir, ".agent_memory/graph/graph.json", overlay.head);
4129
+ const errors = [...validation.errors];
4130
+ const warnings = [...validation.warnings];
4131
+ const requiredActions = [];
4132
+ if (stalePackets.length) {
4133
+ errors.push(`${stalePackets.length} stale memory packet(s) require update, verification, or supersession.`);
4134
+ requiredActions.push("Run kage refresh, then update or supersede stale packets.");
4135
+ }
4136
+ if (!codeGraphCurrent || !memoryGraphCurrent) {
4137
+ errors.push("Generated graph artifacts are missing or not current for this branch head.");
4138
+ requiredActions.push("Run kage refresh --project <dir> before merge.");
4139
+ }
4140
+ if (!memoryPacketChanges.length && overlay.changed_files.some((path) => !path.startsWith(".agent_memory/"))) {
4141
+ warnings.push("No repo memory packet changed for this branch. If durable knowledge was learned, run kage propose --from-diff or kage learn.");
4142
+ }
4143
+ if (!requiredActions.length)
4144
+ requiredActions.push("PR memory and graph checks passed.");
4145
+ return {
4146
+ ok: errors.length === 0,
4147
+ project_dir: projectDir,
4148
+ branch: overlay.branch,
4149
+ head: overlay.head,
4150
+ changed_files: overlay.changed_files,
4151
+ validation,
4152
+ stale_packets: stalePackets,
4153
+ memory_packet_changes: memoryPacketChanges,
4154
+ code_graph_current: codeGraphCurrent,
4155
+ memory_graph_current: memoryGraphCurrent,
4156
+ errors,
4157
+ warnings,
4158
+ required_actions: requiredActions,
4159
+ };
4160
+ }
3928
4161
  function exportPublicBundle(projectDir) {
3929
4162
  ensureMemoryDirs(projectDir);
3930
4163
  const candidates = loadPacketsFromDir(publicCandidatesDir(projectDir));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
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": [
package/viewer/app.js CHANGED
@@ -35,21 +35,37 @@
35
35
 
36
36
  var palette = {
37
37
  repo: "#41ff8f",
38
- memory: "#b88cff",
38
+ memory: "#41ff8f",
39
39
  path: "#ff6b6b",
40
40
  tag: "#ffd166",
41
41
  package: "#6ad7ff",
42
42
  command: "#9be7c0",
43
43
  memory_type: "#41ff8f",
44
44
  file: "#6ad7ff",
45
- symbol: "#b88cff",
46
- route: "#ff8fab",
45
+ symbol: "#9be7c0",
46
+ route: "#6ad7ff",
47
47
  test: "#ffd166",
48
- external: "#93a4a0",
48
+ external: "#62776b",
49
49
  script: "#6ad7ff",
50
50
  default: "#9be7c0"
51
51
  };
52
52
 
53
+ var graphPalette = {
54
+ background: "#020503",
55
+ grid: "rgba(65,255,143,0.040)",
56
+ gridStrong: "rgba(65,255,143,0.070)",
57
+ text: "#d7f9df",
58
+ muted: "#6ea77d",
59
+ memory: "#41ff8f",
60
+ code: "#6ad7ff",
61
+ amber: "#ffd166",
62
+ danger: "#ff6b6b",
63
+ dependency: "#62776b",
64
+ body: "rgba(4,12,8,0.88)",
65
+ bodyCode: "rgba(5,16,18,0.88)",
66
+ bodyMemory: "rgba(5,18,10,0.90)"
67
+ };
68
+
53
69
  var els = {
54
70
  graphFile: document.getElementById("graphFile"),
55
71
  graphSummary: document.getElementById("graphSummary"),
@@ -454,7 +470,7 @@
454
470
  function render() {
455
471
  if (!state.graph) return;
456
472
 
457
- var query = normalize(els.searchInput.value);
473
+ var query = parseSearchQuery(els.searchInput.value);
458
474
  var mode = els.viewMode.value;
459
475
  var type = els.typeFilter.value;
460
476
  var relation = els.relationFilter.value;
@@ -464,7 +480,7 @@
464
480
  state.entities.forEach(function (entity) {
465
481
  if (mode !== "combined" && entity.graph_kind !== mode) return;
466
482
  var passesType = !type || entity.type === type;
467
- var passesSearch = !query || searchableText(entity).indexOf(query) !== -1;
483
+ var passesSearch = matchesSearchQuery(entity, query);
468
484
  if (passesType && passesSearch) matchedEntityIds.add(entity.id);
469
485
  });
470
486
 
@@ -472,7 +488,7 @@
472
488
  if (mode !== "combined" && edge.graph_kind !== mode) return;
473
489
  var fromMatched = matchedEntityIds.has(edge.from);
474
490
  var toMatched = matchedEntityIds.has(edge.to);
475
- var edgeMatchesSearch = !query || searchableText(edge).indexOf(query) !== -1;
491
+ var edgeMatchesSearch = matchesSearchQuery(edge, query);
476
492
  var passesRelation = !relation || edge.relation === relation;
477
493
  if (passesRelation && (edgeMatchesSearch || fromMatched || toMatched)) {
478
494
  matchedEdgeIds.add(edge.id);
@@ -483,7 +499,7 @@
483
499
  }
484
500
  });
485
501
 
486
- if (!query && !type && !relation) {
502
+ if (!query.active && !type && !relation) {
487
503
  matchedEntityIds = new Set(state.entities.filter(function (entity) { return mode === "combined" || entity.graph_kind === mode; }).map(function (entity) { return entity.id; }));
488
504
  matchedEdgeIds = new Set(state.edges.filter(function (edge) { return mode === "combined" || edge.graph_kind === mode; }).map(function (edge) { return edge.id; }));
489
505
  }
@@ -520,7 +536,7 @@
520
536
  Array.from(entities).forEach(function (id) {
521
537
  var entity = state.entityById.get(id);
522
538
  if (!entity) return;
523
- if (isDependencyEntity(entity) && !(options.query && searchableText(entity).indexOf(options.query) !== -1)) {
539
+ if (isDependencyEntity(entity) && !matchesSearchQuery(entity, options.query)) {
524
540
  entities.delete(id);
525
541
  }
526
542
  });
@@ -751,9 +767,15 @@
751
767
 
752
768
  function drawCanvasGrid(ctx, width, height) {
753
769
  ctx.save();
754
- ctx.fillStyle = "#020503";
770
+ ctx.fillStyle = graphPalette.background;
755
771
  ctx.fillRect(0, 0, width, height);
756
- ctx.strokeStyle = "rgba(65,255,143,0.045)";
772
+ var gradient = ctx.createRadialGradient(width * 0.52, height * 0.44, 40, width * 0.52, height * 0.44, Math.max(width, height) * 0.72);
773
+ gradient.addColorStop(0, "rgba(65,255,143,0.080)");
774
+ gradient.addColorStop(0.48, "rgba(65,255,143,0.018)");
775
+ gradient.addColorStop(1, "rgba(2,5,3,0)");
776
+ ctx.fillStyle = gradient;
777
+ ctx.fillRect(0, 0, width, height);
778
+ ctx.strokeStyle = graphPalette.grid;
757
779
  ctx.lineWidth = 1;
758
780
  var grid = 28;
759
781
  for (var x = 0; x < width; x += grid) {
@@ -768,22 +790,29 @@
768
790
  ctx.lineTo(width, y);
769
791
  ctx.stroke();
770
792
  }
793
+ ctx.strokeStyle = graphPalette.gridStrong;
794
+ ctx.beginPath();
795
+ ctx.moveTo(0, height / 2);
796
+ ctx.lineTo(width, height / 2);
797
+ ctx.moveTo(width / 2, 0);
798
+ ctx.lineTo(width / 2, height);
799
+ ctx.stroke();
771
800
  ctx.restore();
772
801
  }
773
802
 
774
803
  function drawCanvasEdges(ctx) {
775
804
  var nodeMap = state.sim.nodeById;
776
805
  var focusId = focusedCanvasNodeId();
777
- var query = normalize(els.searchInput.value);
806
+ var query = parseSearchQuery(els.searchInput.value);
778
807
  var dense = state.sim.nodes.length > 55;
779
808
  state.sim.edges.forEach(function (edge) {
780
809
  var from = nodeMap.get(edge.from);
781
810
  var to = nodeMap.get(edge.to);
782
811
  if (!from || !to) return;
783
812
  var connected = focusId && (edge.from === focusId || edge.to === focusId);
784
- var matches = !query || searchableText(edge).indexOf(query) !== -1 || searchableText(from.entity).indexOf(query) !== -1 || searchableText(to.entity).indexOf(query) !== -1;
785
- var alpha = !matches ? 0.04 : focusId ? (connected ? 0.78 : 0.07) : (dense ? 0.18 : 0.34);
786
- var color = hexToRgb(palette[from.entity.type] || palette[from.entity.graph_kind] || palette.default);
813
+ var matches = matchesSearchQuery(edge, query) || matchesSearchQuery(from.entity, query) || matchesSearchQuery(to.entity, query);
814
+ var alpha = !matches ? 0.035 : focusId ? (connected ? 0.62 : 0.055) : (dense ? 0.13 : 0.22);
815
+ var color = hexToRgb(edgeThemeColor(edge, from.entity, to.entity));
787
816
  var dx = to.x - from.x;
788
817
  var dy = to.y - from.y;
789
818
  var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
@@ -794,7 +823,7 @@
794
823
  ctx.moveTo(from.x, from.y);
795
824
  ctx.quadraticCurveTo(cx, cy, to.x, to.y);
796
825
  ctx.strokeStyle = "rgba(" + color.r + "," + color.g + "," + color.b + "," + alpha + ")";
797
- ctx.lineWidth = connected ? 2.8 : 1.2;
826
+ ctx.lineWidth = connected ? 2.2 : 1;
798
827
  ctx.stroke();
799
828
  if (connected || (!dense && state.sim.zoom > 1.25)) drawArrow(ctx, from, to, cx, cy, color, alpha);
800
829
  if (connected && state.sim.zoom > 0.62) drawEdgeLabel(ctx, edge, cx, cy);
@@ -803,7 +832,7 @@
803
832
 
804
833
  function drawCanvasNodes(ctx) {
805
834
  var focusId = focusedCanvasNodeId();
806
- var query = normalize(els.searchInput.value);
835
+ var query = parseSearchQuery(els.searchInput.value);
807
836
  var dense = state.sim.nodes.length > 55;
808
837
  state.sim.nodes.forEach(function (node) {
809
838
  var entity = node.entity;
@@ -812,35 +841,42 @@
812
841
  var connected = focusId && (node.id === focusId || state.sim.edges.some(function (edge) {
813
842
  return (edge.from === focusId && edge.to === node.id) || (edge.to === focusId && edge.from === node.id);
814
843
  }));
815
- var matches = !query || searchableText(entity).indexOf(query) !== -1;
844
+ var matches = matchesSearchQuery(entity, query);
816
845
  var alpha = !matches ? 0.12 : focusId && !connected ? 0.20 : 1;
817
- var color = palette[entity.type] || palette[entity.graph_kind] || palette.default;
846
+ var color = nodeThemeColor(entity);
818
847
  ctx.save();
819
848
  ctx.globalAlpha = alpha;
820
- if (selected || hovered || (!focusId && !dense)) {
849
+ if (selected || hovered) {
821
850
  ctx.shadowColor = color;
822
- ctx.shadowBlur = selected ? 20 : hovered ? 16 : 5;
851
+ ctx.shadowBlur = selected ? 14 : 10;
823
852
  }
824
853
  drawNodeShape(ctx, node.x, node.y, node.r, entity);
825
- var grad = ctx.createRadialGradient(node.x - node.r * 0.3, node.y - node.r * 0.3, 1, node.x, node.y, node.r * 1.35);
826
- grad.addColorStop(0, brighten(color, 54));
827
- grad.addColorStop(1, color);
828
- ctx.fillStyle = grad;
854
+ ctx.fillStyle = nodeFillColor(entity);
829
855
  ctx.fill();
856
+ ctx.strokeStyle = color;
857
+ ctx.lineWidth = selected || hovered ? 2.2 : 1.2;
858
+ ctx.stroke();
859
+ if (entity.graph_kind === "memory") {
860
+ ctx.fillStyle = color;
861
+ ctx.globalAlpha = alpha * 0.85;
862
+ ctx.beginPath();
863
+ ctx.arc(node.x, node.y, Math.max(2.4, node.r * 0.18), 0, Math.PI * 2);
864
+ ctx.fill();
865
+ }
830
866
  ctx.restore();
831
867
 
832
868
  if (selected || hovered) {
833
869
  ctx.save();
834
870
  drawNodeShape(ctx, node.x, node.y, node.r + 4, entity);
835
871
  ctx.strokeStyle = color;
836
- ctx.lineWidth = selected ? 3 : 2;
872
+ ctx.lineWidth = selected ? 2.6 : 1.8;
837
873
  ctx.shadowColor = color;
838
- ctx.shadowBlur = 12;
874
+ ctx.shadowBlur = 8;
839
875
  ctx.stroke();
840
876
  ctx.restore();
841
877
  }
842
878
 
843
- var shouldLabel = matches && (selected || hovered || (query && matches) || (!dense && state.sim.zoom > 0.75) || (dense && state.sim.zoom > 1.55 && node.r > 13));
879
+ var shouldLabel = matches && (selected || hovered || (query.active && matches) || (!dense && state.sim.zoom > 0.75) || (dense && state.sim.zoom > 1.55 && node.r > 13));
844
880
  if (shouldLabel) drawNodeLabel(ctx, node, selected || hovered);
845
881
  });
846
882
  }
@@ -851,8 +887,8 @@
851
887
  var tipY = to.y - to.r * Math.sin(angle);
852
888
  ctx.beginPath();
853
889
  ctx.moveTo(tipX, tipY);
854
- ctx.lineTo(tipX - 8 * Math.cos(angle - 0.35), tipY - 8 * Math.sin(angle - 0.35));
855
- ctx.lineTo(tipX - 8 * Math.cos(angle + 0.35), tipY - 8 * Math.sin(angle + 0.35));
890
+ ctx.lineTo(tipX - 6 * Math.cos(angle - 0.32), tipY - 6 * Math.sin(angle - 0.32));
891
+ ctx.lineTo(tipX - 6 * Math.cos(angle + 0.32), tipY - 6 * Math.sin(angle + 0.32));
856
892
  ctx.closePath();
857
893
  ctx.fillStyle = "rgba(" + color.r + "," + color.g + "," + color.b + "," + Math.min(0.85, alpha + 0.10) + ")";
858
894
  ctx.fill();
@@ -861,9 +897,9 @@
861
897
  function drawEdgeLabel(ctx, edge, x, y) {
862
898
  var inv = 1 / state.sim.zoom;
863
899
  ctx.save();
864
- ctx.font = "700 " + (10 * inv).toFixed(1) + "px ui-monospace, Menlo, monospace";
900
+ ctx.font = "700 " + (9 * inv).toFixed(1) + "px ui-monospace, Menlo, monospace";
865
901
  ctx.textAlign = "center";
866
- ctx.fillStyle = "rgba(215,249,223,0.82)";
902
+ ctx.fillStyle = "rgba(155,231,192,0.72)";
867
903
  ctx.fillText(shortName(edge.relation || "related", 22), x, y - 5 * inv);
868
904
  ctx.restore();
869
905
  }
@@ -872,17 +908,17 @@
872
908
  var inv = 1 / state.sim.zoom;
873
909
  var label = shortName(displayName(node.entity), strong ? 30 : 20);
874
910
  ctx.save();
875
- ctx.font = (strong ? "800 " : "700 ") + (12 * inv).toFixed(1) + "px ui-monospace, Menlo, monospace";
911
+ ctx.font = (strong ? "800 " : "700 ") + (11 * inv).toFixed(1) + "px ui-monospace, Menlo, monospace";
876
912
  var width = ctx.measureText(label).width + 16 * inv;
877
913
  var height = 20 * inv;
878
914
  var x = node.x - width / 2;
879
915
  var y = node.y + node.r + 8 * inv;
880
- ctx.fillStyle = "rgba(3,6,4,0.88)";
916
+ ctx.fillStyle = "rgba(2,5,3,0.92)";
881
917
  roundedRect(ctx, x, y, width, height, 4 * inv);
882
918
  ctx.fill();
883
- ctx.strokeStyle = strong ? "rgba(65,255,143,0.55)" : "rgba(65,255,143,0.18)";
919
+ ctx.strokeStyle = strong ? "rgba(65,255,143,0.45)" : "rgba(65,255,143,0.14)";
884
920
  ctx.stroke();
885
- ctx.fillStyle = strong ? "#d7f9df" : "#9be7c0";
921
+ ctx.fillStyle = strong ? graphPalette.text : graphPalette.muted;
886
922
  ctx.textAlign = "center";
887
923
  ctx.fillText(label, node.x, y + 14 * inv);
888
924
  ctx.restore();
@@ -919,6 +955,30 @@
919
955
  ctx.arc(x, y, r, 0, Math.PI * 2);
920
956
  }
921
957
 
958
+ function nodeThemeColor(entity) {
959
+ if (isDependencyEntity(entity) || entity.type === "external") return graphPalette.dependency;
960
+ if (entity.type === "test" || entity.type === "tag") return graphPalette.amber;
961
+ if (entity.type === "bug_fix" || entity.type === "path") return graphPalette.danger;
962
+ if (entity.graph_kind === "memory" || ["memory", "repo", "memory_type", "decision", "runbook", "workflow", "convention", "gotcha", "reference", "policy"].indexOf(entity.type) !== -1) return graphPalette.memory;
963
+ if (entity.graph_kind === "code" || ["file", "symbol", "route", "script", "command", "package"].indexOf(entity.type) !== -1) return graphPalette.code;
964
+ return graphPalette.muted;
965
+ }
966
+
967
+ function nodeFillColor(entity) {
968
+ if (isDependencyEntity(entity) || entity.type === "external") return "rgba(7,13,10,0.88)";
969
+ if (entity.graph_kind === "memory" || entity.type === "memory") return graphPalette.bodyMemory;
970
+ if (entity.graph_kind === "code") return graphPalette.bodyCode;
971
+ return graphPalette.body;
972
+ }
973
+
974
+ function edgeThemeColor(edge, fromEntity, toEntity) {
975
+ if (edge.relation && /test|covers/i.test(edge.relation)) return graphPalette.amber;
976
+ if (edge.relation && /invalid|risk|missing|bug/i.test(edge.relation)) return graphPalette.danger;
977
+ if (isDependencyEntity(fromEntity) || isDependencyEntity(toEntity)) return graphPalette.dependency;
978
+ if (fromEntity.graph_kind === "memory" || toEntity.graph_kind === "memory") return graphPalette.memory;
979
+ return graphPalette.code;
980
+ }
981
+
922
982
  function renderSvg() {
923
983
  els.edgeLayer.textContent = "";
924
984
  els.nodeLayer.textContent = "";
@@ -1461,7 +1521,7 @@
1461
1521
  }
1462
1522
  var entity = node.entity;
1463
1523
  var relationCount = state.sim.edges.filter(function (edge) { return edge.from === node.id || edge.to === node.id; }).length;
1464
- var color = palette[entity.type] || palette[entity.graph_kind] || palette.default;
1524
+ var color = nodeThemeColor(entity);
1465
1525
  els.tooltip.innerHTML = [
1466
1526
  "<div class=\"tt-name\"></div>",
1467
1527
  "<div class=\"tt-type\"></div>",
@@ -1653,6 +1713,77 @@
1653
1713
  return normalize(JSON.stringify(value || {}));
1654
1714
  }
1655
1715
 
1716
+ function parseSearchQuery(value) {
1717
+ var raw = normalize(value);
1718
+ var tokens = raw
1719
+ .replace(/[^a-z0-9_./:-]+/g, " ")
1720
+ .split(/\s+/)
1721
+ .map(searchStem)
1722
+ .filter(Boolean)
1723
+ .filter(function (token) { return !SEARCH_STOP_WORDS.has(token); });
1724
+ if ((tokens.indexOf("run") !== -1 || tokens.indexOf("runn") !== -1) && tokens.indexOf("test") !== -1) tokens.push("runtest");
1725
+ var groups = tokens.map(function (token) {
1726
+ return unique([token].concat(SEARCH_SYNONYMS[token] || []).map(searchStem).filter(Boolean));
1727
+ });
1728
+ return {
1729
+ active: raw.trim().length > 0,
1730
+ raw: raw,
1731
+ tokens: unique(groups.reduce(function (all, group) { return all.concat(group); }, [])),
1732
+ groups: groups
1733
+ };
1734
+ }
1735
+
1736
+ var SEARCH_STOP_WORDS = new Set([
1737
+ "a", "an", "and", "are", "about", "can", "do", "does", "for", "from", "how", "i", "in", "is", "it", "me", "of", "on", "or", "please", "show", "that", "the", "there", "this", "to", "what", "when", "where", "which", "who", "why", "with"
1738
+ ]);
1739
+
1740
+ var SEARCH_SYNONYMS = {
1741
+ memory: ["packet", "runbook", "decision", "workflow", "gotcha", "reference"],
1742
+ test: ["tests", "testing", "vitest", "jest", "pytest", "spec"],
1743
+ run: ["running", "command", "script", "npm", "pnpm", "yarn"],
1744
+ runn: ["run", "running", "command", "script", "npm", "pnpm", "yarn"],
1745
+ runtest: ["run", "test", "runbook", "command"],
1746
+ start: ["serve", "dev", "launch"],
1747
+ build: ["compile", "tsc"],
1748
+ bug: ["fix", "gotcha", "error"],
1749
+ route: ["endpoint", "api"],
1750
+ file: ["path"],
1751
+ dependency: ["package", "external", "deps"]
1752
+ };
1753
+
1754
+ function matchesSearchQuery(value, query) {
1755
+ if (!query || !query.active) return true;
1756
+ var text = searchableText(value);
1757
+ if (query.raw && text.indexOf(query.raw) !== -1) return true;
1758
+ if (!query.tokens.length) return true;
1759
+ var textTokens = new Set(text
1760
+ .replace(/[^a-z0-9_./:-]+/g, " ")
1761
+ .split(/\s+/)
1762
+ .map(searchStem)
1763
+ .filter(Boolean));
1764
+ var requiredGroups = (query.groups || []).filter(function (group) {
1765
+ return group.some(function (token) { return SEARCH_SOFT_TOKENS.has(token) ? text.indexOf(token) !== -1 || textTokens.has(token) : true; });
1766
+ });
1767
+ if (!requiredGroups.length) requiredGroups = query.groups || [query.tokens];
1768
+ return requiredGroups.every(function (group) {
1769
+ return group.some(function (token) {
1770
+ if (text.indexOf(token) !== -1) return true;
1771
+ return textTokens.has(token);
1772
+ });
1773
+ });
1774
+ }
1775
+
1776
+ var SEARCH_SOFT_TOKENS = new Set(["memory", "packet", "about"]);
1777
+
1778
+ function searchStem(value) {
1779
+ var token = String(value || "").toLowerCase();
1780
+ if (token.length > 5 && token.endsWith("ing")) token = token.slice(0, -3);
1781
+ if (token.length > 4 && token.endsWith("ies")) token = token.slice(0, -3) + "y";
1782
+ if (token.length > 4 && token.endsWith("es")) token = token.slice(0, -2);
1783
+ if (token.length > 3 && token.endsWith("s")) token = token.slice(0, -1);
1784
+ return token;
1785
+ }
1786
+
1656
1787
  function shortName(value, max) {
1657
1788
  var text = String(value || "");
1658
1789
  return text.length > max ? text.slice(0, Math.max(1, max - 1)) + "..." : text;
package/viewer/styles.css CHANGED
@@ -331,7 +331,7 @@ input:focus, select:focus, button:focus, .file-picker:focus-within {
331
331
  height: 100%;
332
332
  overflow: hidden;
333
333
  background:
334
- radial-gradient(circle at 52% 46%, rgba(65, 255, 143, 0.10), transparent 44%),
334
+ radial-gradient(circle at 52% 46%, rgba(65, 255, 143, 0.055), transparent 44%),
335
335
  #020503;
336
336
  }
337
337
 
@@ -367,10 +367,10 @@ input:focus, select:focus, button:focus, .file-picker:focus-within {
367
367
  display: none;
368
368
  max-width: min(320px, calc(100% - 28px));
369
369
  padding: 10px 12px;
370
- border: 1px solid rgba(65, 255, 143, 0.32);
371
- border-radius: 6px;
372
- background: rgba(3, 6, 4, 0.90);
373
- box-shadow: 0 14px 34px rgba(0, 0, 0, 0.38), inset 0 0 0 1px rgba(65, 255, 143, 0.08);
370
+ border: 1px solid rgba(65, 255, 143, 0.28);
371
+ border-radius: 4px;
372
+ background: rgba(2, 5, 3, 0.94);
373
+ box-shadow: 0 14px 34px rgba(0, 0, 0, 0.42), inset 0 0 0 1px rgba(65, 255, 143, 0.06);
374
374
  backdrop-filter: blur(12px);
375
375
  color: var(--text);
376
376
  pointer-events: none;
@@ -378,14 +378,14 @@ input:focus, select:focus, button:focus, .file-picker:focus-within {
378
378
  .graph-tooltip.visible { display: block; }
379
379
  .tt-name {
380
380
  color: var(--terminal);
381
- font-weight: 850;
381
+ font-weight: 820;
382
382
  overflow-wrap: anywhere;
383
383
  }
384
384
  .tt-type {
385
385
  margin-top: 4px;
386
386
  font-size: 10px;
387
387
  font-weight: 800;
388
- letter-spacing: 0.08em;
388
+ letter-spacing: 0.10em;
389
389
  text-transform: uppercase;
390
390
  }
391
391
  .tt-summary {