engramx 0.3.2 → 0.4.0

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
@@ -15,7 +15,7 @@
15
15
  <a href="https://github.com/NickCirv/engram/actions"><img src="https://github.com/NickCirv/engram/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
16
16
  <img src="https://img.shields.io/badge/license-Apache%202.0-blue" alt="License">
17
17
  <img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" alt="Node">
18
- <img src="https://img.shields.io/badge/tests-467%20passing-brightgreen" alt="Tests">
18
+ <img src="https://img.shields.io/badge/tests-486%20passing-brightgreen" alt="Tests">
19
19
  <img src="https://img.shields.io/badge/LLM%20cost-$0-green" alt="Zero LLM cost">
20
20
  <img src="https://img.shields.io/badge/native%20deps-zero-green" alt="Zero native deps">
21
21
  <img src="https://img.shields.io/badge/token%20reduction-82%25-orange" alt="82% token reduction">
@@ -310,7 +310,7 @@ function writeToFile(filePath, summary) {
310
310
  writeFileSync2(filePath, newContent);
311
311
  }
312
312
  async function autogen(projectRoot, target, task) {
313
- const { getStore } = await import("./core-AJD3SS6U.js");
313
+ const { getStore } = await import("./core-WTKXDUDO.js");
314
314
  const store = await getStore(projectRoot);
315
315
  try {
316
316
  let view = VIEWS.general;
@@ -65,7 +65,8 @@ var GraphStore = class _GraphStore {
65
65
  "CREATE INDEX IF NOT EXISTS idx_nodes_source_file ON nodes(source_file)",
66
66
  "CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source)",
67
67
  "CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target)",
68
- "CREATE INDEX IF NOT EXISTS idx_edges_relation ON edges(relation)"
68
+ "CREATE INDEX IF NOT EXISTS idx_edges_relation ON edges(relation)",
69
+ "CREATE INDEX IF NOT EXISTS idx_edges_source_file ON edges(source_file)"
69
70
  ];
70
71
  for (const sql of indexes) {
71
72
  try {
@@ -113,6 +114,22 @@ var GraphStore = class _GraphStore {
113
114
  ]
114
115
  );
115
116
  }
117
+ /**
118
+ * Remove all nodes and edges associated with a specific source file.
119
+ * Used by the file watcher for incremental re-indexing — old nodes for
120
+ * a changed file are cleared before re-extracting.
121
+ */
122
+ deleteBySourceFile(sourceFile) {
123
+ this.db.run("BEGIN TRANSACTION");
124
+ try {
125
+ this.db.run("DELETE FROM edges WHERE source_file = ?", [sourceFile]);
126
+ this.db.run("DELETE FROM nodes WHERE source_file = ?", [sourceFile]);
127
+ this.db.run("COMMIT");
128
+ } catch (e) {
129
+ this.db.run("ROLLBACK");
130
+ throw e;
131
+ }
132
+ }
116
133
  bulkUpsert(nodes, edges) {
117
134
  this.db.run("BEGIN TRANSACTION");
118
135
  for (const node of nodes) this.upsertNode(node);
package/dist/cli.js CHANGED
@@ -4,11 +4,14 @@ import {
4
4
  install,
5
5
  status,
6
6
  uninstall
7
- } from "./chunk-JXJNXQUM.js";
7
+ } from "./chunk-ESPAWLH6.js";
8
8
  import {
9
9
  benchmark,
10
10
  computeKeywordIDF,
11
+ extractFile,
12
+ getDbPath,
11
13
  getFileContext,
14
+ getStore,
12
15
  godNodes,
13
16
  init,
14
17
  learn,
@@ -17,13 +20,13 @@ import {
17
20
  query,
18
21
  stats,
19
22
  toPosixPath
20
- } from "./chunk-3NUHMLRV.js";
23
+ } from "./chunk-R46DNLNR.js";
21
24
 
22
25
  // src/cli.ts
23
26
  import { Command } from "commander";
24
27
  import chalk from "chalk";
25
28
  import {
26
- existsSync as existsSync6,
29
+ existsSync as existsSync7,
27
30
  readFileSync as readFileSync4,
28
31
  writeFileSync as writeFileSync2,
29
32
  mkdirSync,
@@ -31,7 +34,7 @@ import {
31
34
  copyFileSync,
32
35
  renameSync as renameSync3
33
36
  } from "fs";
34
- import { dirname as dirname3, join as join6, resolve as pathResolve } from "path";
37
+ import { dirname as dirname3, join as join7, resolve as pathResolve } from "path";
35
38
  import { homedir } from "os";
36
39
 
37
40
  // src/intercept/safety.ts
@@ -41,8 +44,8 @@ var PASSTHROUGH = null;
41
44
  var DEFAULT_HANDLER_TIMEOUT_MS = 2e3;
42
45
  async function withTimeout(promise, ms = DEFAULT_HANDLER_TIMEOUT_MS) {
43
46
  let timer;
44
- const timeout = new Promise((resolve3) => {
45
- timer = setTimeout(() => resolve3(PASSTHROUGH), ms);
47
+ const timeout = new Promise((resolve6) => {
48
+ timer = setTimeout(() => resolve6(PASSTHROUGH), ms);
46
49
  });
47
50
  try {
48
51
  return await Promise.race([promise, timeout]);
@@ -440,7 +443,10 @@ async function handleBash(payload) {
440
443
 
441
444
  // src/intercept/handlers/session-start.ts
442
445
  import { existsSync as existsSync3, readFileSync } from "fs";
446
+ import { execFile } from "child_process";
447
+ import { promisify } from "util";
443
448
  import { basename, dirname as dirname2, join as join3, resolve as resolve2 } from "path";
449
+ var execFileAsync = promisify(execFile);
444
450
  var MAX_GOD_NODES = 10;
445
451
  var MAX_LANDMINES_IN_BRIEF = 3;
446
452
  function readGitBranch(projectRoot) {
@@ -494,6 +500,37 @@ function formatBrief(args) {
494
500
  );
495
501
  return lines.join("\n");
496
502
  }
503
+ async function queryMempalace(projectName) {
504
+ try {
505
+ const { stdout } = await execFileAsync(
506
+ "mcp-mempalace",
507
+ ["mempalace-search", "--query", projectName],
508
+ { timeout: 1500, encoding: "utf-8" }
509
+ );
510
+ const trimmed = stdout.trim();
511
+ if (!trimmed || trimmed.length < 20) return null;
512
+ try {
513
+ const parsed = JSON.parse(trimmed);
514
+ const results = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.results) ? parsed.results : [];
515
+ if (results.length === 0) return null;
516
+ const lines = ["[mempalace] Recent context:"];
517
+ for (const r of results.slice(0, 3)) {
518
+ const content = typeof r === "string" ? r : typeof r?.content === "string" ? r.content : typeof r?.document === "string" ? r.document : null;
519
+ if (content) {
520
+ const short = content.length > 120 ? content.slice(0, 117) + "..." : content;
521
+ lines.push(` - ${short}`);
522
+ }
523
+ }
524
+ return lines.length > 1 ? lines.join("\n") : null;
525
+ } catch {
526
+ const maxLen = 400;
527
+ const capped = trimmed.length > maxLen ? trimmed.slice(0, maxLen - 3) + "..." : trimmed;
528
+ return `[mempalace] ${capped}`;
529
+ }
530
+ } catch {
531
+ return null;
532
+ }
533
+ }
497
534
  function describeAgo(ms) {
498
535
  if (ms < 0) return "just now";
499
536
  const s = Math.floor(ms / 1e3);
@@ -515,7 +552,9 @@ async function handleSessionStart(payload) {
515
552
  if (projectRoot === null) return PASSTHROUGH;
516
553
  if (isHookDisabled(projectRoot)) return PASSTHROUGH;
517
554
  try {
518
- const [gods, mistakeList, graphStats] = await Promise.all([
555
+ const branch = readGitBranch(projectRoot);
556
+ const projectName = basename(projectRoot);
557
+ const [gods, mistakeList, graphStats, mempalaceContext] = await Promise.all([
519
558
  godNodes(projectRoot, MAX_GOD_NODES).catch(() => []),
520
559
  mistakes(projectRoot, { limit: MAX_LANDMINES_IN_BRIEF }).catch(
521
560
  () => []
@@ -529,11 +568,10 @@ async function handleSessionStart(payload) {
529
568
  ambiguousPct: 0,
530
569
  lastMined: 0,
531
570
  totalQueryTokensSaved: 0
532
- }))
571
+ })),
572
+ queryMempalace(projectName)
533
573
  ]);
534
574
  if (graphStats.nodes === 0 && gods.length === 0) return PASSTHROUGH;
535
- const branch = readGitBranch(projectRoot);
536
- const projectName = basename(projectRoot);
537
575
  const text = formatBrief({
538
576
  projectName,
539
577
  branch,
@@ -549,7 +587,8 @@ async function handleSessionStart(payload) {
549
587
  sourceFile: m.sourceFile
550
588
  }))
551
589
  });
552
- return buildSessionContextResponse("SessionStart", text);
590
+ const fullText = mempalaceContext ? text + "\n\n" + mempalaceContext : text;
591
+ return buildSessionContextResponse("SessionStart", fullText);
553
592
  } catch {
554
593
  return PASSTHROUGH;
555
594
  }
@@ -809,6 +848,123 @@ async function handlePostTool(payload) {
809
848
  return PASSTHROUGH;
810
849
  }
811
850
 
851
+ // src/intercept/handlers/pre-compact.ts
852
+ import { basename as basename2, resolve as resolve3 } from "path";
853
+ var MAX_GOD_NODES_COMPACT = 5;
854
+ var MAX_LANDMINES_COMPACT = 3;
855
+ function formatCompactBrief(args) {
856
+ const lines = [];
857
+ lines.push(
858
+ `[engram] Compaction survival \u2014 ${args.projectName} (${args.nodeCount} nodes, ${args.edgeCount} edges)`
859
+ );
860
+ if (args.godNodes.length > 0) {
861
+ lines.push("Key entities:");
862
+ for (const g of args.godNodes) {
863
+ lines.push(` - ${g.label} [${g.kind}] \u2014 ${g.sourceFile}`);
864
+ }
865
+ }
866
+ if (args.landmines.length > 0) {
867
+ lines.push("Active landmines:");
868
+ for (const m of args.landmines) {
869
+ lines.push(` - ${m.sourceFile}: ${m.label}`);
870
+ }
871
+ }
872
+ lines.push(
873
+ "engram is active \u2014 Read/Edit/Write interception continues after compaction."
874
+ );
875
+ return lines.join("\n");
876
+ }
877
+ async function handlePreCompact(payload) {
878
+ if (payload.hook_event_name !== "PreCompact") return PASSTHROUGH;
879
+ const cwd = payload.cwd;
880
+ if (!isValidCwd(cwd)) return PASSTHROUGH;
881
+ const projectRoot = findProjectRoot(cwd);
882
+ if (projectRoot === null) return PASSTHROUGH;
883
+ if (isHookDisabled(projectRoot)) return PASSTHROUGH;
884
+ try {
885
+ const [gods, mistakeList, graphStats] = await Promise.all([
886
+ godNodes(projectRoot, MAX_GOD_NODES_COMPACT).catch(() => []),
887
+ mistakes(projectRoot, { limit: MAX_LANDMINES_COMPACT }).catch(
888
+ () => []
889
+ ),
890
+ stats(projectRoot).catch(() => ({
891
+ nodes: 0,
892
+ edges: 0,
893
+ communities: 0,
894
+ extractedPct: 0,
895
+ inferredPct: 0,
896
+ ambiguousPct: 0,
897
+ lastMined: 0,
898
+ totalQueryTokensSaved: 0
899
+ }))
900
+ ]);
901
+ if (graphStats.nodes === 0 && gods.length === 0) return PASSTHROUGH;
902
+ const projectName = basename2(resolve3(projectRoot));
903
+ const text = formatCompactBrief({
904
+ projectName,
905
+ nodeCount: graphStats.nodes,
906
+ edgeCount: graphStats.edges,
907
+ godNodes: gods.map((g) => ({
908
+ label: g.label,
909
+ kind: g.kind,
910
+ sourceFile: g.sourceFile
911
+ })),
912
+ landmines: mistakeList.map((m) => ({
913
+ label: m.label,
914
+ sourceFile: m.sourceFile
915
+ }))
916
+ });
917
+ return buildSessionContextResponse("SessionStart", text);
918
+ } catch {
919
+ return PASSTHROUGH;
920
+ }
921
+ }
922
+
923
+ // src/intercept/handlers/cwd-changed.ts
924
+ import { basename as basename3, resolve as resolve4 } from "path";
925
+ var MAX_GOD_NODES_SWITCH = 5;
926
+ async function handleCwdChanged(payload) {
927
+ if (payload.hook_event_name !== "CwdChanged") return PASSTHROUGH;
928
+ const cwd = payload.cwd;
929
+ if (!isValidCwd(cwd)) return PASSTHROUGH;
930
+ const projectRoot = findProjectRoot(cwd);
931
+ if (projectRoot === null) return PASSTHROUGH;
932
+ if (isHookDisabled(projectRoot)) return PASSTHROUGH;
933
+ try {
934
+ const [gods, graphStats] = await Promise.all([
935
+ godNodes(projectRoot, MAX_GOD_NODES_SWITCH).catch(() => []),
936
+ stats(projectRoot).catch(() => ({
937
+ nodes: 0,
938
+ edges: 0,
939
+ communities: 0,
940
+ extractedPct: 0,
941
+ inferredPct: 0,
942
+ ambiguousPct: 0,
943
+ lastMined: 0,
944
+ totalQueryTokensSaved: 0
945
+ }))
946
+ ]);
947
+ if (graphStats.nodes === 0) return PASSTHROUGH;
948
+ const projectName = basename3(resolve4(projectRoot));
949
+ const lines = [];
950
+ lines.push(
951
+ `[engram] Project switched to ${projectName} (${graphStats.nodes} nodes, ${graphStats.edges} edges)`
952
+ );
953
+ if (gods.length > 0) {
954
+ lines.push("Core entities:");
955
+ for (const g of gods.slice(0, MAX_GOD_NODES_SWITCH)) {
956
+ lines.push(` - ${g.label} [${g.kind}] \u2014 ${g.sourceFile}`);
957
+ }
958
+ }
959
+ lines.push(
960
+ "engram interception is active for this project."
961
+ );
962
+ return buildSessionContextResponse("SessionStart", lines.join("\n"));
963
+ } catch {
964
+ return PASSTHROUGH;
965
+ }
966
+ }
967
+
812
968
  // src/intercept/dispatch.ts
813
969
  function validatePayload(raw) {
814
970
  if (raw === null || typeof raw !== "object") return null;
@@ -839,6 +995,14 @@ async function dispatchHook(rawPayload) {
839
995
  return runHandler(
840
996
  () => handlePostTool(payload)
841
997
  );
998
+ case "PreCompact":
999
+ return runHandler(
1000
+ () => handlePreCompact(payload)
1001
+ );
1002
+ case "CwdChanged":
1003
+ return runHandler(
1004
+ () => handleCwdChanged(payload)
1005
+ );
842
1006
  default:
843
1007
  return PASSTHROUGH;
844
1008
  }
@@ -898,6 +1062,106 @@ function extractPreToolDecision(result) {
898
1062
  return "passthrough";
899
1063
  }
900
1064
 
1065
+ // src/watcher.ts
1066
+ import { watch, existsSync as existsSync5, statSync as statSync3 } from "fs";
1067
+ import { resolve as resolve5, relative as relative2, extname } from "path";
1068
+ var WATCHABLE_EXTENSIONS = /* @__PURE__ */ new Set([
1069
+ ".ts",
1070
+ ".tsx",
1071
+ ".js",
1072
+ ".jsx",
1073
+ ".py",
1074
+ ".go",
1075
+ ".rs",
1076
+ ".java",
1077
+ ".c",
1078
+ ".cpp",
1079
+ ".cs",
1080
+ ".rb"
1081
+ ]);
1082
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
1083
+ ".engram",
1084
+ "node_modules",
1085
+ ".git",
1086
+ "dist",
1087
+ "build",
1088
+ ".next",
1089
+ "__pycache__",
1090
+ ".venv",
1091
+ "target",
1092
+ "vendor"
1093
+ ]);
1094
+ var DEBOUNCE_MS = 300;
1095
+ function shouldIgnore(relPath) {
1096
+ const parts = relPath.split(/[/\\]/);
1097
+ return parts.some((p) => IGNORED_DIRS.has(p));
1098
+ }
1099
+ async function reindexFile(absPath, projectRoot) {
1100
+ const ext = extname(absPath).toLowerCase();
1101
+ if (!WATCHABLE_EXTENSIONS.has(ext)) return 0;
1102
+ if (!existsSync5(absPath)) return 0;
1103
+ try {
1104
+ if (statSync3(absPath).isDirectory()) return 0;
1105
+ } catch {
1106
+ return 0;
1107
+ }
1108
+ const relPath = toPosixPath(relative2(projectRoot, absPath));
1109
+ if (shouldIgnore(relPath)) return 0;
1110
+ const store = await getStore(projectRoot);
1111
+ try {
1112
+ store.deleteBySourceFile(relPath);
1113
+ const { nodes, edges } = extractFile(absPath, projectRoot);
1114
+ if (nodes.length > 0 || edges.length > 0) {
1115
+ store.bulkUpsert(nodes, edges);
1116
+ }
1117
+ return nodes.length;
1118
+ } finally {
1119
+ store.close();
1120
+ }
1121
+ }
1122
+ function watchProject(projectRoot, options = {}) {
1123
+ const root = resolve5(projectRoot);
1124
+ const controller = new AbortController();
1125
+ if (!existsSync5(getDbPath(root))) {
1126
+ throw new Error(
1127
+ `engram: no graph found at ${root}. Run 'engram init' first.`
1128
+ );
1129
+ }
1130
+ const debounceTimers = /* @__PURE__ */ new Map();
1131
+ const watcher = watch(root, { recursive: true, signal: controller.signal });
1132
+ watcher.on("change", (_eventType, filename) => {
1133
+ if (typeof filename !== "string") return;
1134
+ const absPath = resolve5(root, filename);
1135
+ const relPath = toPosixPath(relative2(root, absPath));
1136
+ if (shouldIgnore(relPath)) return;
1137
+ const ext = extname(filename).toLowerCase();
1138
+ if (!WATCHABLE_EXTENSIONS.has(ext)) return;
1139
+ const existing = debounceTimers.get(absPath);
1140
+ if (existing) clearTimeout(existing);
1141
+ debounceTimers.set(
1142
+ absPath,
1143
+ setTimeout(async () => {
1144
+ debounceTimers.delete(absPath);
1145
+ try {
1146
+ const count = await reindexFile(absPath, root);
1147
+ if (count > 0) {
1148
+ options.onReindex?.(relPath, count);
1149
+ }
1150
+ } catch (err) {
1151
+ options.onError?.(
1152
+ err instanceof Error ? err : new Error(String(err))
1153
+ );
1154
+ }
1155
+ }, DEBOUNCE_MS)
1156
+ );
1157
+ });
1158
+ watcher.on("error", (err) => {
1159
+ options.onError?.(err instanceof Error ? err : new Error(String(err)));
1160
+ });
1161
+ options.onReady?.();
1162
+ return controller;
1163
+ }
1164
+
901
1165
  // src/intercept/cursor-adapter.ts
902
1166
  var ALLOW = { permission: "allow" };
903
1167
  function toClaudeReadPayload(cursorPayload) {
@@ -941,7 +1205,9 @@ var ENGRAM_HOOK_EVENTS = [
941
1205
  "PreToolUse",
942
1206
  "PostToolUse",
943
1207
  "SessionStart",
944
- "UserPromptSubmit"
1208
+ "UserPromptSubmit",
1209
+ "PreCompact",
1210
+ "CwdChanged"
945
1211
  ];
946
1212
  var ENGRAM_PRETOOL_MATCHER = "Read|Edit|Write|Bash";
947
1213
  var DEFAULT_ENGRAM_COMMAND = "engram intercept";
@@ -969,6 +1235,14 @@ function buildEngramHookEntries(command = DEFAULT_ENGRAM_COMMAND, timeout = DEFA
969
1235
  UserPromptSubmit: {
970
1236
  // No matcher — UserPromptSubmit has no tool name.
971
1237
  hooks: [baseCmd]
1238
+ },
1239
+ PreCompact: {
1240
+ // No matcher — PreCompact has no tool name.
1241
+ hooks: [baseCmd]
1242
+ },
1243
+ CwdChanged: {
1244
+ // No matcher — CwdChanged has no tool name.
1245
+ hooks: [baseCmd]
972
1246
  }
973
1247
  };
974
1248
  }
@@ -1152,13 +1426,13 @@ function formatStatsSummary(summary) {
1152
1426
 
1153
1427
  // src/intercept/memory-md.ts
1154
1428
  import {
1155
- existsSync as existsSync5,
1429
+ existsSync as existsSync6,
1156
1430
  readFileSync as readFileSync3,
1157
1431
  writeFileSync,
1158
1432
  renameSync as renameSync2,
1159
- statSync as statSync3
1433
+ statSync as statSync4
1160
1434
  } from "fs";
1161
- import { join as join5 } from "path";
1435
+ import { join as join6 } from "path";
1162
1436
  var ENGRAM_MARKER_START = "<!-- engram:structural-facts:start -->";
1163
1437
  var ENGRAM_MARKER_END = "<!-- engram:structural-facts:end -->";
1164
1438
  var MAX_MEMORY_FILE_BYTES = 1e6;
@@ -1229,11 +1503,11 @@ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
1229
1503
  if (engramSection.length > MAX_ENGRAM_SECTION_BYTES) {
1230
1504
  return false;
1231
1505
  }
1232
- const memoryPath = join5(projectRoot, "MEMORY.md");
1506
+ const memoryPath = join6(projectRoot, "MEMORY.md");
1233
1507
  try {
1234
1508
  let existing = "";
1235
- if (existsSync5(memoryPath)) {
1236
- const st = statSync3(memoryPath);
1509
+ if (existsSync6(memoryPath)) {
1510
+ const st = statSync4(memoryPath);
1237
1511
  if (st.size > MAX_MEMORY_FILE_BYTES) {
1238
1512
  return false;
1239
1513
  }
@@ -1250,7 +1524,7 @@ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
1250
1524
  }
1251
1525
 
1252
1526
  // src/cli.ts
1253
- import { basename as basename2 } from "path";
1527
+ import { basename as basename4 } from "path";
1254
1528
  var program = new Command();
1255
1529
  program.name("engram").description(
1256
1530
  "Context as infra for AI coding tools \u2014 hook-based Read/Edit interception + structural graph summaries"
@@ -1287,9 +1561,9 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
1287
1561
  console.log(chalk.green("\n\u2705 Ready. Your AI now has persistent memory."));
1288
1562
  console.log(chalk.dim(" Graph stored in .engram/graph.db"));
1289
1563
  const resolvedProject = pathResolve(projectPath);
1290
- const localSettings = join6(resolvedProject, ".claude", "settings.local.json");
1291
- const projectSettings = join6(resolvedProject, ".claude", "settings.json");
1292
- const hasHooks = existsSync6(localSettings) && readFileSync4(localSettings, "utf-8").includes("engram intercept") || existsSync6(projectSettings) && readFileSync4(projectSettings, "utf-8").includes("engram intercept");
1564
+ const localSettings = join7(resolvedProject, ".claude", "settings.local.json");
1565
+ const projectSettings = join7(resolvedProject, ".claude", "settings.json");
1566
+ const hasHooks = existsSync7(localSettings) && readFileSync4(localSettings, "utf-8").includes("engram intercept") || existsSync7(projectSettings) && readFileSync4(projectSettings, "utf-8").includes("engram intercept");
1293
1567
  if (!hasHooks) {
1294
1568
  console.log(
1295
1569
  chalk.yellow("\n\u{1F4A1} Next step: ") + chalk.white("engram install-hook") + chalk.dim(
@@ -1303,6 +1577,32 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
1303
1577
  );
1304
1578
  }
1305
1579
  });
1580
+ program.command("watch").description("Watch project for file changes and re-index incrementally").argument("[path]", "Project directory", ".").action(async (projectPath) => {
1581
+ const resolvedPath = pathResolve(projectPath);
1582
+ console.log(
1583
+ chalk.dim("\u{1F441} Watching ") + chalk.white(resolvedPath) + chalk.dim(" for changes...")
1584
+ );
1585
+ const controller = watchProject(resolvedPath, {
1586
+ onReindex: (filePath, nodeCount) => {
1587
+ console.log(
1588
+ chalk.green(" \u21BB ") + chalk.white(filePath) + chalk.dim(` (${nodeCount} nodes)`)
1589
+ );
1590
+ },
1591
+ onError: (err) => {
1592
+ console.error(chalk.red(" \u2717 ") + err.message);
1593
+ },
1594
+ onReady: () => {
1595
+ console.log(chalk.green(" \u2713 Watcher active.") + chalk.dim(" Press Ctrl+C to stop."));
1596
+ }
1597
+ });
1598
+ process.on("SIGINT", () => {
1599
+ controller.abort();
1600
+ console.log(chalk.dim("\n Watcher stopped."));
1601
+ process.exit(0);
1602
+ });
1603
+ await new Promise(() => {
1604
+ });
1605
+ });
1306
1606
  program.command("query").description("Query the knowledge graph").argument("<question>", "Natural language question or keywords").option("--dfs", "Use DFS traversal", false).option("-d, --depth <n>", "Traversal depth", "3").option("-b, --budget <n>", "Token budget", "2000").option("-p, --project <path>", "Project directory", ".").action(async (question, opts) => {
1307
1607
  const result = await query(opts.project, question, {
1308
1608
  mode: opts.dfs ? "dfs" : "bfs",
@@ -1428,11 +1728,11 @@ function resolveSettingsPath(scope, projectPath) {
1428
1728
  const absProject = pathResolve(projectPath);
1429
1729
  switch (scope) {
1430
1730
  case "local":
1431
- return join6(absProject, ".claude", "settings.local.json");
1731
+ return join7(absProject, ".claude", "settings.local.json");
1432
1732
  case "project":
1433
- return join6(absProject, ".claude", "settings.json");
1733
+ return join7(absProject, ".claude", "settings.json");
1434
1734
  case "user":
1435
- return join6(homedir(), ".claude", "settings.json");
1735
+ return join7(homedir(), ".claude", "settings.json");
1436
1736
  default:
1437
1737
  return null;
1438
1738
  }
@@ -1526,7 +1826,7 @@ program.command("install-hook").description("Install engram hook entries into Cl
1526
1826
  process.exit(1);
1527
1827
  }
1528
1828
  let existing = {};
1529
- if (existsSync6(settingsPath)) {
1829
+ if (existsSync7(settingsPath)) {
1530
1830
  try {
1531
1831
  const raw = readFileSync4(settingsPath, "utf-8");
1532
1832
  existing = raw.trim() ? JSON.parse(raw) : {};
@@ -1574,7 +1874,7 @@ program.command("install-hook").description("Install engram hook entries into Cl
1574
1874
  }
1575
1875
  try {
1576
1876
  mkdirSync(dirname3(settingsPath), { recursive: true });
1577
- if (existsSync6(settingsPath)) {
1877
+ if (existsSync7(settingsPath)) {
1578
1878
  const backupPath = `${settingsPath}.engram-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
1579
1879
  copyFileSync(settingsPath, backupPath);
1580
1880
  console.log(chalk.dim(` Backup: ${backupPath}`));
@@ -1618,7 +1918,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
1618
1918
  console.error(chalk.red(`Unknown scope: ${opts.scope}`));
1619
1919
  process.exit(1);
1620
1920
  }
1621
- if (!existsSync6(settingsPath)) {
1921
+ if (!existsSync7(settingsPath)) {
1622
1922
  console.log(
1623
1923
  chalk.yellow(`No settings file at ${settingsPath} \u2014 nothing to remove.`)
1624
1924
  );
@@ -1734,7 +2034,7 @@ program.command("hook-disable").description("Disable engram hooks via kill switc
1734
2034
  console.error(chalk.dim("Run 'engram init' first."));
1735
2035
  process.exit(1);
1736
2036
  }
1737
- const flagPath = join6(projectRoot, ".engram", "hook-disabled");
2037
+ const flagPath = join7(projectRoot, ".engram", "hook-disabled");
1738
2038
  try {
1739
2039
  writeFileSync2(flagPath, (/* @__PURE__ */ new Date()).toISOString());
1740
2040
  console.log(
@@ -1758,8 +2058,8 @@ program.command("hook-enable").description("Re-enable engram hooks (remove kill
1758
2058
  console.error(chalk.red(`Not an engram project: ${absProject}`));
1759
2059
  process.exit(1);
1760
2060
  }
1761
- const flagPath = join6(projectRoot, ".engram", "hook-disabled");
1762
- if (!existsSync6(flagPath)) {
2061
+ const flagPath = join7(projectRoot, ".engram", "hook-disabled");
2062
+ if (!existsSync7(flagPath)) {
1763
2063
  console.log(
1764
2064
  chalk.yellow(`engram hooks already enabled for ${projectRoot}`)
1765
2065
  );
@@ -1801,8 +2101,8 @@ program.command("memory-sync").description(
1801
2101
  }
1802
2102
  let branch = null;
1803
2103
  try {
1804
- const headPath = join6(projectRoot, ".git", "HEAD");
1805
- if (existsSync6(headPath)) {
2104
+ const headPath = join7(projectRoot, ".git", "HEAD");
2105
+ if (existsSync7(headPath)) {
1806
2106
  const content = readFileSync4(headPath, "utf-8").trim();
1807
2107
  const m = content.match(/^ref:\s+refs\/heads\/(.+)$/);
1808
2108
  if (m) branch = m[1];
@@ -1810,7 +2110,7 @@ program.command("memory-sync").description(
1810
2110
  } catch {
1811
2111
  }
1812
2112
  const section = buildEngramSection({
1813
- projectName: basename2(projectRoot),
2113
+ projectName: basename4(projectRoot),
1814
2114
  branch,
1815
2115
  stats: {
1816
2116
  nodes: graphStats.nodes,
@@ -1829,7 +2129,7 @@ program.command("memory-sync").description(
1829
2129
  \u{1F4DD} engram memory-sync`)
1830
2130
  );
1831
2131
  console.log(
1832
- chalk.dim(` Target: ${join6(projectRoot, "MEMORY.md")}`)
2132
+ chalk.dim(` Target: ${join7(projectRoot, "MEMORY.md")}`)
1833
2133
  );
1834
2134
  if (opts.dryRun) {
1835
2135
  console.log(chalk.cyan("\n Section to write (dry-run):\n"));
@@ -11,7 +11,7 @@ import {
11
11
  path,
12
12
  query,
13
13
  stats
14
- } from "./chunk-3NUHMLRV.js";
14
+ } from "./chunk-R46DNLNR.js";
15
15
  export {
16
16
  benchmark,
17
17
  computeKeywordIDF,
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  generateSummary,
5
5
  install,
6
6
  uninstall
7
- } from "./chunk-JXJNXQUM.js";
7
+ } from "./chunk-ESPAWLH6.js";
8
8
  import {
9
9
  GraphStore,
10
10
  SUPPORTED_EXTENSIONS,
@@ -23,7 +23,7 @@ import {
23
23
  sliceGraphemeSafe,
24
24
  stats,
25
25
  truncateGraphemeSafe
26
- } from "./chunk-3NUHMLRV.js";
26
+ } from "./chunk-R46DNLNR.js";
27
27
  export {
28
28
  GraphStore,
29
29
  SUPPORTED_EXTENSIONS,
package/dist/serve.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  query,
9
9
  stats,
10
10
  truncateGraphemeSafe
11
- } from "./chunk-3NUHMLRV.js";
11
+ } from "./chunk-R46DNLNR.js";
12
12
 
13
13
  // src/serve.ts
14
14
  function clampInt(value, defaultValue, min, max) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engramx",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "The structural code graph your AI agent can't forget to use. A Claude Code hook layer that intercepts Read/Edit/Write/Bash and replaces file contents with ~300-token structural graph summaries. 82% measured token reduction. Context rot is empirically solved — cite Chroma. Local SQLite, zero LLM cost, zero cloud, zero native deps.",
5
5
  "type": "module",
6
6
  "bin": {