engramx 0.3.1 → 0.3.2

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-439%20passing-brightgreen" alt="Tests">
18
+ <img src="https://img.shields.io/badge/tests-467%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">
@@ -110,6 +110,37 @@ engram hook-enable # re-enable
110
110
  engram uninstall-hook # surgical removal, preserves other hooks
111
111
  ```
112
112
 
113
+ ## Experience Tiers
114
+
115
+ Each tier builds on the previous. You can stop at any level — each one works standalone.
116
+
117
+ | Tier | What you run | What you get | Token savings |
118
+ |---|---|---|---|
119
+ | **1. Graph only** | `engram init` | CLI queries, MCP server, `engram gen` for CLAUDE.md | ~6x per query vs reading files |
120
+ | **2. + Sentinel hooks** | `engram install-hook` | Automatic Read interception, Edit landmine warnings, session-start briefs, prompt pre-query | ~82% per session (measured) |
121
+ | **3. + Skills index** | `engram init --with-skills` | Graph includes your `~/.claude/skills/` — queries surface relevant skills alongside code | ~23% overhead on graph size |
122
+ | **4. + Git hooks** | `engram hooks install` | Auto-rebuild graph on every `git commit` — graph never goes stale | Zero token cost |
123
+
124
+ **Recommended full setup** (one-time, per project):
125
+
126
+ ```bash
127
+ npm install -g engramx # install globally
128
+ cd ~/my-project
129
+ engram init --with-skills # build graph + index skills
130
+ engram install-hook # wire Sentinel into Claude Code
131
+ engram hooks install # auto-rebuild on commit
132
+ ```
133
+
134
+ After this, every Claude Code session in the project automatically gets structural context, landmine warnings, and session briefs — with no manual queries needed.
135
+
136
+ **Optional — MEMORY.md integration** (v0.3.1+):
137
+
138
+ ```bash
139
+ engram gen --memory-md # write structural facts into Claude's native MEMORY.md
140
+ ```
141
+
142
+ This writes a marker-bounded block into `~/.claude/projects/.../memory/MEMORY.md` with your project's core entities and structure. Claude's Auto-Dream owns the prose; engram owns the structure. They complement each other — engram never touches content outside its markers.
143
+
113
144
  ## All Commands
114
145
 
115
146
  ### Core (v0.1/v0.2 — unchanged)
@@ -627,6 +627,12 @@ function renderFileStructure(store, relativeFilePath, tokenBudget = 600) {
627
627
  };
628
628
  }
629
629
 
630
+ // src/graph/path-utils.ts
631
+ function toPosixPath(p) {
632
+ if (!p) return p;
633
+ return p.replace(/\\/g, "/");
634
+ }
635
+
630
636
  // src/miners/ast-miner.ts
631
637
  import { readFileSync as readFileSync2, readdirSync, realpathSync } from "fs";
632
638
  import { basename, extname, join, relative } from "path";
@@ -716,7 +722,7 @@ function extractFile(filePath, rootDir) {
716
722
  if (!lang) return { nodes: [], edges: [] };
717
723
  const content = readFileSync2(filePath, "utf-8");
718
724
  const lines = content.split("\n");
719
- const relPath = relative(rootDir, filePath);
725
+ const relPath = toPosixPath(relative(rootDir, filePath));
720
726
  const stem = basename(filePath, ext);
721
727
  const now = Date.now();
722
728
  const nodes = [];
@@ -1230,7 +1236,7 @@ function parseFrontmatter(content) {
1230
1236
  }
1231
1237
  function parseYaml(block) {
1232
1238
  const data = {};
1233
- const lines = block.split("\n");
1239
+ const lines = block.replace(/\r/g, "").split("\n");
1234
1240
  let i = 0;
1235
1241
  while (i < lines.length) {
1236
1242
  const line = lines[i];
@@ -1606,7 +1612,7 @@ async function getFileContext(projectRoot, absFilePath) {
1606
1612
  try {
1607
1613
  const root = resolve2(projectRoot);
1608
1614
  const abs = resolve2(absFilePath);
1609
- const relPath = relative2(root, abs);
1615
+ const relPath = toPosixPath(relative2(root, abs));
1610
1616
  if (relPath.startsWith("..") || relPath === "") {
1611
1617
  return empty;
1612
1618
  }
@@ -1801,6 +1807,7 @@ export {
1801
1807
  MAX_MISTAKE_LABEL_CHARS,
1802
1808
  queryGraph,
1803
1809
  shortestPath,
1810
+ toPosixPath,
1804
1811
  SUPPORTED_EXTENSIONS,
1805
1812
  extractFile,
1806
1813
  extractDirectory,
@@ -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-2TWPNHRQ.js");
313
+ const { getStore } = await import("./core-AJD3SS6U.js");
314
314
  const store = await getStore(projectRoot);
315
315
  try {
316
316
  let view = VIEWS.general;
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  install,
5
5
  status,
6
6
  uninstall
7
- } from "./chunk-IYO4HETA.js";
7
+ } from "./chunk-JXJNXQUM.js";
8
8
  import {
9
9
  benchmark,
10
10
  computeKeywordIDF,
@@ -15,8 +15,9 @@ import {
15
15
  mistakes,
16
16
  path,
17
17
  query,
18
- stats
19
- } from "./chunk-RGDHLGWQ.js";
18
+ stats,
19
+ toPosixPath
20
+ } from "./chunk-3NUHMLRV.js";
20
21
 
21
22
  // src/cli.ts
22
23
  import { Command } from "commander";
@@ -104,6 +105,10 @@ function isHardSystemPath(absPath) {
104
105
  const p = absPath.replaceAll(sep, "/");
105
106
  if (p === "/" || p.startsWith("/dev/") || p.startsWith("/proc/")) return true;
106
107
  if (p.startsWith("/sys/")) return true;
108
+ const upper = p.toUpperCase();
109
+ if (upper.startsWith("//./") || upper.startsWith("//?/")) return true;
110
+ if (/^[A-Z]:\/WINDOWS(\/|$)/.test(upper)) return true;
111
+ if (/^[A-Z]:\/(PROGRAM FILES|PROGRAMDATA)(\/|$)/.test(upper)) return true;
107
112
  return false;
108
113
  }
109
114
  var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
@@ -257,6 +262,9 @@ function isValidCwd(cwd) {
257
262
  }
258
263
  function resolveInterceptContext(filePath, cwd) {
259
264
  if (!filePath) return { proceed: false, reason: "empty-path" };
265
+ if (isHardSystemPath(filePath)) {
266
+ return { proceed: false, reason: "system-path" };
267
+ }
260
268
  const absPath = normalizePath(filePath, cwd);
261
269
  if (!absPath) return { proceed: false, reason: "normalize-failed" };
262
270
  if (isHardSystemPath(absPath)) {
@@ -371,7 +379,9 @@ async function handleEditOrWrite(payload) {
371
379
  if (!ctx.proceed) return PASSTHROUGH;
372
380
  if (isContentUnsafeForIntercept(ctx.absPath)) return PASSTHROUGH;
373
381
  if (isHookDisabled(ctx.projectRoot)) return PASSTHROUGH;
374
- const relPath = relative(resolvePath(ctx.projectRoot), ctx.absPath);
382
+ const relPath = toPosixPath(
383
+ relative(resolvePath(ctx.projectRoot), ctx.absPath)
384
+ );
375
385
  if (!relPath || relPath.startsWith("..")) return PASSTHROUGH;
376
386
  let found;
377
387
  try {
@@ -1276,6 +1286,22 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
1276
1286
  }
1277
1287
  console.log(chalk.green("\n\u2705 Ready. Your AI now has persistent memory."));
1278
1288
  console.log(chalk.dim(" Graph stored in .engram/graph.db"));
1289
+ 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");
1293
+ if (!hasHooks) {
1294
+ console.log(
1295
+ chalk.yellow("\n\u{1F4A1} Next step: ") + chalk.white("engram install-hook") + chalk.dim(
1296
+ " \u2014 enables automatic Read interception (82% token savings)"
1297
+ )
1298
+ );
1299
+ console.log(
1300
+ chalk.dim(
1301
+ " Also recommended: " + chalk.white("engram hooks install") + " \u2014 auto-rebuild graph on git commit"
1302
+ )
1303
+ );
1304
+ }
1279
1305
  });
1280
1306
  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) => {
1281
1307
  const result = await query(opts.project, question, {
@@ -1417,23 +1443,28 @@ program.command("intercept").description(
1417
1443
  const stdinTimeout = setTimeout(() => {
1418
1444
  process.exit(0);
1419
1445
  }, 3e3);
1446
+ stdinTimeout.unref();
1420
1447
  let input = "";
1448
+ let stdinFailed = false;
1421
1449
  try {
1422
1450
  for await (const chunk of process.stdin) {
1423
1451
  input += chunk;
1424
1452
  if (input.length > 1e6) break;
1425
1453
  }
1426
1454
  } catch {
1427
- clearTimeout(stdinTimeout);
1428
- process.exit(0);
1455
+ stdinFailed = true;
1429
1456
  }
1430
1457
  clearTimeout(stdinTimeout);
1431
- if (!input.trim()) process.exit(0);
1458
+ if (stdinFailed || !input.trim()) {
1459
+ process.exitCode = 0;
1460
+ return;
1461
+ }
1432
1462
  let payload;
1433
1463
  try {
1434
1464
  payload = JSON.parse(input);
1435
1465
  } catch {
1436
- process.exit(0);
1466
+ process.exitCode = 0;
1467
+ return;
1437
1468
  }
1438
1469
  try {
1439
1470
  const result = await dispatchHook(payload);
@@ -1442,7 +1473,7 @@ program.command("intercept").description(
1442
1473
  }
1443
1474
  } catch {
1444
1475
  }
1445
- process.exit(0);
1476
+ process.exitCode = 0;
1446
1477
  });
1447
1478
  program.command("cursor-intercept").description(
1448
1479
  "Cursor beforeReadFile hook entry point (experimental). Reads JSON from stdin, writes Cursor-shaped response JSON to stdout."
@@ -11,7 +11,7 @@ import {
11
11
  path,
12
12
  query,
13
13
  stats
14
- } from "./chunk-RGDHLGWQ.js";
14
+ } from "./chunk-3NUHMLRV.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-IYO4HETA.js";
7
+ } from "./chunk-JXJNXQUM.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-RGDHLGWQ.js";
26
+ } from "./chunk-3NUHMLRV.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-RGDHLGWQ.js";
11
+ } from "./chunk-3NUHMLRV.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.1",
3
+ "version": "0.3.2",
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": {