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 +32 -1
- package/dist/{chunk-RGDHLGWQ.js → chunk-3NUHMLRV.js} +10 -3
- package/dist/{chunk-IYO4HETA.js → chunk-JXJNXQUM.js} +1 -1
- package/dist/cli.js +40 -9
- package/dist/{core-2TWPNHRQ.js → core-AJD3SS6U.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/serve.js +1 -1
- package/package.json +1 -1
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1428
|
-
process.exit(0);
|
|
1455
|
+
stdinFailed = true;
|
|
1429
1456
|
}
|
|
1430
1457
|
clearTimeout(stdinTimeout);
|
|
1431
|
-
if (!input.trim())
|
|
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.
|
|
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.
|
|
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."
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
generateSummary,
|
|
5
5
|
install,
|
|
6
6
|
uninstall
|
|
7
|
-
} from "./chunk-
|
|
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-
|
|
26
|
+
} from "./chunk-3NUHMLRV.js";
|
|
27
27
|
export {
|
|
28
28
|
GraphStore,
|
|
29
29
|
SUPPORTED_EXTENSIONS,
|
package/dist/serve.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engramx",
|
|
3
|
-
"version": "0.3.
|
|
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": {
|