engramx 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,17 +18,33 @@
18
18
  <img src="https://img.shields.io/badge/tests-439%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
+ <img src="https://img.shields.io/badge/token%20reduction-82%25-orange" alt="82% token reduction">
21
22
  </p>
22
23
 
23
24
  ---
24
25
 
25
- **Context as infra for your AI coding tools.**
26
+ # The structural code graph your AI agent can't forget to use.
26
27
 
27
- engram installs a Claude Code hook layer that intercepts every `Read`, `Edit`, `Write`, and `Bash cat` replacing full file reads with ~300-token structural graph summaries *before the agent even sees them*. No more re-exploring the codebase every session. No more agents forgetting to use the tool you gave them.
28
+ **Context rot is empirically solved.** Chroma's July 2025 research proved that even Claude Opus 4.6 scores only 76% on MRCR v2 8-needle at 1M tokens. Long context windows don't save you they drown you. engram is the answer: a **structural graph** of your codebase that replaces file reads with ~300-token summaries *before the agent sees them*.
28
29
 
29
- **v0.3 "Sentinel":** the agent can't forget to use engram because engram sits between the agent and the filesystem.
30
+ engram installs a Claude Code hook layer at the tool-call boundary. Every `Read`, `Edit`, `Write`, and `Bash cat` gets intercepted. When the graph has confident coverage of a file, the raw read never happens — the agent sees a structural summary instead.
30
31
 
31
- Zero LLM cost. Zero cloud. Zero native deps. Works today in Claude Code.
32
+ Not a memory tool. Not a RAG layer. Not a context manager. **A structural code graph with a Claude Code hook layer that turns it into the memory your agent can't forget to exist.**
33
+
34
+ | What it is | What it isn't |
35
+ |---|---|
36
+ | Structural code graph (AST + git + session miners) | Prose memory like Anthropic's MEMORY.md |
37
+ | Local SQLite, zero cloud, zero native deps | Vector RAG that phones home |
38
+ | Hook-based interception at the tool boundary | A tool the agent has to remember to call |
39
+ | 82% measured token reduction on real code | Another LongMemEval chatbot benchmark |
40
+ | Complements native Claude memory | Competes with native Claude memory |
41
+
42
+ ```bash
43
+ npm install -g engramx
44
+ cd ~/my-project
45
+ engram init # scan codebase → .engram/graph.db (~40 ms, 0 tokens)
46
+ engram install-hook # wire the Sentinel into Claude Code
47
+ ```
32
48
 
33
49
  ```bash
34
50
  npm install -g engramx
@@ -45,6 +61,13 @@ That's it. The next Claude Code session in that directory automatically:
45
61
  - **Injects a project brief at session start** (SessionStart additionalContext)
46
62
  - **Logs every decision for `engram hook-stats`** (PostToolUse observer)
47
63
 
64
+ ## Architecture Diagram
65
+
66
+ An 11-page visual walkthrough of the full lifecycle — the four hook events, the Read handler's 9-branch decision tree (with a real JSON response), the six-layer ecosystem substrate, and measured numbers from engram's own code.
67
+
68
+ - 📄 **[View the PDF](docs/engram-sentinel-ecosystem.pdf)** — A3 landscape, 11 pages, 1 MB. GitHub renders it inline when clicked.
69
+ - 🌐 **[HTML source](docs/engram-sentinel-ecosystem.html)** — single self-contained file. Download raw and open in any browser for the interactive scroll-reveal version.
70
+
48
71
  ## The Problem
49
72
 
50
73
  Every Claude Code session burns ~52,500 tokens on things you already told the agent yesterday. Reading the same files, re-exploring the same modules, re-discovering the same patterns. Even with a great CLAUDE.md, the agent still falls back to `Read` because `Read` is what it knows.
@@ -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-MPNNCPFW.js");
313
+ const { getStore } = await import("./core-2TWPNHRQ.js");
314
314
  const store = await getStore(projectRoot);
315
315
  try {
316
316
  let view = VIEWS.general;
@@ -1660,6 +1660,41 @@ async function getFileContext(projectRoot, absFilePath) {
1660
1660
  return empty;
1661
1661
  }
1662
1662
  }
1663
+ async function computeKeywordIDF(projectRoot, keywords) {
1664
+ if (keywords.length === 0) return [];
1665
+ try {
1666
+ const root = resolve2(projectRoot);
1667
+ const dbPath = getDbPath(root);
1668
+ if (!existsSync5(dbPath)) return [];
1669
+ const store = await getStore(root);
1670
+ try {
1671
+ const allNodes = store.getAllNodes();
1672
+ const total = allNodes.length;
1673
+ if (total === 0) return [];
1674
+ const labels = allNodes.map((n) => n.label.toLowerCase());
1675
+ const results = [];
1676
+ for (const kw of keywords) {
1677
+ const kwLower = kw.toLowerCase();
1678
+ let df = 0;
1679
+ for (const label of labels) {
1680
+ if (label.includes(kwLower)) df += 1;
1681
+ }
1682
+ const idf = df === 0 ? 0 : Math.log(total / df);
1683
+ results.push({
1684
+ keyword: kw,
1685
+ documentFrequency: df,
1686
+ idf
1687
+ });
1688
+ }
1689
+ results.sort((a, b) => b.idf - a.idf);
1690
+ return results;
1691
+ } finally {
1692
+ store.close();
1693
+ }
1694
+ } catch {
1695
+ return [];
1696
+ }
1697
+ }
1663
1698
  async function learn(projectRoot, text, sourceLabel = "manual") {
1664
1699
  const { nodes, edges } = learnFromSession(text, sourceLabel);
1665
1700
  if (nodes.length === 0 && edges.length === 0) return { nodesAdded: 0 };
@@ -1778,6 +1813,7 @@ export {
1778
1813
  godNodes,
1779
1814
  stats,
1780
1815
  getFileContext,
1816
+ computeKeywordIDF,
1781
1817
  learn,
1782
1818
  mistakes,
1783
1819
  benchmark
package/dist/cli.js CHANGED
@@ -4,9 +4,10 @@ import {
4
4
  install,
5
5
  status,
6
6
  uninstall
7
- } from "./chunk-7KYR4SPZ.js";
7
+ } from "./chunk-IYO4HETA.js";
8
8
  import {
9
9
  benchmark,
10
+ computeKeywordIDF,
10
11
  getFileContext,
11
12
  godNodes,
12
13
  init,
@@ -15,21 +16,21 @@ import {
15
16
  path,
16
17
  query,
17
18
  stats
18
- } from "./chunk-NYFDM4FR.js";
19
+ } from "./chunk-RGDHLGWQ.js";
19
20
 
20
21
  // src/cli.ts
21
22
  import { Command } from "commander";
22
23
  import chalk from "chalk";
23
24
  import {
24
- existsSync as existsSync5,
25
- readFileSync as readFileSync3,
26
- writeFileSync,
25
+ existsSync as existsSync6,
26
+ readFileSync as readFileSync4,
27
+ writeFileSync as writeFileSync2,
27
28
  mkdirSync,
28
29
  unlinkSync,
29
30
  copyFileSync,
30
- renameSync as renameSync2
31
+ renameSync as renameSync3
31
32
  } from "fs";
32
- import { dirname as dirname3, join as join5, resolve as pathResolve } from "path";
33
+ import { dirname as dirname3, join as join6, resolve as pathResolve } from "path";
33
34
  import { homedir } from "os";
34
35
 
35
36
  // src/intercept/safety.ts
@@ -548,6 +549,8 @@ async function handleSessionStart(payload) {
548
549
  var MIN_SIGNIFICANT_TERMS = 2;
549
550
  var MIN_MATCHED_NODES = 3;
550
551
  var PROMPT_INJECTION_TOKEN_BUDGET = 500;
552
+ var MIN_IDF_THRESHOLD = 1.386;
553
+ var MAX_SEED_KEYWORDS = 5;
551
554
  var STOPWORDS = /* @__PURE__ */ new Set([
552
555
  "a",
553
556
  "an",
@@ -642,13 +645,32 @@ async function handleUserPromptSubmit(payload) {
642
645
  const prompt = payload.prompt;
643
646
  if (!prompt || typeof prompt !== "string") return PASSTHROUGH;
644
647
  if (prompt.length > 8e3) return PASSTHROUGH;
645
- const keywords = extractKeywords(prompt);
646
- if (keywords.length < MIN_SIGNIFICANT_TERMS) return PASSTHROUGH;
648
+ const rawKeywords = extractKeywords(prompt);
649
+ if (rawKeywords.length < MIN_SIGNIFICANT_TERMS) return PASSTHROUGH;
647
650
  const cwd = payload.cwd;
648
651
  if (!isValidCwd(cwd)) return PASSTHROUGH;
649
652
  const projectRoot = findProjectRoot(cwd);
650
653
  if (projectRoot === null) return PASSTHROUGH;
651
654
  if (isHookDisabled(projectRoot)) return PASSTHROUGH;
655
+ let keywords;
656
+ try {
657
+ const scored = await computeKeywordIDF(projectRoot, rawKeywords);
658
+ if (scored.length === 0) {
659
+ keywords = rawKeywords;
660
+ } else {
661
+ const discriminative = scored.filter((s) => s.idf >= MIN_IDF_THRESHOLD);
662
+ if (discriminative.length === 0) {
663
+ return PASSTHROUGH;
664
+ }
665
+ keywords = scored.filter((s) => s.idf > 0).slice(0, MAX_SEED_KEYWORDS).map((s) => s.keyword);
666
+ if (keywords.length === 0) {
667
+ keywords = rawKeywords;
668
+ }
669
+ }
670
+ } catch {
671
+ keywords = rawKeywords;
672
+ }
673
+ if (keywords.length === 0) return PASSTHROUGH;
652
674
  let result;
653
675
  try {
654
676
  result = await query(projectRoot, keywords.join(" "), {
@@ -866,6 +888,44 @@ function extractPreToolDecision(result) {
866
888
  return "passthrough";
867
889
  }
868
890
 
891
+ // src/intercept/cursor-adapter.ts
892
+ var ALLOW = { permission: "allow" };
893
+ function toClaudeReadPayload(cursorPayload) {
894
+ const filePath = cursorPayload.file_path;
895
+ if (!filePath || typeof filePath !== "string") return null;
896
+ const workspaceRoot = Array.isArray(cursorPayload.workspace_roots) && cursorPayload.workspace_roots.length > 0 ? cursorPayload.workspace_roots[0] : process.cwd();
897
+ return {
898
+ tool_name: "Read",
899
+ tool_input: { file_path: filePath },
900
+ cwd: workspaceRoot
901
+ };
902
+ }
903
+ function extractSummaryFromClaudeResult(result) {
904
+ if (result === PASSTHROUGH || result === null) return null;
905
+ if (typeof result !== "object") return null;
906
+ const hookSpecific = result.hookSpecificOutput;
907
+ if (!hookSpecific || typeof hookSpecific !== "object") return null;
908
+ const reason = hookSpecific.permissionDecisionReason;
909
+ if (typeof reason !== "string" || reason.length === 0) return null;
910
+ return reason;
911
+ }
912
+ async function handleCursorBeforeReadFile(payload) {
913
+ try {
914
+ if (!payload || typeof payload !== "object") return ALLOW;
915
+ const claudePayload = toClaudeReadPayload(payload);
916
+ if (claudePayload === null) return ALLOW;
917
+ const result = await handleRead(claudePayload);
918
+ const summary = extractSummaryFromClaudeResult(result);
919
+ if (summary === null) return ALLOW;
920
+ return {
921
+ permission: "deny",
922
+ user_message: summary
923
+ };
924
+ } catch {
925
+ return ALLOW;
926
+ }
927
+ }
928
+
869
929
  // src/intercept/installer.ts
870
930
  var ENGRAM_HOOK_EVENTS = [
871
931
  "PreToolUse",
@@ -1080,7 +1140,107 @@ function formatStatsSummary(summary) {
1080
1140
  return lines.join("\n");
1081
1141
  }
1082
1142
 
1143
+ // src/intercept/memory-md.ts
1144
+ import {
1145
+ existsSync as existsSync5,
1146
+ readFileSync as readFileSync3,
1147
+ writeFileSync,
1148
+ renameSync as renameSync2,
1149
+ statSync as statSync3
1150
+ } from "fs";
1151
+ import { join as join5 } from "path";
1152
+ var ENGRAM_MARKER_START = "<!-- engram:structural-facts:start -->";
1153
+ var ENGRAM_MARKER_END = "<!-- engram:structural-facts:end -->";
1154
+ var MAX_MEMORY_FILE_BYTES = 1e6;
1155
+ var MAX_ENGRAM_SECTION_BYTES = 8e3;
1156
+ function buildEngramSection(facts) {
1157
+ const lines = [];
1158
+ lines.push("## engram \u2014 structural facts");
1159
+ lines.push("");
1160
+ lines.push(
1161
+ `_Auto-maintained by engram. Do not edit inside the marker block \u2014 the next \`engram memory-sync\` overwrites it. This section complements Auto-Dream: Auto-Dream owns prose memory, engram owns the code graph._`
1162
+ );
1163
+ lines.push("");
1164
+ lines.push(`**Project:** ${facts.projectName}`);
1165
+ if (facts.branch) lines.push(`**Branch:** ${facts.branch}`);
1166
+ lines.push(
1167
+ `**Graph:** ${facts.stats.nodes} nodes, ${facts.stats.edges} edges, ${facts.stats.extractedPct}% extracted`
1168
+ );
1169
+ if (facts.lastMined > 0) {
1170
+ lines.push(
1171
+ `**Last mined:** ${new Date(facts.lastMined).toISOString()}`
1172
+ );
1173
+ }
1174
+ lines.push("");
1175
+ if (facts.godNodes.length > 0) {
1176
+ lines.push("### Core entities");
1177
+ for (const g of facts.godNodes.slice(0, 10)) {
1178
+ lines.push(`- \`${g.label}\` [${g.kind}] \u2014 ${g.sourceFile}`);
1179
+ }
1180
+ lines.push("");
1181
+ }
1182
+ if (facts.landmines.length > 0) {
1183
+ lines.push("### Known landmines");
1184
+ for (const m of facts.landmines.slice(0, 5)) {
1185
+ lines.push(`- **${m.sourceFile}** \u2014 ${m.label}`);
1186
+ }
1187
+ lines.push("");
1188
+ }
1189
+ lines.push(
1190
+ '_For the full graph, run `engram query "..."` or `engram gods`._'
1191
+ );
1192
+ return lines.join("\n");
1193
+ }
1194
+ function upsertEngramSection(existingContent, engramSection) {
1195
+ const block = `${ENGRAM_MARKER_START}
1196
+ ${engramSection}
1197
+ ${ENGRAM_MARKER_END}`;
1198
+ if (!existingContent) {
1199
+ return `# MEMORY.md
1200
+
1201
+ ${block}
1202
+ `;
1203
+ }
1204
+ const startIdx = existingContent.indexOf(ENGRAM_MARKER_START);
1205
+ const endIdx = existingContent.indexOf(ENGRAM_MARKER_END);
1206
+ if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {
1207
+ const trimmed = existingContent.trimEnd();
1208
+ return `${trimmed}
1209
+
1210
+ ${block}
1211
+ `;
1212
+ }
1213
+ const before = existingContent.slice(0, startIdx);
1214
+ const after = existingContent.slice(endIdx + ENGRAM_MARKER_END.length);
1215
+ return `${before}${block}${after}`;
1216
+ }
1217
+ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
1218
+ if (!projectRoot || typeof projectRoot !== "string") return false;
1219
+ if (engramSection.length > MAX_ENGRAM_SECTION_BYTES) {
1220
+ return false;
1221
+ }
1222
+ const memoryPath = join5(projectRoot, "MEMORY.md");
1223
+ try {
1224
+ let existing = "";
1225
+ if (existsSync5(memoryPath)) {
1226
+ const st = statSync3(memoryPath);
1227
+ if (st.size > MAX_MEMORY_FILE_BYTES) {
1228
+ return false;
1229
+ }
1230
+ existing = readFileSync3(memoryPath, "utf-8");
1231
+ }
1232
+ const updated = upsertEngramSection(existing, engramSection);
1233
+ const tmpPath = memoryPath + ".engram-tmp";
1234
+ writeFileSync(tmpPath, updated);
1235
+ renameSync2(tmpPath, memoryPath);
1236
+ return true;
1237
+ } catch {
1238
+ return false;
1239
+ }
1240
+ }
1241
+
1083
1242
  // src/cli.ts
1243
+ import { basename as basename2 } from "path";
1084
1244
  var program = new Command();
1085
1245
  program.name("engram").description(
1086
1246
  "Context as infra for AI coding tools \u2014 hook-based Read/Edit interception + structural graph summaries"
@@ -1242,11 +1402,11 @@ function resolveSettingsPath(scope, projectPath) {
1242
1402
  const absProject = pathResolve(projectPath);
1243
1403
  switch (scope) {
1244
1404
  case "local":
1245
- return join5(absProject, ".claude", "settings.local.json");
1405
+ return join6(absProject, ".claude", "settings.local.json");
1246
1406
  case "project":
1247
- return join5(absProject, ".claude", "settings.json");
1407
+ return join6(absProject, ".claude", "settings.json");
1248
1408
  case "user":
1249
- return join5(homedir(), ".claude", "settings.json");
1409
+ return join6(homedir(), ".claude", "settings.json");
1250
1410
  default:
1251
1411
  return null;
1252
1412
  }
@@ -1284,6 +1444,45 @@ program.command("intercept").description(
1284
1444
  }
1285
1445
  process.exit(0);
1286
1446
  });
1447
+ program.command("cursor-intercept").description(
1448
+ "Cursor beforeReadFile hook entry point (experimental). Reads JSON from stdin, writes Cursor-shaped response JSON to stdout."
1449
+ ).action(async () => {
1450
+ const ALLOW_JSON = '{"permission":"allow"}';
1451
+ const stdinTimeout = setTimeout(() => {
1452
+ process.stdout.write(ALLOW_JSON);
1453
+ process.exit(0);
1454
+ }, 3e3);
1455
+ let input = "";
1456
+ try {
1457
+ for await (const chunk of process.stdin) {
1458
+ input += chunk;
1459
+ if (input.length > 1e6) break;
1460
+ }
1461
+ } catch {
1462
+ clearTimeout(stdinTimeout);
1463
+ process.stdout.write(ALLOW_JSON);
1464
+ process.exit(0);
1465
+ }
1466
+ clearTimeout(stdinTimeout);
1467
+ if (!input.trim()) {
1468
+ process.stdout.write(ALLOW_JSON);
1469
+ process.exit(0);
1470
+ }
1471
+ let payload;
1472
+ try {
1473
+ payload = JSON.parse(input);
1474
+ } catch {
1475
+ process.stdout.write(ALLOW_JSON);
1476
+ process.exit(0);
1477
+ }
1478
+ try {
1479
+ const result = await handleCursorBeforeReadFile(payload);
1480
+ process.stdout.write(JSON.stringify(result));
1481
+ } catch {
1482
+ process.stdout.write(ALLOW_JSON);
1483
+ }
1484
+ process.exit(0);
1485
+ });
1287
1486
  program.command("install-hook").description("Install engram hook entries into Claude Code settings").option("--scope <scope>", "local | project | user", "local").option("--dry-run", "Show diff without writing", false).option("-p, --project <path>", "Project directory", ".").action(
1288
1487
  async (opts) => {
1289
1488
  const settingsPath = resolveSettingsPath(opts.scope, opts.project);
@@ -1296,9 +1495,9 @@ program.command("install-hook").description("Install engram hook entries into Cl
1296
1495
  process.exit(1);
1297
1496
  }
1298
1497
  let existing = {};
1299
- if (existsSync5(settingsPath)) {
1498
+ if (existsSync6(settingsPath)) {
1300
1499
  try {
1301
- const raw = readFileSync3(settingsPath, "utf-8");
1500
+ const raw = readFileSync4(settingsPath, "utf-8");
1302
1501
  existing = raw.trim() ? JSON.parse(raw) : {};
1303
1502
  } catch (err) {
1304
1503
  console.error(
@@ -1344,17 +1543,17 @@ program.command("install-hook").description("Install engram hook entries into Cl
1344
1543
  }
1345
1544
  try {
1346
1545
  mkdirSync(dirname3(settingsPath), { recursive: true });
1347
- if (existsSync5(settingsPath)) {
1546
+ if (existsSync6(settingsPath)) {
1348
1547
  const backupPath = `${settingsPath}.engram-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
1349
1548
  copyFileSync(settingsPath, backupPath);
1350
1549
  console.log(chalk.dim(` Backup: ${backupPath}`));
1351
1550
  }
1352
1551
  const tmpPath = settingsPath + ".engram-tmp";
1353
- writeFileSync(
1552
+ writeFileSync2(
1354
1553
  tmpPath,
1355
1554
  JSON.stringify(result.updated, null, 2) + "\n"
1356
1555
  );
1357
- renameSync2(tmpPath, settingsPath);
1556
+ renameSync3(tmpPath, settingsPath);
1358
1557
  } catch (err) {
1359
1558
  console.error(
1360
1559
  chalk.red(`
@@ -1388,7 +1587,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
1388
1587
  console.error(chalk.red(`Unknown scope: ${opts.scope}`));
1389
1588
  process.exit(1);
1390
1589
  }
1391
- if (!existsSync5(settingsPath)) {
1590
+ if (!existsSync6(settingsPath)) {
1392
1591
  console.log(
1393
1592
  chalk.yellow(`No settings file at ${settingsPath} \u2014 nothing to remove.`)
1394
1593
  );
@@ -1396,7 +1595,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
1396
1595
  }
1397
1596
  let existing;
1398
1597
  try {
1399
- const raw = readFileSync3(settingsPath, "utf-8");
1598
+ const raw = readFileSync4(settingsPath, "utf-8");
1400
1599
  existing = raw.trim() ? JSON.parse(raw) : {};
1401
1600
  } catch (err) {
1402
1601
  console.error(
@@ -1416,8 +1615,8 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
1416
1615
  const backupPath = `${settingsPath}.engram-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
1417
1616
  copyFileSync(settingsPath, backupPath);
1418
1617
  const tmpPath = settingsPath + ".engram-tmp";
1419
- writeFileSync(tmpPath, JSON.stringify(result.updated, null, 2) + "\n");
1420
- renameSync2(tmpPath, settingsPath);
1618
+ writeFileSync2(tmpPath, JSON.stringify(result.updated, null, 2) + "\n");
1619
+ renameSync3(tmpPath, settingsPath);
1421
1620
  console.log(
1422
1621
  chalk.green(
1423
1622
  `
@@ -1504,9 +1703,9 @@ program.command("hook-disable").description("Disable engram hooks via kill switc
1504
1703
  console.error(chalk.dim("Run 'engram init' first."));
1505
1704
  process.exit(1);
1506
1705
  }
1507
- const flagPath = join5(projectRoot, ".engram", "hook-disabled");
1706
+ const flagPath = join6(projectRoot, ".engram", "hook-disabled");
1508
1707
  try {
1509
- writeFileSync(flagPath, (/* @__PURE__ */ new Date()).toISOString());
1708
+ writeFileSync2(flagPath, (/* @__PURE__ */ new Date()).toISOString());
1510
1709
  console.log(
1511
1710
  chalk.green(`\u2705 engram hooks disabled for ${projectRoot}`)
1512
1711
  );
@@ -1528,8 +1727,8 @@ program.command("hook-enable").description("Re-enable engram hooks (remove kill
1528
1727
  console.error(chalk.red(`Not an engram project: ${absProject}`));
1529
1728
  process.exit(1);
1530
1729
  }
1531
- const flagPath = join5(projectRoot, ".engram", "hook-disabled");
1532
- if (!existsSync5(flagPath)) {
1730
+ const flagPath = join6(projectRoot, ".engram", "hook-disabled");
1731
+ if (!existsSync6(flagPath)) {
1533
1732
  console.log(
1534
1733
  chalk.yellow(`engram hooks already enabled for ${projectRoot}`)
1535
1734
  );
@@ -1547,4 +1746,90 @@ program.command("hook-enable").description("Re-enable engram hooks (remove kill
1547
1746
  process.exit(1);
1548
1747
  }
1549
1748
  });
1749
+ program.command("memory-sync").description(
1750
+ "Write engram's structural facts into MEMORY.md (complementary to Anthropic Auto-Dream)"
1751
+ ).option("-p, --project <path>", "Project directory", ".").option("--dry-run", "Print what would be written without writing", false).action(
1752
+ async (opts) => {
1753
+ const absProject = pathResolve(opts.project);
1754
+ const projectRoot = findProjectRoot(absProject);
1755
+ if (!projectRoot) {
1756
+ console.error(
1757
+ chalk.red(`Not an engram project: ${absProject}`)
1758
+ );
1759
+ console.error(chalk.dim("Run 'engram init' first."));
1760
+ process.exit(1);
1761
+ }
1762
+ const [gods, mistakeList, graphStats] = await Promise.all([
1763
+ godNodes(projectRoot, 10).catch(() => []),
1764
+ mistakes(projectRoot, { limit: 5 }).catch(() => []),
1765
+ stats(projectRoot).catch(() => null)
1766
+ ]);
1767
+ if (!graphStats) {
1768
+ console.error(chalk.red("Failed to read graph stats."));
1769
+ process.exit(1);
1770
+ }
1771
+ let branch = null;
1772
+ try {
1773
+ const headPath = join6(projectRoot, ".git", "HEAD");
1774
+ if (existsSync6(headPath)) {
1775
+ const content = readFileSync4(headPath, "utf-8").trim();
1776
+ const m = content.match(/^ref:\s+refs\/heads\/(.+)$/);
1777
+ if (m) branch = m[1];
1778
+ }
1779
+ } catch {
1780
+ }
1781
+ const section = buildEngramSection({
1782
+ projectName: basename2(projectRoot),
1783
+ branch,
1784
+ stats: {
1785
+ nodes: graphStats.nodes,
1786
+ edges: graphStats.edges,
1787
+ extractedPct: graphStats.extractedPct
1788
+ },
1789
+ godNodes: gods,
1790
+ landmines: mistakeList.map((m) => ({
1791
+ label: m.label,
1792
+ sourceFile: m.sourceFile
1793
+ })),
1794
+ lastMined: graphStats.lastMined
1795
+ });
1796
+ console.log(
1797
+ chalk.bold(`
1798
+ \u{1F4DD} engram memory-sync`)
1799
+ );
1800
+ console.log(
1801
+ chalk.dim(` Target: ${join6(projectRoot, "MEMORY.md")}`)
1802
+ );
1803
+ if (opts.dryRun) {
1804
+ console.log(chalk.cyan("\n Section to write (dry-run):\n"));
1805
+ console.log(
1806
+ section.split("\n").map((l) => " " + l).join("\n")
1807
+ );
1808
+ console.log(chalk.dim("\n (dry-run \u2014 no changes written)"));
1809
+ return;
1810
+ }
1811
+ const ok = writeEngramSectionToMemoryMd(projectRoot, section);
1812
+ if (!ok) {
1813
+ console.error(
1814
+ chalk.red(
1815
+ "\n \u274C Write failed. MEMORY.md may be too large, or the engram section exceeded its size cap."
1816
+ )
1817
+ );
1818
+ process.exit(1);
1819
+ }
1820
+ console.log(
1821
+ chalk.green(
1822
+ `
1823
+ \u2705 Synced ${gods.length} god nodes${mistakeList.length > 0 ? ` and ${mistakeList.length} landmines` : ""} to MEMORY.md`
1824
+ )
1825
+ );
1826
+ console.log(
1827
+ chalk.dim(
1828
+ `
1829
+ Next: Anthropic's Auto-Dream will consolidate this alongside its prose entries.
1830
+ `
1831
+ )
1832
+ );
1833
+ }
1834
+ );
1550
1835
  program.parse();
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  benchmark,
3
+ computeKeywordIDF,
3
4
  getDbPath,
4
5
  getFileContext,
5
6
  getStore,
@@ -10,9 +11,10 @@ import {
10
11
  path,
11
12
  query,
12
13
  stats
13
- } from "./chunk-NYFDM4FR.js";
14
+ } from "./chunk-RGDHLGWQ.js";
14
15
  export {
15
16
  benchmark,
17
+ computeKeywordIDF,
16
18
  getDbPath,
17
19
  getFileContext,
18
20
  getStore,
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  generateSummary,
5
5
  install,
6
6
  uninstall
7
- } from "./chunk-7KYR4SPZ.js";
7
+ } from "./chunk-IYO4HETA.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-NYFDM4FR.js";
26
+ } from "./chunk-RGDHLGWQ.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-NYFDM4FR.js";
11
+ } from "./chunk-RGDHLGWQ.js";
12
12
 
13
13
  // src/serve.ts
14
14
  function clampInt(value, defaultValue, min, max) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "engramx",
3
- "version": "0.3.0",
4
- "description": "Context as infra for AI coding tools a Claude Code hook layer that intercepts Read/Edit/Grep and replaces file reads with structural graph summaries. Persistent, local, zero LLM cost.",
3
+ "version": "0.3.1",
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": {
7
7
  "engram": "dist/cli.js",
@@ -17,16 +17,20 @@
17
17
  "prepublishOnly": "npm run build"
18
18
  },
19
19
  "keywords": [
20
- "ai",
21
- "coding",
22
- "memory",
23
- "context",
24
- "tree-sitter",
20
+ "structural-code-graph",
21
+ "claude-code-hooks",
22
+ "ai-coding",
23
+ "context-engineering",
25
24
  "knowledge-graph",
25
+ "ast",
26
26
  "mcp",
27
- "claude",
27
+ "claude-code",
28
28
  "cursor",
29
- "token-savings"
29
+ "token-reduction",
30
+ "context-rot",
31
+ "local-first",
32
+ "agent-memory",
33
+ "hook-interception"
30
34
  ],
31
35
  "author": "Nicholas Ashkar",
32
36
  "license": "Apache-2.0",