engramx 0.3.1 → 0.4.0
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-IYO4HETA.js → chunk-ESPAWLH6.js} +1 -1
- package/dist/{chunk-RGDHLGWQ.js → chunk-R46DNLNR.js} +28 -4
- package/dist/cli.js +370 -39
- package/dist/{core-2TWPNHRQ.js → core-WTKXDUDO.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-486%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)
|
|
@@ -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-WTKXDUDO.js");
|
|
314
314
|
const store = await getStore(projectRoot);
|
|
315
315
|
try {
|
|
316
316
|
let view = VIEWS.general;
|
|
@@ -65,7 +65,8 @@ var GraphStore = class _GraphStore {
|
|
|
65
65
|
"CREATE INDEX IF NOT EXISTS idx_nodes_source_file ON nodes(source_file)",
|
|
66
66
|
"CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source)",
|
|
67
67
|
"CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target)",
|
|
68
|
-
"CREATE INDEX IF NOT EXISTS idx_edges_relation ON edges(relation)"
|
|
68
|
+
"CREATE INDEX IF NOT EXISTS idx_edges_relation ON edges(relation)",
|
|
69
|
+
"CREATE INDEX IF NOT EXISTS idx_edges_source_file ON edges(source_file)"
|
|
69
70
|
];
|
|
70
71
|
for (const sql of indexes) {
|
|
71
72
|
try {
|
|
@@ -113,6 +114,22 @@ var GraphStore = class _GraphStore {
|
|
|
113
114
|
]
|
|
114
115
|
);
|
|
115
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Remove all nodes and edges associated with a specific source file.
|
|
119
|
+
* Used by the file watcher for incremental re-indexing — old nodes for
|
|
120
|
+
* a changed file are cleared before re-extracting.
|
|
121
|
+
*/
|
|
122
|
+
deleteBySourceFile(sourceFile) {
|
|
123
|
+
this.db.run("BEGIN TRANSACTION");
|
|
124
|
+
try {
|
|
125
|
+
this.db.run("DELETE FROM edges WHERE source_file = ?", [sourceFile]);
|
|
126
|
+
this.db.run("DELETE FROM nodes WHERE source_file = ?", [sourceFile]);
|
|
127
|
+
this.db.run("COMMIT");
|
|
128
|
+
} catch (e) {
|
|
129
|
+
this.db.run("ROLLBACK");
|
|
130
|
+
throw e;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
116
133
|
bulkUpsert(nodes, edges) {
|
|
117
134
|
this.db.run("BEGIN TRANSACTION");
|
|
118
135
|
for (const node of nodes) this.upsertNode(node);
|
|
@@ -627,6 +644,12 @@ function renderFileStructure(store, relativeFilePath, tokenBudget = 600) {
|
|
|
627
644
|
};
|
|
628
645
|
}
|
|
629
646
|
|
|
647
|
+
// src/graph/path-utils.ts
|
|
648
|
+
function toPosixPath(p) {
|
|
649
|
+
if (!p) return p;
|
|
650
|
+
return p.replace(/\\/g, "/");
|
|
651
|
+
}
|
|
652
|
+
|
|
630
653
|
// src/miners/ast-miner.ts
|
|
631
654
|
import { readFileSync as readFileSync2, readdirSync, realpathSync } from "fs";
|
|
632
655
|
import { basename, extname, join, relative } from "path";
|
|
@@ -716,7 +739,7 @@ function extractFile(filePath, rootDir) {
|
|
|
716
739
|
if (!lang) return { nodes: [], edges: [] };
|
|
717
740
|
const content = readFileSync2(filePath, "utf-8");
|
|
718
741
|
const lines = content.split("\n");
|
|
719
|
-
const relPath = relative(rootDir, filePath);
|
|
742
|
+
const relPath = toPosixPath(relative(rootDir, filePath));
|
|
720
743
|
const stem = basename(filePath, ext);
|
|
721
744
|
const now = Date.now();
|
|
722
745
|
const nodes = [];
|
|
@@ -1230,7 +1253,7 @@ function parseFrontmatter(content) {
|
|
|
1230
1253
|
}
|
|
1231
1254
|
function parseYaml(block) {
|
|
1232
1255
|
const data = {};
|
|
1233
|
-
const lines = block.split("\n");
|
|
1256
|
+
const lines = block.replace(/\r/g, "").split("\n");
|
|
1234
1257
|
let i = 0;
|
|
1235
1258
|
while (i < lines.length) {
|
|
1236
1259
|
const line = lines[i];
|
|
@@ -1606,7 +1629,7 @@ async function getFileContext(projectRoot, absFilePath) {
|
|
|
1606
1629
|
try {
|
|
1607
1630
|
const root = resolve2(projectRoot);
|
|
1608
1631
|
const abs = resolve2(absFilePath);
|
|
1609
|
-
const relPath = relative2(root, abs);
|
|
1632
|
+
const relPath = toPosixPath(relative2(root, abs));
|
|
1610
1633
|
if (relPath.startsWith("..") || relPath === "") {
|
|
1611
1634
|
return empty;
|
|
1612
1635
|
}
|
|
@@ -1801,6 +1824,7 @@ export {
|
|
|
1801
1824
|
MAX_MISTAKE_LABEL_CHARS,
|
|
1802
1825
|
queryGraph,
|
|
1803
1826
|
shortestPath,
|
|
1827
|
+
toPosixPath,
|
|
1804
1828
|
SUPPORTED_EXTENSIONS,
|
|
1805
1829
|
extractFile,
|
|
1806
1830
|
extractDirectory,
|
package/dist/cli.js
CHANGED
|
@@ -4,25 +4,29 @@ import {
|
|
|
4
4
|
install,
|
|
5
5
|
status,
|
|
6
6
|
uninstall
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-ESPAWLH6.js";
|
|
8
8
|
import {
|
|
9
9
|
benchmark,
|
|
10
10
|
computeKeywordIDF,
|
|
11
|
+
extractFile,
|
|
12
|
+
getDbPath,
|
|
11
13
|
getFileContext,
|
|
14
|
+
getStore,
|
|
12
15
|
godNodes,
|
|
13
16
|
init,
|
|
14
17
|
learn,
|
|
15
18
|
mistakes,
|
|
16
19
|
path,
|
|
17
20
|
query,
|
|
18
|
-
stats
|
|
19
|
-
|
|
21
|
+
stats,
|
|
22
|
+
toPosixPath
|
|
23
|
+
} from "./chunk-R46DNLNR.js";
|
|
20
24
|
|
|
21
25
|
// src/cli.ts
|
|
22
26
|
import { Command } from "commander";
|
|
23
27
|
import chalk from "chalk";
|
|
24
28
|
import {
|
|
25
|
-
existsSync as
|
|
29
|
+
existsSync as existsSync7,
|
|
26
30
|
readFileSync as readFileSync4,
|
|
27
31
|
writeFileSync as writeFileSync2,
|
|
28
32
|
mkdirSync,
|
|
@@ -30,7 +34,7 @@ import {
|
|
|
30
34
|
copyFileSync,
|
|
31
35
|
renameSync as renameSync3
|
|
32
36
|
} from "fs";
|
|
33
|
-
import { dirname as dirname3, join as
|
|
37
|
+
import { dirname as dirname3, join as join7, resolve as pathResolve } from "path";
|
|
34
38
|
import { homedir } from "os";
|
|
35
39
|
|
|
36
40
|
// src/intercept/safety.ts
|
|
@@ -40,8 +44,8 @@ var PASSTHROUGH = null;
|
|
|
40
44
|
var DEFAULT_HANDLER_TIMEOUT_MS = 2e3;
|
|
41
45
|
async function withTimeout(promise, ms = DEFAULT_HANDLER_TIMEOUT_MS) {
|
|
42
46
|
let timer;
|
|
43
|
-
const timeout = new Promise((
|
|
44
|
-
timer = setTimeout(() =>
|
|
47
|
+
const timeout = new Promise((resolve6) => {
|
|
48
|
+
timer = setTimeout(() => resolve6(PASSTHROUGH), ms);
|
|
45
49
|
});
|
|
46
50
|
try {
|
|
47
51
|
return await Promise.race([promise, timeout]);
|
|
@@ -104,6 +108,10 @@ function isHardSystemPath(absPath) {
|
|
|
104
108
|
const p = absPath.replaceAll(sep, "/");
|
|
105
109
|
if (p === "/" || p.startsWith("/dev/") || p.startsWith("/proc/")) return true;
|
|
106
110
|
if (p.startsWith("/sys/")) return true;
|
|
111
|
+
const upper = p.toUpperCase();
|
|
112
|
+
if (upper.startsWith("//./") || upper.startsWith("//?/")) return true;
|
|
113
|
+
if (/^[A-Z]:\/WINDOWS(\/|$)/.test(upper)) return true;
|
|
114
|
+
if (/^[A-Z]:\/(PROGRAM FILES|PROGRAMDATA)(\/|$)/.test(upper)) return true;
|
|
107
115
|
return false;
|
|
108
116
|
}
|
|
109
117
|
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
@@ -257,6 +265,9 @@ function isValidCwd(cwd) {
|
|
|
257
265
|
}
|
|
258
266
|
function resolveInterceptContext(filePath, cwd) {
|
|
259
267
|
if (!filePath) return { proceed: false, reason: "empty-path" };
|
|
268
|
+
if (isHardSystemPath(filePath)) {
|
|
269
|
+
return { proceed: false, reason: "system-path" };
|
|
270
|
+
}
|
|
260
271
|
const absPath = normalizePath(filePath, cwd);
|
|
261
272
|
if (!absPath) return { proceed: false, reason: "normalize-failed" };
|
|
262
273
|
if (isHardSystemPath(absPath)) {
|
|
@@ -371,7 +382,9 @@ async function handleEditOrWrite(payload) {
|
|
|
371
382
|
if (!ctx.proceed) return PASSTHROUGH;
|
|
372
383
|
if (isContentUnsafeForIntercept(ctx.absPath)) return PASSTHROUGH;
|
|
373
384
|
if (isHookDisabled(ctx.projectRoot)) return PASSTHROUGH;
|
|
374
|
-
const relPath =
|
|
385
|
+
const relPath = toPosixPath(
|
|
386
|
+
relative(resolvePath(ctx.projectRoot), ctx.absPath)
|
|
387
|
+
);
|
|
375
388
|
if (!relPath || relPath.startsWith("..")) return PASSTHROUGH;
|
|
376
389
|
let found;
|
|
377
390
|
try {
|
|
@@ -430,7 +443,10 @@ async function handleBash(payload) {
|
|
|
430
443
|
|
|
431
444
|
// src/intercept/handlers/session-start.ts
|
|
432
445
|
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
446
|
+
import { execFile } from "child_process";
|
|
447
|
+
import { promisify } from "util";
|
|
433
448
|
import { basename, dirname as dirname2, join as join3, resolve as resolve2 } from "path";
|
|
449
|
+
var execFileAsync = promisify(execFile);
|
|
434
450
|
var MAX_GOD_NODES = 10;
|
|
435
451
|
var MAX_LANDMINES_IN_BRIEF = 3;
|
|
436
452
|
function readGitBranch(projectRoot) {
|
|
@@ -484,6 +500,37 @@ function formatBrief(args) {
|
|
|
484
500
|
);
|
|
485
501
|
return lines.join("\n");
|
|
486
502
|
}
|
|
503
|
+
async function queryMempalace(projectName) {
|
|
504
|
+
try {
|
|
505
|
+
const { stdout } = await execFileAsync(
|
|
506
|
+
"mcp-mempalace",
|
|
507
|
+
["mempalace-search", "--query", projectName],
|
|
508
|
+
{ timeout: 1500, encoding: "utf-8" }
|
|
509
|
+
);
|
|
510
|
+
const trimmed = stdout.trim();
|
|
511
|
+
if (!trimmed || trimmed.length < 20) return null;
|
|
512
|
+
try {
|
|
513
|
+
const parsed = JSON.parse(trimmed);
|
|
514
|
+
const results = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.results) ? parsed.results : [];
|
|
515
|
+
if (results.length === 0) return null;
|
|
516
|
+
const lines = ["[mempalace] Recent context:"];
|
|
517
|
+
for (const r of results.slice(0, 3)) {
|
|
518
|
+
const content = typeof r === "string" ? r : typeof r?.content === "string" ? r.content : typeof r?.document === "string" ? r.document : null;
|
|
519
|
+
if (content) {
|
|
520
|
+
const short = content.length > 120 ? content.slice(0, 117) + "..." : content;
|
|
521
|
+
lines.push(` - ${short}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return lines.length > 1 ? lines.join("\n") : null;
|
|
525
|
+
} catch {
|
|
526
|
+
const maxLen = 400;
|
|
527
|
+
const capped = trimmed.length > maxLen ? trimmed.slice(0, maxLen - 3) + "..." : trimmed;
|
|
528
|
+
return `[mempalace] ${capped}`;
|
|
529
|
+
}
|
|
530
|
+
} catch {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
487
534
|
function describeAgo(ms) {
|
|
488
535
|
if (ms < 0) return "just now";
|
|
489
536
|
const s = Math.floor(ms / 1e3);
|
|
@@ -505,7 +552,9 @@ async function handleSessionStart(payload) {
|
|
|
505
552
|
if (projectRoot === null) return PASSTHROUGH;
|
|
506
553
|
if (isHookDisabled(projectRoot)) return PASSTHROUGH;
|
|
507
554
|
try {
|
|
508
|
-
const
|
|
555
|
+
const branch = readGitBranch(projectRoot);
|
|
556
|
+
const projectName = basename(projectRoot);
|
|
557
|
+
const [gods, mistakeList, graphStats, mempalaceContext] = await Promise.all([
|
|
509
558
|
godNodes(projectRoot, MAX_GOD_NODES).catch(() => []),
|
|
510
559
|
mistakes(projectRoot, { limit: MAX_LANDMINES_IN_BRIEF }).catch(
|
|
511
560
|
() => []
|
|
@@ -519,11 +568,10 @@ async function handleSessionStart(payload) {
|
|
|
519
568
|
ambiguousPct: 0,
|
|
520
569
|
lastMined: 0,
|
|
521
570
|
totalQueryTokensSaved: 0
|
|
522
|
-
}))
|
|
571
|
+
})),
|
|
572
|
+
queryMempalace(projectName)
|
|
523
573
|
]);
|
|
524
574
|
if (graphStats.nodes === 0 && gods.length === 0) return PASSTHROUGH;
|
|
525
|
-
const branch = readGitBranch(projectRoot);
|
|
526
|
-
const projectName = basename(projectRoot);
|
|
527
575
|
const text = formatBrief({
|
|
528
576
|
projectName,
|
|
529
577
|
branch,
|
|
@@ -539,7 +587,8 @@ async function handleSessionStart(payload) {
|
|
|
539
587
|
sourceFile: m.sourceFile
|
|
540
588
|
}))
|
|
541
589
|
});
|
|
542
|
-
|
|
590
|
+
const fullText = mempalaceContext ? text + "\n\n" + mempalaceContext : text;
|
|
591
|
+
return buildSessionContextResponse("SessionStart", fullText);
|
|
543
592
|
} catch {
|
|
544
593
|
return PASSTHROUGH;
|
|
545
594
|
}
|
|
@@ -799,6 +848,123 @@ async function handlePostTool(payload) {
|
|
|
799
848
|
return PASSTHROUGH;
|
|
800
849
|
}
|
|
801
850
|
|
|
851
|
+
// src/intercept/handlers/pre-compact.ts
|
|
852
|
+
import { basename as basename2, resolve as resolve3 } from "path";
|
|
853
|
+
var MAX_GOD_NODES_COMPACT = 5;
|
|
854
|
+
var MAX_LANDMINES_COMPACT = 3;
|
|
855
|
+
function formatCompactBrief(args) {
|
|
856
|
+
const lines = [];
|
|
857
|
+
lines.push(
|
|
858
|
+
`[engram] Compaction survival \u2014 ${args.projectName} (${args.nodeCount} nodes, ${args.edgeCount} edges)`
|
|
859
|
+
);
|
|
860
|
+
if (args.godNodes.length > 0) {
|
|
861
|
+
lines.push("Key entities:");
|
|
862
|
+
for (const g of args.godNodes) {
|
|
863
|
+
lines.push(` - ${g.label} [${g.kind}] \u2014 ${g.sourceFile}`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (args.landmines.length > 0) {
|
|
867
|
+
lines.push("Active landmines:");
|
|
868
|
+
for (const m of args.landmines) {
|
|
869
|
+
lines.push(` - ${m.sourceFile}: ${m.label}`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
lines.push(
|
|
873
|
+
"engram is active \u2014 Read/Edit/Write interception continues after compaction."
|
|
874
|
+
);
|
|
875
|
+
return lines.join("\n");
|
|
876
|
+
}
|
|
877
|
+
async function handlePreCompact(payload) {
|
|
878
|
+
if (payload.hook_event_name !== "PreCompact") return PASSTHROUGH;
|
|
879
|
+
const cwd = payload.cwd;
|
|
880
|
+
if (!isValidCwd(cwd)) return PASSTHROUGH;
|
|
881
|
+
const projectRoot = findProjectRoot(cwd);
|
|
882
|
+
if (projectRoot === null) return PASSTHROUGH;
|
|
883
|
+
if (isHookDisabled(projectRoot)) return PASSTHROUGH;
|
|
884
|
+
try {
|
|
885
|
+
const [gods, mistakeList, graphStats] = await Promise.all([
|
|
886
|
+
godNodes(projectRoot, MAX_GOD_NODES_COMPACT).catch(() => []),
|
|
887
|
+
mistakes(projectRoot, { limit: MAX_LANDMINES_COMPACT }).catch(
|
|
888
|
+
() => []
|
|
889
|
+
),
|
|
890
|
+
stats(projectRoot).catch(() => ({
|
|
891
|
+
nodes: 0,
|
|
892
|
+
edges: 0,
|
|
893
|
+
communities: 0,
|
|
894
|
+
extractedPct: 0,
|
|
895
|
+
inferredPct: 0,
|
|
896
|
+
ambiguousPct: 0,
|
|
897
|
+
lastMined: 0,
|
|
898
|
+
totalQueryTokensSaved: 0
|
|
899
|
+
}))
|
|
900
|
+
]);
|
|
901
|
+
if (graphStats.nodes === 0 && gods.length === 0) return PASSTHROUGH;
|
|
902
|
+
const projectName = basename2(resolve3(projectRoot));
|
|
903
|
+
const text = formatCompactBrief({
|
|
904
|
+
projectName,
|
|
905
|
+
nodeCount: graphStats.nodes,
|
|
906
|
+
edgeCount: graphStats.edges,
|
|
907
|
+
godNodes: gods.map((g) => ({
|
|
908
|
+
label: g.label,
|
|
909
|
+
kind: g.kind,
|
|
910
|
+
sourceFile: g.sourceFile
|
|
911
|
+
})),
|
|
912
|
+
landmines: mistakeList.map((m) => ({
|
|
913
|
+
label: m.label,
|
|
914
|
+
sourceFile: m.sourceFile
|
|
915
|
+
}))
|
|
916
|
+
});
|
|
917
|
+
return buildSessionContextResponse("SessionStart", text);
|
|
918
|
+
} catch {
|
|
919
|
+
return PASSTHROUGH;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// src/intercept/handlers/cwd-changed.ts
|
|
924
|
+
import { basename as basename3, resolve as resolve4 } from "path";
|
|
925
|
+
var MAX_GOD_NODES_SWITCH = 5;
|
|
926
|
+
async function handleCwdChanged(payload) {
|
|
927
|
+
if (payload.hook_event_name !== "CwdChanged") return PASSTHROUGH;
|
|
928
|
+
const cwd = payload.cwd;
|
|
929
|
+
if (!isValidCwd(cwd)) return PASSTHROUGH;
|
|
930
|
+
const projectRoot = findProjectRoot(cwd);
|
|
931
|
+
if (projectRoot === null) return PASSTHROUGH;
|
|
932
|
+
if (isHookDisabled(projectRoot)) return PASSTHROUGH;
|
|
933
|
+
try {
|
|
934
|
+
const [gods, graphStats] = await Promise.all([
|
|
935
|
+
godNodes(projectRoot, MAX_GOD_NODES_SWITCH).catch(() => []),
|
|
936
|
+
stats(projectRoot).catch(() => ({
|
|
937
|
+
nodes: 0,
|
|
938
|
+
edges: 0,
|
|
939
|
+
communities: 0,
|
|
940
|
+
extractedPct: 0,
|
|
941
|
+
inferredPct: 0,
|
|
942
|
+
ambiguousPct: 0,
|
|
943
|
+
lastMined: 0,
|
|
944
|
+
totalQueryTokensSaved: 0
|
|
945
|
+
}))
|
|
946
|
+
]);
|
|
947
|
+
if (graphStats.nodes === 0) return PASSTHROUGH;
|
|
948
|
+
const projectName = basename3(resolve4(projectRoot));
|
|
949
|
+
const lines = [];
|
|
950
|
+
lines.push(
|
|
951
|
+
`[engram] Project switched to ${projectName} (${graphStats.nodes} nodes, ${graphStats.edges} edges)`
|
|
952
|
+
);
|
|
953
|
+
if (gods.length > 0) {
|
|
954
|
+
lines.push("Core entities:");
|
|
955
|
+
for (const g of gods.slice(0, MAX_GOD_NODES_SWITCH)) {
|
|
956
|
+
lines.push(` - ${g.label} [${g.kind}] \u2014 ${g.sourceFile}`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
lines.push(
|
|
960
|
+
"engram interception is active for this project."
|
|
961
|
+
);
|
|
962
|
+
return buildSessionContextResponse("SessionStart", lines.join("\n"));
|
|
963
|
+
} catch {
|
|
964
|
+
return PASSTHROUGH;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
802
968
|
// src/intercept/dispatch.ts
|
|
803
969
|
function validatePayload(raw) {
|
|
804
970
|
if (raw === null || typeof raw !== "object") return null;
|
|
@@ -829,6 +995,14 @@ async function dispatchHook(rawPayload) {
|
|
|
829
995
|
return runHandler(
|
|
830
996
|
() => handlePostTool(payload)
|
|
831
997
|
);
|
|
998
|
+
case "PreCompact":
|
|
999
|
+
return runHandler(
|
|
1000
|
+
() => handlePreCompact(payload)
|
|
1001
|
+
);
|
|
1002
|
+
case "CwdChanged":
|
|
1003
|
+
return runHandler(
|
|
1004
|
+
() => handleCwdChanged(payload)
|
|
1005
|
+
);
|
|
832
1006
|
default:
|
|
833
1007
|
return PASSTHROUGH;
|
|
834
1008
|
}
|
|
@@ -888,6 +1062,106 @@ function extractPreToolDecision(result) {
|
|
|
888
1062
|
return "passthrough";
|
|
889
1063
|
}
|
|
890
1064
|
|
|
1065
|
+
// src/watcher.ts
|
|
1066
|
+
import { watch, existsSync as existsSync5, statSync as statSync3 } from "fs";
|
|
1067
|
+
import { resolve as resolve5, relative as relative2, extname } from "path";
|
|
1068
|
+
var WATCHABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1069
|
+
".ts",
|
|
1070
|
+
".tsx",
|
|
1071
|
+
".js",
|
|
1072
|
+
".jsx",
|
|
1073
|
+
".py",
|
|
1074
|
+
".go",
|
|
1075
|
+
".rs",
|
|
1076
|
+
".java",
|
|
1077
|
+
".c",
|
|
1078
|
+
".cpp",
|
|
1079
|
+
".cs",
|
|
1080
|
+
".rb"
|
|
1081
|
+
]);
|
|
1082
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
1083
|
+
".engram",
|
|
1084
|
+
"node_modules",
|
|
1085
|
+
".git",
|
|
1086
|
+
"dist",
|
|
1087
|
+
"build",
|
|
1088
|
+
".next",
|
|
1089
|
+
"__pycache__",
|
|
1090
|
+
".venv",
|
|
1091
|
+
"target",
|
|
1092
|
+
"vendor"
|
|
1093
|
+
]);
|
|
1094
|
+
var DEBOUNCE_MS = 300;
|
|
1095
|
+
function shouldIgnore(relPath) {
|
|
1096
|
+
const parts = relPath.split(/[/\\]/);
|
|
1097
|
+
return parts.some((p) => IGNORED_DIRS.has(p));
|
|
1098
|
+
}
|
|
1099
|
+
async function reindexFile(absPath, projectRoot) {
|
|
1100
|
+
const ext = extname(absPath).toLowerCase();
|
|
1101
|
+
if (!WATCHABLE_EXTENSIONS.has(ext)) return 0;
|
|
1102
|
+
if (!existsSync5(absPath)) return 0;
|
|
1103
|
+
try {
|
|
1104
|
+
if (statSync3(absPath).isDirectory()) return 0;
|
|
1105
|
+
} catch {
|
|
1106
|
+
return 0;
|
|
1107
|
+
}
|
|
1108
|
+
const relPath = toPosixPath(relative2(projectRoot, absPath));
|
|
1109
|
+
if (shouldIgnore(relPath)) return 0;
|
|
1110
|
+
const store = await getStore(projectRoot);
|
|
1111
|
+
try {
|
|
1112
|
+
store.deleteBySourceFile(relPath);
|
|
1113
|
+
const { nodes, edges } = extractFile(absPath, projectRoot);
|
|
1114
|
+
if (nodes.length > 0 || edges.length > 0) {
|
|
1115
|
+
store.bulkUpsert(nodes, edges);
|
|
1116
|
+
}
|
|
1117
|
+
return nodes.length;
|
|
1118
|
+
} finally {
|
|
1119
|
+
store.close();
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
function watchProject(projectRoot, options = {}) {
|
|
1123
|
+
const root = resolve5(projectRoot);
|
|
1124
|
+
const controller = new AbortController();
|
|
1125
|
+
if (!existsSync5(getDbPath(root))) {
|
|
1126
|
+
throw new Error(
|
|
1127
|
+
`engram: no graph found at ${root}. Run 'engram init' first.`
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
const debounceTimers = /* @__PURE__ */ new Map();
|
|
1131
|
+
const watcher = watch(root, { recursive: true, signal: controller.signal });
|
|
1132
|
+
watcher.on("change", (_eventType, filename) => {
|
|
1133
|
+
if (typeof filename !== "string") return;
|
|
1134
|
+
const absPath = resolve5(root, filename);
|
|
1135
|
+
const relPath = toPosixPath(relative2(root, absPath));
|
|
1136
|
+
if (shouldIgnore(relPath)) return;
|
|
1137
|
+
const ext = extname(filename).toLowerCase();
|
|
1138
|
+
if (!WATCHABLE_EXTENSIONS.has(ext)) return;
|
|
1139
|
+
const existing = debounceTimers.get(absPath);
|
|
1140
|
+
if (existing) clearTimeout(existing);
|
|
1141
|
+
debounceTimers.set(
|
|
1142
|
+
absPath,
|
|
1143
|
+
setTimeout(async () => {
|
|
1144
|
+
debounceTimers.delete(absPath);
|
|
1145
|
+
try {
|
|
1146
|
+
const count = await reindexFile(absPath, root);
|
|
1147
|
+
if (count > 0) {
|
|
1148
|
+
options.onReindex?.(relPath, count);
|
|
1149
|
+
}
|
|
1150
|
+
} catch (err) {
|
|
1151
|
+
options.onError?.(
|
|
1152
|
+
err instanceof Error ? err : new Error(String(err))
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
}, DEBOUNCE_MS)
|
|
1156
|
+
);
|
|
1157
|
+
});
|
|
1158
|
+
watcher.on("error", (err) => {
|
|
1159
|
+
options.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
1160
|
+
});
|
|
1161
|
+
options.onReady?.();
|
|
1162
|
+
return controller;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
891
1165
|
// src/intercept/cursor-adapter.ts
|
|
892
1166
|
var ALLOW = { permission: "allow" };
|
|
893
1167
|
function toClaudeReadPayload(cursorPayload) {
|
|
@@ -931,7 +1205,9 @@ var ENGRAM_HOOK_EVENTS = [
|
|
|
931
1205
|
"PreToolUse",
|
|
932
1206
|
"PostToolUse",
|
|
933
1207
|
"SessionStart",
|
|
934
|
-
"UserPromptSubmit"
|
|
1208
|
+
"UserPromptSubmit",
|
|
1209
|
+
"PreCompact",
|
|
1210
|
+
"CwdChanged"
|
|
935
1211
|
];
|
|
936
1212
|
var ENGRAM_PRETOOL_MATCHER = "Read|Edit|Write|Bash";
|
|
937
1213
|
var DEFAULT_ENGRAM_COMMAND = "engram intercept";
|
|
@@ -959,6 +1235,14 @@ function buildEngramHookEntries(command = DEFAULT_ENGRAM_COMMAND, timeout = DEFA
|
|
|
959
1235
|
UserPromptSubmit: {
|
|
960
1236
|
// No matcher — UserPromptSubmit has no tool name.
|
|
961
1237
|
hooks: [baseCmd]
|
|
1238
|
+
},
|
|
1239
|
+
PreCompact: {
|
|
1240
|
+
// No matcher — PreCompact has no tool name.
|
|
1241
|
+
hooks: [baseCmd]
|
|
1242
|
+
},
|
|
1243
|
+
CwdChanged: {
|
|
1244
|
+
// No matcher — CwdChanged has no tool name.
|
|
1245
|
+
hooks: [baseCmd]
|
|
962
1246
|
}
|
|
963
1247
|
};
|
|
964
1248
|
}
|
|
@@ -1142,13 +1426,13 @@ function formatStatsSummary(summary) {
|
|
|
1142
1426
|
|
|
1143
1427
|
// src/intercept/memory-md.ts
|
|
1144
1428
|
import {
|
|
1145
|
-
existsSync as
|
|
1429
|
+
existsSync as existsSync6,
|
|
1146
1430
|
readFileSync as readFileSync3,
|
|
1147
1431
|
writeFileSync,
|
|
1148
1432
|
renameSync as renameSync2,
|
|
1149
|
-
statSync as
|
|
1433
|
+
statSync as statSync4
|
|
1150
1434
|
} from "fs";
|
|
1151
|
-
import { join as
|
|
1435
|
+
import { join as join6 } from "path";
|
|
1152
1436
|
var ENGRAM_MARKER_START = "<!-- engram:structural-facts:start -->";
|
|
1153
1437
|
var ENGRAM_MARKER_END = "<!-- engram:structural-facts:end -->";
|
|
1154
1438
|
var MAX_MEMORY_FILE_BYTES = 1e6;
|
|
@@ -1219,11 +1503,11 @@ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
|
|
|
1219
1503
|
if (engramSection.length > MAX_ENGRAM_SECTION_BYTES) {
|
|
1220
1504
|
return false;
|
|
1221
1505
|
}
|
|
1222
|
-
const memoryPath =
|
|
1506
|
+
const memoryPath = join6(projectRoot, "MEMORY.md");
|
|
1223
1507
|
try {
|
|
1224
1508
|
let existing = "";
|
|
1225
|
-
if (
|
|
1226
|
-
const st =
|
|
1509
|
+
if (existsSync6(memoryPath)) {
|
|
1510
|
+
const st = statSync4(memoryPath);
|
|
1227
1511
|
if (st.size > MAX_MEMORY_FILE_BYTES) {
|
|
1228
1512
|
return false;
|
|
1229
1513
|
}
|
|
@@ -1240,7 +1524,7 @@ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
|
|
|
1240
1524
|
}
|
|
1241
1525
|
|
|
1242
1526
|
// src/cli.ts
|
|
1243
|
-
import { basename as
|
|
1527
|
+
import { basename as basename4 } from "path";
|
|
1244
1528
|
var program = new Command();
|
|
1245
1529
|
program.name("engram").description(
|
|
1246
1530
|
"Context as infra for AI coding tools \u2014 hook-based Read/Edit interception + structural graph summaries"
|
|
@@ -1276,6 +1560,48 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
|
|
|
1276
1560
|
}
|
|
1277
1561
|
console.log(chalk.green("\n\u2705 Ready. Your AI now has persistent memory."));
|
|
1278
1562
|
console.log(chalk.dim(" Graph stored in .engram/graph.db"));
|
|
1563
|
+
const resolvedProject = pathResolve(projectPath);
|
|
1564
|
+
const localSettings = join7(resolvedProject, ".claude", "settings.local.json");
|
|
1565
|
+
const projectSettings = join7(resolvedProject, ".claude", "settings.json");
|
|
1566
|
+
const hasHooks = existsSync7(localSettings) && readFileSync4(localSettings, "utf-8").includes("engram intercept") || existsSync7(projectSettings) && readFileSync4(projectSettings, "utf-8").includes("engram intercept");
|
|
1567
|
+
if (!hasHooks) {
|
|
1568
|
+
console.log(
|
|
1569
|
+
chalk.yellow("\n\u{1F4A1} Next step: ") + chalk.white("engram install-hook") + chalk.dim(
|
|
1570
|
+
" \u2014 enables automatic Read interception (82% token savings)"
|
|
1571
|
+
)
|
|
1572
|
+
);
|
|
1573
|
+
console.log(
|
|
1574
|
+
chalk.dim(
|
|
1575
|
+
" Also recommended: " + chalk.white("engram hooks install") + " \u2014 auto-rebuild graph on git commit"
|
|
1576
|
+
)
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
});
|
|
1580
|
+
program.command("watch").description("Watch project for file changes and re-index incrementally").argument("[path]", "Project directory", ".").action(async (projectPath) => {
|
|
1581
|
+
const resolvedPath = pathResolve(projectPath);
|
|
1582
|
+
console.log(
|
|
1583
|
+
chalk.dim("\u{1F441} Watching ") + chalk.white(resolvedPath) + chalk.dim(" for changes...")
|
|
1584
|
+
);
|
|
1585
|
+
const controller = watchProject(resolvedPath, {
|
|
1586
|
+
onReindex: (filePath, nodeCount) => {
|
|
1587
|
+
console.log(
|
|
1588
|
+
chalk.green(" \u21BB ") + chalk.white(filePath) + chalk.dim(` (${nodeCount} nodes)`)
|
|
1589
|
+
);
|
|
1590
|
+
},
|
|
1591
|
+
onError: (err) => {
|
|
1592
|
+
console.error(chalk.red(" \u2717 ") + err.message);
|
|
1593
|
+
},
|
|
1594
|
+
onReady: () => {
|
|
1595
|
+
console.log(chalk.green(" \u2713 Watcher active.") + chalk.dim(" Press Ctrl+C to stop."));
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1598
|
+
process.on("SIGINT", () => {
|
|
1599
|
+
controller.abort();
|
|
1600
|
+
console.log(chalk.dim("\n Watcher stopped."));
|
|
1601
|
+
process.exit(0);
|
|
1602
|
+
});
|
|
1603
|
+
await new Promise(() => {
|
|
1604
|
+
});
|
|
1279
1605
|
});
|
|
1280
1606
|
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
1607
|
const result = await query(opts.project, question, {
|
|
@@ -1402,11 +1728,11 @@ function resolveSettingsPath(scope, projectPath) {
|
|
|
1402
1728
|
const absProject = pathResolve(projectPath);
|
|
1403
1729
|
switch (scope) {
|
|
1404
1730
|
case "local":
|
|
1405
|
-
return
|
|
1731
|
+
return join7(absProject, ".claude", "settings.local.json");
|
|
1406
1732
|
case "project":
|
|
1407
|
-
return
|
|
1733
|
+
return join7(absProject, ".claude", "settings.json");
|
|
1408
1734
|
case "user":
|
|
1409
|
-
return
|
|
1735
|
+
return join7(homedir(), ".claude", "settings.json");
|
|
1410
1736
|
default:
|
|
1411
1737
|
return null;
|
|
1412
1738
|
}
|
|
@@ -1417,23 +1743,28 @@ program.command("intercept").description(
|
|
|
1417
1743
|
const stdinTimeout = setTimeout(() => {
|
|
1418
1744
|
process.exit(0);
|
|
1419
1745
|
}, 3e3);
|
|
1746
|
+
stdinTimeout.unref();
|
|
1420
1747
|
let input = "";
|
|
1748
|
+
let stdinFailed = false;
|
|
1421
1749
|
try {
|
|
1422
1750
|
for await (const chunk of process.stdin) {
|
|
1423
1751
|
input += chunk;
|
|
1424
1752
|
if (input.length > 1e6) break;
|
|
1425
1753
|
}
|
|
1426
1754
|
} catch {
|
|
1427
|
-
|
|
1428
|
-
process.exit(0);
|
|
1755
|
+
stdinFailed = true;
|
|
1429
1756
|
}
|
|
1430
1757
|
clearTimeout(stdinTimeout);
|
|
1431
|
-
if (!input.trim())
|
|
1758
|
+
if (stdinFailed || !input.trim()) {
|
|
1759
|
+
process.exitCode = 0;
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1432
1762
|
let payload;
|
|
1433
1763
|
try {
|
|
1434
1764
|
payload = JSON.parse(input);
|
|
1435
1765
|
} catch {
|
|
1436
|
-
process.
|
|
1766
|
+
process.exitCode = 0;
|
|
1767
|
+
return;
|
|
1437
1768
|
}
|
|
1438
1769
|
try {
|
|
1439
1770
|
const result = await dispatchHook(payload);
|
|
@@ -1442,7 +1773,7 @@ program.command("intercept").description(
|
|
|
1442
1773
|
}
|
|
1443
1774
|
} catch {
|
|
1444
1775
|
}
|
|
1445
|
-
process.
|
|
1776
|
+
process.exitCode = 0;
|
|
1446
1777
|
});
|
|
1447
1778
|
program.command("cursor-intercept").description(
|
|
1448
1779
|
"Cursor beforeReadFile hook entry point (experimental). Reads JSON from stdin, writes Cursor-shaped response JSON to stdout."
|
|
@@ -1495,7 +1826,7 @@ program.command("install-hook").description("Install engram hook entries into Cl
|
|
|
1495
1826
|
process.exit(1);
|
|
1496
1827
|
}
|
|
1497
1828
|
let existing = {};
|
|
1498
|
-
if (
|
|
1829
|
+
if (existsSync7(settingsPath)) {
|
|
1499
1830
|
try {
|
|
1500
1831
|
const raw = readFileSync4(settingsPath, "utf-8");
|
|
1501
1832
|
existing = raw.trim() ? JSON.parse(raw) : {};
|
|
@@ -1543,7 +1874,7 @@ program.command("install-hook").description("Install engram hook entries into Cl
|
|
|
1543
1874
|
}
|
|
1544
1875
|
try {
|
|
1545
1876
|
mkdirSync(dirname3(settingsPath), { recursive: true });
|
|
1546
|
-
if (
|
|
1877
|
+
if (existsSync7(settingsPath)) {
|
|
1547
1878
|
const backupPath = `${settingsPath}.engram-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
|
|
1548
1879
|
copyFileSync(settingsPath, backupPath);
|
|
1549
1880
|
console.log(chalk.dim(` Backup: ${backupPath}`));
|
|
@@ -1587,7 +1918,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
|
|
|
1587
1918
|
console.error(chalk.red(`Unknown scope: ${opts.scope}`));
|
|
1588
1919
|
process.exit(1);
|
|
1589
1920
|
}
|
|
1590
|
-
if (!
|
|
1921
|
+
if (!existsSync7(settingsPath)) {
|
|
1591
1922
|
console.log(
|
|
1592
1923
|
chalk.yellow(`No settings file at ${settingsPath} \u2014 nothing to remove.`)
|
|
1593
1924
|
);
|
|
@@ -1703,7 +2034,7 @@ program.command("hook-disable").description("Disable engram hooks via kill switc
|
|
|
1703
2034
|
console.error(chalk.dim("Run 'engram init' first."));
|
|
1704
2035
|
process.exit(1);
|
|
1705
2036
|
}
|
|
1706
|
-
const flagPath =
|
|
2037
|
+
const flagPath = join7(projectRoot, ".engram", "hook-disabled");
|
|
1707
2038
|
try {
|
|
1708
2039
|
writeFileSync2(flagPath, (/* @__PURE__ */ new Date()).toISOString());
|
|
1709
2040
|
console.log(
|
|
@@ -1727,8 +2058,8 @@ program.command("hook-enable").description("Re-enable engram hooks (remove kill
|
|
|
1727
2058
|
console.error(chalk.red(`Not an engram project: ${absProject}`));
|
|
1728
2059
|
process.exit(1);
|
|
1729
2060
|
}
|
|
1730
|
-
const flagPath =
|
|
1731
|
-
if (!
|
|
2061
|
+
const flagPath = join7(projectRoot, ".engram", "hook-disabled");
|
|
2062
|
+
if (!existsSync7(flagPath)) {
|
|
1732
2063
|
console.log(
|
|
1733
2064
|
chalk.yellow(`engram hooks already enabled for ${projectRoot}`)
|
|
1734
2065
|
);
|
|
@@ -1770,8 +2101,8 @@ program.command("memory-sync").description(
|
|
|
1770
2101
|
}
|
|
1771
2102
|
let branch = null;
|
|
1772
2103
|
try {
|
|
1773
|
-
const headPath =
|
|
1774
|
-
if (
|
|
2104
|
+
const headPath = join7(projectRoot, ".git", "HEAD");
|
|
2105
|
+
if (existsSync7(headPath)) {
|
|
1775
2106
|
const content = readFileSync4(headPath, "utf-8").trim();
|
|
1776
2107
|
const m = content.match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
1777
2108
|
if (m) branch = m[1];
|
|
@@ -1779,7 +2110,7 @@ program.command("memory-sync").description(
|
|
|
1779
2110
|
} catch {
|
|
1780
2111
|
}
|
|
1781
2112
|
const section = buildEngramSection({
|
|
1782
|
-
projectName:
|
|
2113
|
+
projectName: basename4(projectRoot),
|
|
1783
2114
|
branch,
|
|
1784
2115
|
stats: {
|
|
1785
2116
|
nodes: graphStats.nodes,
|
|
@@ -1798,7 +2129,7 @@ program.command("memory-sync").description(
|
|
|
1798
2129
|
\u{1F4DD} engram memory-sync`)
|
|
1799
2130
|
);
|
|
1800
2131
|
console.log(
|
|
1801
|
-
chalk.dim(` Target: ${
|
|
2132
|
+
chalk.dim(` Target: ${join7(projectRoot, "MEMORY.md")}`)
|
|
1802
2133
|
);
|
|
1803
2134
|
if (opts.dryRun) {
|
|
1804
2135
|
console.log(chalk.cyan("\n Section to write (dry-run):\n"));
|
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-ESPAWLH6.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-R46DNLNR.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
|
+
"version": "0.4.0",
|
|
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": {
|