engramx 0.3.0 → 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 +59 -5
- package/dist/{chunk-NYFDM4FR.js → chunk-3NUHMLRV.js} +46 -3
- package/dist/{chunk-7KYR4SPZ.js → chunk-JXJNXQUM.js} +1 -1
- package/dist/cli.js +347 -31
- package/dist/{core-MPNNCPFW.js → core-AJD3SS6U.js} +3 -1
- package/dist/index.js +2 -2
- package/dist/serve.js +1 -1
- package/package.json +13 -9
package/README.md
CHANGED
|
@@ -15,20 +15,36 @@
|
|
|
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
|
+
<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
|
-
|
|
26
|
+
# The structural code graph your AI agent can't forget to use.
|
|
26
27
|
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
@@ -87,6 +110,37 @@ engram hook-enable # re-enable
|
|
|
87
110
|
engram uninstall-hook # surgical removal, preserves other hooks
|
|
88
111
|
```
|
|
89
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
|
+
|
|
90
144
|
## All Commands
|
|
91
145
|
|
|
92
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
|
}
|
|
@@ -1660,6 +1666,41 @@ async function getFileContext(projectRoot, absFilePath) {
|
|
|
1660
1666
|
return empty;
|
|
1661
1667
|
}
|
|
1662
1668
|
}
|
|
1669
|
+
async function computeKeywordIDF(projectRoot, keywords) {
|
|
1670
|
+
if (keywords.length === 0) return [];
|
|
1671
|
+
try {
|
|
1672
|
+
const root = resolve2(projectRoot);
|
|
1673
|
+
const dbPath = getDbPath(root);
|
|
1674
|
+
if (!existsSync5(dbPath)) return [];
|
|
1675
|
+
const store = await getStore(root);
|
|
1676
|
+
try {
|
|
1677
|
+
const allNodes = store.getAllNodes();
|
|
1678
|
+
const total = allNodes.length;
|
|
1679
|
+
if (total === 0) return [];
|
|
1680
|
+
const labels = allNodes.map((n) => n.label.toLowerCase());
|
|
1681
|
+
const results = [];
|
|
1682
|
+
for (const kw of keywords) {
|
|
1683
|
+
const kwLower = kw.toLowerCase();
|
|
1684
|
+
let df = 0;
|
|
1685
|
+
for (const label of labels) {
|
|
1686
|
+
if (label.includes(kwLower)) df += 1;
|
|
1687
|
+
}
|
|
1688
|
+
const idf = df === 0 ? 0 : Math.log(total / df);
|
|
1689
|
+
results.push({
|
|
1690
|
+
keyword: kw,
|
|
1691
|
+
documentFrequency: df,
|
|
1692
|
+
idf
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
results.sort((a, b) => b.idf - a.idf);
|
|
1696
|
+
return results;
|
|
1697
|
+
} finally {
|
|
1698
|
+
store.close();
|
|
1699
|
+
}
|
|
1700
|
+
} catch {
|
|
1701
|
+
return [];
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1663
1704
|
async function learn(projectRoot, text, sourceLabel = "manual") {
|
|
1664
1705
|
const { nodes, edges } = learnFromSession(text, sourceLabel);
|
|
1665
1706
|
if (nodes.length === 0 && edges.length === 0) return { nodesAdded: 0 };
|
|
@@ -1766,6 +1807,7 @@ export {
|
|
|
1766
1807
|
MAX_MISTAKE_LABEL_CHARS,
|
|
1767
1808
|
queryGraph,
|
|
1768
1809
|
shortestPath,
|
|
1810
|
+
toPosixPath,
|
|
1769
1811
|
SUPPORTED_EXTENSIONS,
|
|
1770
1812
|
extractFile,
|
|
1771
1813
|
extractDirectory,
|
|
@@ -1778,6 +1820,7 @@ export {
|
|
|
1778
1820
|
godNodes,
|
|
1779
1821
|
stats,
|
|
1780
1822
|
getFileContext,
|
|
1823
|
+
computeKeywordIDF,
|
|
1781
1824
|
learn,
|
|
1782
1825
|
mistakes,
|
|
1783
1826
|
benchmark
|
|
@@ -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,9 +4,10 @@ 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
|
+
computeKeywordIDF,
|
|
10
11
|
getFileContext,
|
|
11
12
|
godNodes,
|
|
12
13
|
init,
|
|
@@ -14,22 +15,23 @@ import {
|
|
|
14
15
|
mistakes,
|
|
15
16
|
path,
|
|
16
17
|
query,
|
|
17
|
-
stats
|
|
18
|
-
|
|
18
|
+
stats,
|
|
19
|
+
toPosixPath
|
|
20
|
+
} from "./chunk-3NUHMLRV.js";
|
|
19
21
|
|
|
20
22
|
// src/cli.ts
|
|
21
23
|
import { Command } from "commander";
|
|
22
24
|
import chalk from "chalk";
|
|
23
25
|
import {
|
|
24
|
-
existsSync as
|
|
25
|
-
readFileSync as
|
|
26
|
-
writeFileSync,
|
|
26
|
+
existsSync as existsSync6,
|
|
27
|
+
readFileSync as readFileSync4,
|
|
28
|
+
writeFileSync as writeFileSync2,
|
|
27
29
|
mkdirSync,
|
|
28
30
|
unlinkSync,
|
|
29
31
|
copyFileSync,
|
|
30
|
-
renameSync as
|
|
32
|
+
renameSync as renameSync3
|
|
31
33
|
} from "fs";
|
|
32
|
-
import { dirname as dirname3, join as
|
|
34
|
+
import { dirname as dirname3, join as join6, resolve as pathResolve } from "path";
|
|
33
35
|
import { homedir } from "os";
|
|
34
36
|
|
|
35
37
|
// src/intercept/safety.ts
|
|
@@ -103,6 +105,10 @@ function isHardSystemPath(absPath) {
|
|
|
103
105
|
const p = absPath.replaceAll(sep, "/");
|
|
104
106
|
if (p === "/" || p.startsWith("/dev/") || p.startsWith("/proc/")) return true;
|
|
105
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;
|
|
106
112
|
return false;
|
|
107
113
|
}
|
|
108
114
|
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
@@ -256,6 +262,9 @@ function isValidCwd(cwd) {
|
|
|
256
262
|
}
|
|
257
263
|
function resolveInterceptContext(filePath, cwd) {
|
|
258
264
|
if (!filePath) return { proceed: false, reason: "empty-path" };
|
|
265
|
+
if (isHardSystemPath(filePath)) {
|
|
266
|
+
return { proceed: false, reason: "system-path" };
|
|
267
|
+
}
|
|
259
268
|
const absPath = normalizePath(filePath, cwd);
|
|
260
269
|
if (!absPath) return { proceed: false, reason: "normalize-failed" };
|
|
261
270
|
if (isHardSystemPath(absPath)) {
|
|
@@ -370,7 +379,9 @@ async function handleEditOrWrite(payload) {
|
|
|
370
379
|
if (!ctx.proceed) return PASSTHROUGH;
|
|
371
380
|
if (isContentUnsafeForIntercept(ctx.absPath)) return PASSTHROUGH;
|
|
372
381
|
if (isHookDisabled(ctx.projectRoot)) return PASSTHROUGH;
|
|
373
|
-
const relPath =
|
|
382
|
+
const relPath = toPosixPath(
|
|
383
|
+
relative(resolvePath(ctx.projectRoot), ctx.absPath)
|
|
384
|
+
);
|
|
374
385
|
if (!relPath || relPath.startsWith("..")) return PASSTHROUGH;
|
|
375
386
|
let found;
|
|
376
387
|
try {
|
|
@@ -548,6 +559,8 @@ async function handleSessionStart(payload) {
|
|
|
548
559
|
var MIN_SIGNIFICANT_TERMS = 2;
|
|
549
560
|
var MIN_MATCHED_NODES = 3;
|
|
550
561
|
var PROMPT_INJECTION_TOKEN_BUDGET = 500;
|
|
562
|
+
var MIN_IDF_THRESHOLD = 1.386;
|
|
563
|
+
var MAX_SEED_KEYWORDS = 5;
|
|
551
564
|
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
552
565
|
"a",
|
|
553
566
|
"an",
|
|
@@ -642,13 +655,32 @@ async function handleUserPromptSubmit(payload) {
|
|
|
642
655
|
const prompt = payload.prompt;
|
|
643
656
|
if (!prompt || typeof prompt !== "string") return PASSTHROUGH;
|
|
644
657
|
if (prompt.length > 8e3) return PASSTHROUGH;
|
|
645
|
-
const
|
|
646
|
-
if (
|
|
658
|
+
const rawKeywords = extractKeywords(prompt);
|
|
659
|
+
if (rawKeywords.length < MIN_SIGNIFICANT_TERMS) return PASSTHROUGH;
|
|
647
660
|
const cwd = payload.cwd;
|
|
648
661
|
if (!isValidCwd(cwd)) return PASSTHROUGH;
|
|
649
662
|
const projectRoot = findProjectRoot(cwd);
|
|
650
663
|
if (projectRoot === null) return PASSTHROUGH;
|
|
651
664
|
if (isHookDisabled(projectRoot)) return PASSTHROUGH;
|
|
665
|
+
let keywords;
|
|
666
|
+
try {
|
|
667
|
+
const scored = await computeKeywordIDF(projectRoot, rawKeywords);
|
|
668
|
+
if (scored.length === 0) {
|
|
669
|
+
keywords = rawKeywords;
|
|
670
|
+
} else {
|
|
671
|
+
const discriminative = scored.filter((s) => s.idf >= MIN_IDF_THRESHOLD);
|
|
672
|
+
if (discriminative.length === 0) {
|
|
673
|
+
return PASSTHROUGH;
|
|
674
|
+
}
|
|
675
|
+
keywords = scored.filter((s) => s.idf > 0).slice(0, MAX_SEED_KEYWORDS).map((s) => s.keyword);
|
|
676
|
+
if (keywords.length === 0) {
|
|
677
|
+
keywords = rawKeywords;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
} catch {
|
|
681
|
+
keywords = rawKeywords;
|
|
682
|
+
}
|
|
683
|
+
if (keywords.length === 0) return PASSTHROUGH;
|
|
652
684
|
let result;
|
|
653
685
|
try {
|
|
654
686
|
result = await query(projectRoot, keywords.join(" "), {
|
|
@@ -866,6 +898,44 @@ function extractPreToolDecision(result) {
|
|
|
866
898
|
return "passthrough";
|
|
867
899
|
}
|
|
868
900
|
|
|
901
|
+
// src/intercept/cursor-adapter.ts
|
|
902
|
+
var ALLOW = { permission: "allow" };
|
|
903
|
+
function toClaudeReadPayload(cursorPayload) {
|
|
904
|
+
const filePath = cursorPayload.file_path;
|
|
905
|
+
if (!filePath || typeof filePath !== "string") return null;
|
|
906
|
+
const workspaceRoot = Array.isArray(cursorPayload.workspace_roots) && cursorPayload.workspace_roots.length > 0 ? cursorPayload.workspace_roots[0] : process.cwd();
|
|
907
|
+
return {
|
|
908
|
+
tool_name: "Read",
|
|
909
|
+
tool_input: { file_path: filePath },
|
|
910
|
+
cwd: workspaceRoot
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
function extractSummaryFromClaudeResult(result) {
|
|
914
|
+
if (result === PASSTHROUGH || result === null) return null;
|
|
915
|
+
if (typeof result !== "object") return null;
|
|
916
|
+
const hookSpecific = result.hookSpecificOutput;
|
|
917
|
+
if (!hookSpecific || typeof hookSpecific !== "object") return null;
|
|
918
|
+
const reason = hookSpecific.permissionDecisionReason;
|
|
919
|
+
if (typeof reason !== "string" || reason.length === 0) return null;
|
|
920
|
+
return reason;
|
|
921
|
+
}
|
|
922
|
+
async function handleCursorBeforeReadFile(payload) {
|
|
923
|
+
try {
|
|
924
|
+
if (!payload || typeof payload !== "object") return ALLOW;
|
|
925
|
+
const claudePayload = toClaudeReadPayload(payload);
|
|
926
|
+
if (claudePayload === null) return ALLOW;
|
|
927
|
+
const result = await handleRead(claudePayload);
|
|
928
|
+
const summary = extractSummaryFromClaudeResult(result);
|
|
929
|
+
if (summary === null) return ALLOW;
|
|
930
|
+
return {
|
|
931
|
+
permission: "deny",
|
|
932
|
+
user_message: summary
|
|
933
|
+
};
|
|
934
|
+
} catch {
|
|
935
|
+
return ALLOW;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
869
939
|
// src/intercept/installer.ts
|
|
870
940
|
var ENGRAM_HOOK_EVENTS = [
|
|
871
941
|
"PreToolUse",
|
|
@@ -1080,7 +1150,107 @@ function formatStatsSummary(summary) {
|
|
|
1080
1150
|
return lines.join("\n");
|
|
1081
1151
|
}
|
|
1082
1152
|
|
|
1153
|
+
// src/intercept/memory-md.ts
|
|
1154
|
+
import {
|
|
1155
|
+
existsSync as existsSync5,
|
|
1156
|
+
readFileSync as readFileSync3,
|
|
1157
|
+
writeFileSync,
|
|
1158
|
+
renameSync as renameSync2,
|
|
1159
|
+
statSync as statSync3
|
|
1160
|
+
} from "fs";
|
|
1161
|
+
import { join as join5 } from "path";
|
|
1162
|
+
var ENGRAM_MARKER_START = "<!-- engram:structural-facts:start -->";
|
|
1163
|
+
var ENGRAM_MARKER_END = "<!-- engram:structural-facts:end -->";
|
|
1164
|
+
var MAX_MEMORY_FILE_BYTES = 1e6;
|
|
1165
|
+
var MAX_ENGRAM_SECTION_BYTES = 8e3;
|
|
1166
|
+
function buildEngramSection(facts) {
|
|
1167
|
+
const lines = [];
|
|
1168
|
+
lines.push("## engram \u2014 structural facts");
|
|
1169
|
+
lines.push("");
|
|
1170
|
+
lines.push(
|
|
1171
|
+
`_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._`
|
|
1172
|
+
);
|
|
1173
|
+
lines.push("");
|
|
1174
|
+
lines.push(`**Project:** ${facts.projectName}`);
|
|
1175
|
+
if (facts.branch) lines.push(`**Branch:** ${facts.branch}`);
|
|
1176
|
+
lines.push(
|
|
1177
|
+
`**Graph:** ${facts.stats.nodes} nodes, ${facts.stats.edges} edges, ${facts.stats.extractedPct}% extracted`
|
|
1178
|
+
);
|
|
1179
|
+
if (facts.lastMined > 0) {
|
|
1180
|
+
lines.push(
|
|
1181
|
+
`**Last mined:** ${new Date(facts.lastMined).toISOString()}`
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
lines.push("");
|
|
1185
|
+
if (facts.godNodes.length > 0) {
|
|
1186
|
+
lines.push("### Core entities");
|
|
1187
|
+
for (const g of facts.godNodes.slice(0, 10)) {
|
|
1188
|
+
lines.push(`- \`${g.label}\` [${g.kind}] \u2014 ${g.sourceFile}`);
|
|
1189
|
+
}
|
|
1190
|
+
lines.push("");
|
|
1191
|
+
}
|
|
1192
|
+
if (facts.landmines.length > 0) {
|
|
1193
|
+
lines.push("### Known landmines");
|
|
1194
|
+
for (const m of facts.landmines.slice(0, 5)) {
|
|
1195
|
+
lines.push(`- **${m.sourceFile}** \u2014 ${m.label}`);
|
|
1196
|
+
}
|
|
1197
|
+
lines.push("");
|
|
1198
|
+
}
|
|
1199
|
+
lines.push(
|
|
1200
|
+
'_For the full graph, run `engram query "..."` or `engram gods`._'
|
|
1201
|
+
);
|
|
1202
|
+
return lines.join("\n");
|
|
1203
|
+
}
|
|
1204
|
+
function upsertEngramSection(existingContent, engramSection) {
|
|
1205
|
+
const block = `${ENGRAM_MARKER_START}
|
|
1206
|
+
${engramSection}
|
|
1207
|
+
${ENGRAM_MARKER_END}`;
|
|
1208
|
+
if (!existingContent) {
|
|
1209
|
+
return `# MEMORY.md
|
|
1210
|
+
|
|
1211
|
+
${block}
|
|
1212
|
+
`;
|
|
1213
|
+
}
|
|
1214
|
+
const startIdx = existingContent.indexOf(ENGRAM_MARKER_START);
|
|
1215
|
+
const endIdx = existingContent.indexOf(ENGRAM_MARKER_END);
|
|
1216
|
+
if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {
|
|
1217
|
+
const trimmed = existingContent.trimEnd();
|
|
1218
|
+
return `${trimmed}
|
|
1219
|
+
|
|
1220
|
+
${block}
|
|
1221
|
+
`;
|
|
1222
|
+
}
|
|
1223
|
+
const before = existingContent.slice(0, startIdx);
|
|
1224
|
+
const after = existingContent.slice(endIdx + ENGRAM_MARKER_END.length);
|
|
1225
|
+
return `${before}${block}${after}`;
|
|
1226
|
+
}
|
|
1227
|
+
function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
|
|
1228
|
+
if (!projectRoot || typeof projectRoot !== "string") return false;
|
|
1229
|
+
if (engramSection.length > MAX_ENGRAM_SECTION_BYTES) {
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
const memoryPath = join5(projectRoot, "MEMORY.md");
|
|
1233
|
+
try {
|
|
1234
|
+
let existing = "";
|
|
1235
|
+
if (existsSync5(memoryPath)) {
|
|
1236
|
+
const st = statSync3(memoryPath);
|
|
1237
|
+
if (st.size > MAX_MEMORY_FILE_BYTES) {
|
|
1238
|
+
return false;
|
|
1239
|
+
}
|
|
1240
|
+
existing = readFileSync3(memoryPath, "utf-8");
|
|
1241
|
+
}
|
|
1242
|
+
const updated = upsertEngramSection(existing, engramSection);
|
|
1243
|
+
const tmpPath = memoryPath + ".engram-tmp";
|
|
1244
|
+
writeFileSync(tmpPath, updated);
|
|
1245
|
+
renameSync2(tmpPath, memoryPath);
|
|
1246
|
+
return true;
|
|
1247
|
+
} catch {
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1083
1252
|
// src/cli.ts
|
|
1253
|
+
import { basename as basename2 } from "path";
|
|
1084
1254
|
var program = new Command();
|
|
1085
1255
|
program.name("engram").description(
|
|
1086
1256
|
"Context as infra for AI coding tools \u2014 hook-based Read/Edit interception + structural graph summaries"
|
|
@@ -1116,6 +1286,22 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
|
|
|
1116
1286
|
}
|
|
1117
1287
|
console.log(chalk.green("\n\u2705 Ready. Your AI now has persistent memory."));
|
|
1118
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
|
+
}
|
|
1119
1305
|
});
|
|
1120
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) => {
|
|
1121
1307
|
const result = await query(opts.project, question, {
|
|
@@ -1242,11 +1428,11 @@ function resolveSettingsPath(scope, projectPath) {
|
|
|
1242
1428
|
const absProject = pathResolve(projectPath);
|
|
1243
1429
|
switch (scope) {
|
|
1244
1430
|
case "local":
|
|
1245
|
-
return
|
|
1431
|
+
return join6(absProject, ".claude", "settings.local.json");
|
|
1246
1432
|
case "project":
|
|
1247
|
-
return
|
|
1433
|
+
return join6(absProject, ".claude", "settings.json");
|
|
1248
1434
|
case "user":
|
|
1249
|
-
return
|
|
1435
|
+
return join6(homedir(), ".claude", "settings.json");
|
|
1250
1436
|
default:
|
|
1251
1437
|
return null;
|
|
1252
1438
|
}
|
|
@@ -1257,23 +1443,28 @@ program.command("intercept").description(
|
|
|
1257
1443
|
const stdinTimeout = setTimeout(() => {
|
|
1258
1444
|
process.exit(0);
|
|
1259
1445
|
}, 3e3);
|
|
1446
|
+
stdinTimeout.unref();
|
|
1260
1447
|
let input = "";
|
|
1448
|
+
let stdinFailed = false;
|
|
1261
1449
|
try {
|
|
1262
1450
|
for await (const chunk of process.stdin) {
|
|
1263
1451
|
input += chunk;
|
|
1264
1452
|
if (input.length > 1e6) break;
|
|
1265
1453
|
}
|
|
1266
1454
|
} catch {
|
|
1267
|
-
|
|
1268
|
-
process.exit(0);
|
|
1455
|
+
stdinFailed = true;
|
|
1269
1456
|
}
|
|
1270
1457
|
clearTimeout(stdinTimeout);
|
|
1271
|
-
if (!input.trim())
|
|
1458
|
+
if (stdinFailed || !input.trim()) {
|
|
1459
|
+
process.exitCode = 0;
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1272
1462
|
let payload;
|
|
1273
1463
|
try {
|
|
1274
1464
|
payload = JSON.parse(input);
|
|
1275
1465
|
} catch {
|
|
1276
|
-
process.
|
|
1466
|
+
process.exitCode = 0;
|
|
1467
|
+
return;
|
|
1277
1468
|
}
|
|
1278
1469
|
try {
|
|
1279
1470
|
const result = await dispatchHook(payload);
|
|
@@ -1282,6 +1473,45 @@ program.command("intercept").description(
|
|
|
1282
1473
|
}
|
|
1283
1474
|
} catch {
|
|
1284
1475
|
}
|
|
1476
|
+
process.exitCode = 0;
|
|
1477
|
+
});
|
|
1478
|
+
program.command("cursor-intercept").description(
|
|
1479
|
+
"Cursor beforeReadFile hook entry point (experimental). Reads JSON from stdin, writes Cursor-shaped response JSON to stdout."
|
|
1480
|
+
).action(async () => {
|
|
1481
|
+
const ALLOW_JSON = '{"permission":"allow"}';
|
|
1482
|
+
const stdinTimeout = setTimeout(() => {
|
|
1483
|
+
process.stdout.write(ALLOW_JSON);
|
|
1484
|
+
process.exit(0);
|
|
1485
|
+
}, 3e3);
|
|
1486
|
+
let input = "";
|
|
1487
|
+
try {
|
|
1488
|
+
for await (const chunk of process.stdin) {
|
|
1489
|
+
input += chunk;
|
|
1490
|
+
if (input.length > 1e6) break;
|
|
1491
|
+
}
|
|
1492
|
+
} catch {
|
|
1493
|
+
clearTimeout(stdinTimeout);
|
|
1494
|
+
process.stdout.write(ALLOW_JSON);
|
|
1495
|
+
process.exit(0);
|
|
1496
|
+
}
|
|
1497
|
+
clearTimeout(stdinTimeout);
|
|
1498
|
+
if (!input.trim()) {
|
|
1499
|
+
process.stdout.write(ALLOW_JSON);
|
|
1500
|
+
process.exit(0);
|
|
1501
|
+
}
|
|
1502
|
+
let payload;
|
|
1503
|
+
try {
|
|
1504
|
+
payload = JSON.parse(input);
|
|
1505
|
+
} catch {
|
|
1506
|
+
process.stdout.write(ALLOW_JSON);
|
|
1507
|
+
process.exit(0);
|
|
1508
|
+
}
|
|
1509
|
+
try {
|
|
1510
|
+
const result = await handleCursorBeforeReadFile(payload);
|
|
1511
|
+
process.stdout.write(JSON.stringify(result));
|
|
1512
|
+
} catch {
|
|
1513
|
+
process.stdout.write(ALLOW_JSON);
|
|
1514
|
+
}
|
|
1285
1515
|
process.exit(0);
|
|
1286
1516
|
});
|
|
1287
1517
|
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(
|
|
@@ -1296,9 +1526,9 @@ program.command("install-hook").description("Install engram hook entries into Cl
|
|
|
1296
1526
|
process.exit(1);
|
|
1297
1527
|
}
|
|
1298
1528
|
let existing = {};
|
|
1299
|
-
if (
|
|
1529
|
+
if (existsSync6(settingsPath)) {
|
|
1300
1530
|
try {
|
|
1301
|
-
const raw =
|
|
1531
|
+
const raw = readFileSync4(settingsPath, "utf-8");
|
|
1302
1532
|
existing = raw.trim() ? JSON.parse(raw) : {};
|
|
1303
1533
|
} catch (err) {
|
|
1304
1534
|
console.error(
|
|
@@ -1344,17 +1574,17 @@ program.command("install-hook").description("Install engram hook entries into Cl
|
|
|
1344
1574
|
}
|
|
1345
1575
|
try {
|
|
1346
1576
|
mkdirSync(dirname3(settingsPath), { recursive: true });
|
|
1347
|
-
if (
|
|
1577
|
+
if (existsSync6(settingsPath)) {
|
|
1348
1578
|
const backupPath = `${settingsPath}.engram-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
|
|
1349
1579
|
copyFileSync(settingsPath, backupPath);
|
|
1350
1580
|
console.log(chalk.dim(` Backup: ${backupPath}`));
|
|
1351
1581
|
}
|
|
1352
1582
|
const tmpPath = settingsPath + ".engram-tmp";
|
|
1353
|
-
|
|
1583
|
+
writeFileSync2(
|
|
1354
1584
|
tmpPath,
|
|
1355
1585
|
JSON.stringify(result.updated, null, 2) + "\n"
|
|
1356
1586
|
);
|
|
1357
|
-
|
|
1587
|
+
renameSync3(tmpPath, settingsPath);
|
|
1358
1588
|
} catch (err) {
|
|
1359
1589
|
console.error(
|
|
1360
1590
|
chalk.red(`
|
|
@@ -1388,7 +1618,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
|
|
|
1388
1618
|
console.error(chalk.red(`Unknown scope: ${opts.scope}`));
|
|
1389
1619
|
process.exit(1);
|
|
1390
1620
|
}
|
|
1391
|
-
if (!
|
|
1621
|
+
if (!existsSync6(settingsPath)) {
|
|
1392
1622
|
console.log(
|
|
1393
1623
|
chalk.yellow(`No settings file at ${settingsPath} \u2014 nothing to remove.`)
|
|
1394
1624
|
);
|
|
@@ -1396,7 +1626,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
|
|
|
1396
1626
|
}
|
|
1397
1627
|
let existing;
|
|
1398
1628
|
try {
|
|
1399
|
-
const raw =
|
|
1629
|
+
const raw = readFileSync4(settingsPath, "utf-8");
|
|
1400
1630
|
existing = raw.trim() ? JSON.parse(raw) : {};
|
|
1401
1631
|
} catch (err) {
|
|
1402
1632
|
console.error(
|
|
@@ -1416,8 +1646,8 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
|
|
|
1416
1646
|
const backupPath = `${settingsPath}.engram-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
|
|
1417
1647
|
copyFileSync(settingsPath, backupPath);
|
|
1418
1648
|
const tmpPath = settingsPath + ".engram-tmp";
|
|
1419
|
-
|
|
1420
|
-
|
|
1649
|
+
writeFileSync2(tmpPath, JSON.stringify(result.updated, null, 2) + "\n");
|
|
1650
|
+
renameSync3(tmpPath, settingsPath);
|
|
1421
1651
|
console.log(
|
|
1422
1652
|
chalk.green(
|
|
1423
1653
|
`
|
|
@@ -1504,9 +1734,9 @@ program.command("hook-disable").description("Disable engram hooks via kill switc
|
|
|
1504
1734
|
console.error(chalk.dim("Run 'engram init' first."));
|
|
1505
1735
|
process.exit(1);
|
|
1506
1736
|
}
|
|
1507
|
-
const flagPath =
|
|
1737
|
+
const flagPath = join6(projectRoot, ".engram", "hook-disabled");
|
|
1508
1738
|
try {
|
|
1509
|
-
|
|
1739
|
+
writeFileSync2(flagPath, (/* @__PURE__ */ new Date()).toISOString());
|
|
1510
1740
|
console.log(
|
|
1511
1741
|
chalk.green(`\u2705 engram hooks disabled for ${projectRoot}`)
|
|
1512
1742
|
);
|
|
@@ -1528,8 +1758,8 @@ program.command("hook-enable").description("Re-enable engram hooks (remove kill
|
|
|
1528
1758
|
console.error(chalk.red(`Not an engram project: ${absProject}`));
|
|
1529
1759
|
process.exit(1);
|
|
1530
1760
|
}
|
|
1531
|
-
const flagPath =
|
|
1532
|
-
if (!
|
|
1761
|
+
const flagPath = join6(projectRoot, ".engram", "hook-disabled");
|
|
1762
|
+
if (!existsSync6(flagPath)) {
|
|
1533
1763
|
console.log(
|
|
1534
1764
|
chalk.yellow(`engram hooks already enabled for ${projectRoot}`)
|
|
1535
1765
|
);
|
|
@@ -1547,4 +1777,90 @@ program.command("hook-enable").description("Re-enable engram hooks (remove kill
|
|
|
1547
1777
|
process.exit(1);
|
|
1548
1778
|
}
|
|
1549
1779
|
});
|
|
1780
|
+
program.command("memory-sync").description(
|
|
1781
|
+
"Write engram's structural facts into MEMORY.md (complementary to Anthropic Auto-Dream)"
|
|
1782
|
+
).option("-p, --project <path>", "Project directory", ".").option("--dry-run", "Print what would be written without writing", false).action(
|
|
1783
|
+
async (opts) => {
|
|
1784
|
+
const absProject = pathResolve(opts.project);
|
|
1785
|
+
const projectRoot = findProjectRoot(absProject);
|
|
1786
|
+
if (!projectRoot) {
|
|
1787
|
+
console.error(
|
|
1788
|
+
chalk.red(`Not an engram project: ${absProject}`)
|
|
1789
|
+
);
|
|
1790
|
+
console.error(chalk.dim("Run 'engram init' first."));
|
|
1791
|
+
process.exit(1);
|
|
1792
|
+
}
|
|
1793
|
+
const [gods, mistakeList, graphStats] = await Promise.all([
|
|
1794
|
+
godNodes(projectRoot, 10).catch(() => []),
|
|
1795
|
+
mistakes(projectRoot, { limit: 5 }).catch(() => []),
|
|
1796
|
+
stats(projectRoot).catch(() => null)
|
|
1797
|
+
]);
|
|
1798
|
+
if (!graphStats) {
|
|
1799
|
+
console.error(chalk.red("Failed to read graph stats."));
|
|
1800
|
+
process.exit(1);
|
|
1801
|
+
}
|
|
1802
|
+
let branch = null;
|
|
1803
|
+
try {
|
|
1804
|
+
const headPath = join6(projectRoot, ".git", "HEAD");
|
|
1805
|
+
if (existsSync6(headPath)) {
|
|
1806
|
+
const content = readFileSync4(headPath, "utf-8").trim();
|
|
1807
|
+
const m = content.match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
1808
|
+
if (m) branch = m[1];
|
|
1809
|
+
}
|
|
1810
|
+
} catch {
|
|
1811
|
+
}
|
|
1812
|
+
const section = buildEngramSection({
|
|
1813
|
+
projectName: basename2(projectRoot),
|
|
1814
|
+
branch,
|
|
1815
|
+
stats: {
|
|
1816
|
+
nodes: graphStats.nodes,
|
|
1817
|
+
edges: graphStats.edges,
|
|
1818
|
+
extractedPct: graphStats.extractedPct
|
|
1819
|
+
},
|
|
1820
|
+
godNodes: gods,
|
|
1821
|
+
landmines: mistakeList.map((m) => ({
|
|
1822
|
+
label: m.label,
|
|
1823
|
+
sourceFile: m.sourceFile
|
|
1824
|
+
})),
|
|
1825
|
+
lastMined: graphStats.lastMined
|
|
1826
|
+
});
|
|
1827
|
+
console.log(
|
|
1828
|
+
chalk.bold(`
|
|
1829
|
+
\u{1F4DD} engram memory-sync`)
|
|
1830
|
+
);
|
|
1831
|
+
console.log(
|
|
1832
|
+
chalk.dim(` Target: ${join6(projectRoot, "MEMORY.md")}`)
|
|
1833
|
+
);
|
|
1834
|
+
if (opts.dryRun) {
|
|
1835
|
+
console.log(chalk.cyan("\n Section to write (dry-run):\n"));
|
|
1836
|
+
console.log(
|
|
1837
|
+
section.split("\n").map((l) => " " + l).join("\n")
|
|
1838
|
+
);
|
|
1839
|
+
console.log(chalk.dim("\n (dry-run \u2014 no changes written)"));
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
const ok = writeEngramSectionToMemoryMd(projectRoot, section);
|
|
1843
|
+
if (!ok) {
|
|
1844
|
+
console.error(
|
|
1845
|
+
chalk.red(
|
|
1846
|
+
"\n \u274C Write failed. MEMORY.md may be too large, or the engram section exceeded its size cap."
|
|
1847
|
+
)
|
|
1848
|
+
);
|
|
1849
|
+
process.exit(1);
|
|
1850
|
+
}
|
|
1851
|
+
console.log(
|
|
1852
|
+
chalk.green(
|
|
1853
|
+
`
|
|
1854
|
+
\u2705 Synced ${gods.length} god nodes${mistakeList.length > 0 ? ` and ${mistakeList.length} landmines` : ""} to MEMORY.md`
|
|
1855
|
+
)
|
|
1856
|
+
);
|
|
1857
|
+
console.log(
|
|
1858
|
+
chalk.dim(
|
|
1859
|
+
`
|
|
1860
|
+
Next: Anthropic's Auto-Dream will consolidate this alongside its prose entries.
|
|
1861
|
+
`
|
|
1862
|
+
)
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
);
|
|
1550
1866
|
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-
|
|
14
|
+
} from "./chunk-3NUHMLRV.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-
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engramx",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.2",
|
|
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
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
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-
|
|
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",
|