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 +27 -4
- package/dist/{chunk-7KYR4SPZ.js → chunk-IYO4HETA.js} +1 -1
- package/dist/{chunk-NYFDM4FR.js → chunk-RGDHLGWQ.js} +36 -0
- package/dist/cli.js +310 -25
- package/dist/{core-MPNNCPFW.js → core-2TWPNHRQ.js} +3 -1
- package/dist/index.js +2 -2
- package/dist/serve.js +1 -1
- package/package.json +13 -9
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
|
-
|
|
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.
|
|
@@ -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-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-
|
|
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-
|
|
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
|
|
25
|
-
readFileSync as
|
|
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
|
|
31
|
+
renameSync as renameSync3
|
|
31
32
|
} from "fs";
|
|
32
|
-
import { dirname as dirname3, join as
|
|
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
|
|
646
|
-
if (
|
|
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
|
|
1405
|
+
return join6(absProject, ".claude", "settings.local.json");
|
|
1246
1406
|
case "project":
|
|
1247
|
-
return
|
|
1407
|
+
return join6(absProject, ".claude", "settings.json");
|
|
1248
1408
|
case "user":
|
|
1249
|
-
return
|
|
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 (
|
|
1498
|
+
if (existsSync6(settingsPath)) {
|
|
1300
1499
|
try {
|
|
1301
|
-
const raw =
|
|
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 (
|
|
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
|
-
|
|
1552
|
+
writeFileSync2(
|
|
1354
1553
|
tmpPath,
|
|
1355
1554
|
JSON.stringify(result.updated, null, 2) + "\n"
|
|
1356
1555
|
);
|
|
1357
|
-
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
1420
|
-
|
|
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 =
|
|
1706
|
+
const flagPath = join6(projectRoot, ".engram", "hook-disabled");
|
|
1508
1707
|
try {
|
|
1509
|
-
|
|
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 =
|
|
1532
|
-
if (!
|
|
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-
|
|
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-
|
|
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-
|
|
26
|
+
} from "./chunk-RGDHLGWQ.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.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
|
-
"
|
|
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",
|