engramx 0.3.2 → 0.4.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 +1 -1
- package/dist/{chunk-JXJNXQUM.js → chunk-ESPAWLH6.js} +1 -1
- package/dist/{chunk-3NUHMLRV.js → chunk-R46DNLNR.js} +18 -1
- package/dist/cli.js +339 -36
- package/dist/{core-AJD3SS6U.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">
|
|
@@ -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);
|
package/dist/cli.js
CHANGED
|
@@ -4,11 +4,14 @@ 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,
|
|
@@ -17,13 +20,13 @@ import {
|
|
|
17
20
|
query,
|
|
18
21
|
stats,
|
|
19
22
|
toPosixPath
|
|
20
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-R46DNLNR.js";
|
|
21
24
|
|
|
22
25
|
// src/cli.ts
|
|
23
26
|
import { Command } from "commander";
|
|
24
27
|
import chalk from "chalk";
|
|
25
28
|
import {
|
|
26
|
-
existsSync as
|
|
29
|
+
existsSync as existsSync7,
|
|
27
30
|
readFileSync as readFileSync4,
|
|
28
31
|
writeFileSync as writeFileSync2,
|
|
29
32
|
mkdirSync,
|
|
@@ -31,7 +34,7 @@ import {
|
|
|
31
34
|
copyFileSync,
|
|
32
35
|
renameSync as renameSync3
|
|
33
36
|
} from "fs";
|
|
34
|
-
import { dirname as dirname3, join as
|
|
37
|
+
import { dirname as dirname3, join as join7, resolve as pathResolve } from "path";
|
|
35
38
|
import { homedir } from "os";
|
|
36
39
|
|
|
37
40
|
// src/intercept/safety.ts
|
|
@@ -41,8 +44,8 @@ var PASSTHROUGH = null;
|
|
|
41
44
|
var DEFAULT_HANDLER_TIMEOUT_MS = 2e3;
|
|
42
45
|
async function withTimeout(promise, ms = DEFAULT_HANDLER_TIMEOUT_MS) {
|
|
43
46
|
let timer;
|
|
44
|
-
const timeout = new Promise((
|
|
45
|
-
timer = setTimeout(() =>
|
|
47
|
+
const timeout = new Promise((resolve6) => {
|
|
48
|
+
timer = setTimeout(() => resolve6(PASSTHROUGH), ms);
|
|
46
49
|
});
|
|
47
50
|
try {
|
|
48
51
|
return await Promise.race([promise, timeout]);
|
|
@@ -440,7 +443,10 @@ async function handleBash(payload) {
|
|
|
440
443
|
|
|
441
444
|
// src/intercept/handlers/session-start.ts
|
|
442
445
|
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
446
|
+
import { execFile } from "child_process";
|
|
447
|
+
import { promisify } from "util";
|
|
443
448
|
import { basename, dirname as dirname2, join as join3, resolve as resolve2 } from "path";
|
|
449
|
+
var execFileAsync = promisify(execFile);
|
|
444
450
|
var MAX_GOD_NODES = 10;
|
|
445
451
|
var MAX_LANDMINES_IN_BRIEF = 3;
|
|
446
452
|
function readGitBranch(projectRoot) {
|
|
@@ -494,6 +500,37 @@ function formatBrief(args) {
|
|
|
494
500
|
);
|
|
495
501
|
return lines.join("\n");
|
|
496
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
|
+
}
|
|
497
534
|
function describeAgo(ms) {
|
|
498
535
|
if (ms < 0) return "just now";
|
|
499
536
|
const s = Math.floor(ms / 1e3);
|
|
@@ -515,7 +552,9 @@ async function handleSessionStart(payload) {
|
|
|
515
552
|
if (projectRoot === null) return PASSTHROUGH;
|
|
516
553
|
if (isHookDisabled(projectRoot)) return PASSTHROUGH;
|
|
517
554
|
try {
|
|
518
|
-
const
|
|
555
|
+
const branch = readGitBranch(projectRoot);
|
|
556
|
+
const projectName = basename(projectRoot);
|
|
557
|
+
const [gods, mistakeList, graphStats, mempalaceContext] = await Promise.all([
|
|
519
558
|
godNodes(projectRoot, MAX_GOD_NODES).catch(() => []),
|
|
520
559
|
mistakes(projectRoot, { limit: MAX_LANDMINES_IN_BRIEF }).catch(
|
|
521
560
|
() => []
|
|
@@ -529,11 +568,10 @@ async function handleSessionStart(payload) {
|
|
|
529
568
|
ambiguousPct: 0,
|
|
530
569
|
lastMined: 0,
|
|
531
570
|
totalQueryTokensSaved: 0
|
|
532
|
-
}))
|
|
571
|
+
})),
|
|
572
|
+
queryMempalace(projectName)
|
|
533
573
|
]);
|
|
534
574
|
if (graphStats.nodes === 0 && gods.length === 0) return PASSTHROUGH;
|
|
535
|
-
const branch = readGitBranch(projectRoot);
|
|
536
|
-
const projectName = basename(projectRoot);
|
|
537
575
|
const text = formatBrief({
|
|
538
576
|
projectName,
|
|
539
577
|
branch,
|
|
@@ -549,7 +587,8 @@ async function handleSessionStart(payload) {
|
|
|
549
587
|
sourceFile: m.sourceFile
|
|
550
588
|
}))
|
|
551
589
|
});
|
|
552
|
-
|
|
590
|
+
const fullText = mempalaceContext ? text + "\n\n" + mempalaceContext : text;
|
|
591
|
+
return buildSessionContextResponse("SessionStart", fullText);
|
|
553
592
|
} catch {
|
|
554
593
|
return PASSTHROUGH;
|
|
555
594
|
}
|
|
@@ -809,6 +848,123 @@ async function handlePostTool(payload) {
|
|
|
809
848
|
return PASSTHROUGH;
|
|
810
849
|
}
|
|
811
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
|
+
|
|
812
968
|
// src/intercept/dispatch.ts
|
|
813
969
|
function validatePayload(raw) {
|
|
814
970
|
if (raw === null || typeof raw !== "object") return null;
|
|
@@ -839,6 +995,14 @@ async function dispatchHook(rawPayload) {
|
|
|
839
995
|
return runHandler(
|
|
840
996
|
() => handlePostTool(payload)
|
|
841
997
|
);
|
|
998
|
+
case "PreCompact":
|
|
999
|
+
return runHandler(
|
|
1000
|
+
() => handlePreCompact(payload)
|
|
1001
|
+
);
|
|
1002
|
+
case "CwdChanged":
|
|
1003
|
+
return runHandler(
|
|
1004
|
+
() => handleCwdChanged(payload)
|
|
1005
|
+
);
|
|
842
1006
|
default:
|
|
843
1007
|
return PASSTHROUGH;
|
|
844
1008
|
}
|
|
@@ -898,6 +1062,106 @@ function extractPreToolDecision(result) {
|
|
|
898
1062
|
return "passthrough";
|
|
899
1063
|
}
|
|
900
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
|
+
|
|
901
1165
|
// src/intercept/cursor-adapter.ts
|
|
902
1166
|
var ALLOW = { permission: "allow" };
|
|
903
1167
|
function toClaudeReadPayload(cursorPayload) {
|
|
@@ -941,7 +1205,9 @@ var ENGRAM_HOOK_EVENTS = [
|
|
|
941
1205
|
"PreToolUse",
|
|
942
1206
|
"PostToolUse",
|
|
943
1207
|
"SessionStart",
|
|
944
|
-
"UserPromptSubmit"
|
|
1208
|
+
"UserPromptSubmit",
|
|
1209
|
+
"PreCompact",
|
|
1210
|
+
"CwdChanged"
|
|
945
1211
|
];
|
|
946
1212
|
var ENGRAM_PRETOOL_MATCHER = "Read|Edit|Write|Bash";
|
|
947
1213
|
var DEFAULT_ENGRAM_COMMAND = "engram intercept";
|
|
@@ -969,6 +1235,14 @@ function buildEngramHookEntries(command = DEFAULT_ENGRAM_COMMAND, timeout = DEFA
|
|
|
969
1235
|
UserPromptSubmit: {
|
|
970
1236
|
// No matcher — UserPromptSubmit has no tool name.
|
|
971
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]
|
|
972
1246
|
}
|
|
973
1247
|
};
|
|
974
1248
|
}
|
|
@@ -1152,13 +1426,13 @@ function formatStatsSummary(summary) {
|
|
|
1152
1426
|
|
|
1153
1427
|
// src/intercept/memory-md.ts
|
|
1154
1428
|
import {
|
|
1155
|
-
existsSync as
|
|
1429
|
+
existsSync as existsSync6,
|
|
1156
1430
|
readFileSync as readFileSync3,
|
|
1157
1431
|
writeFileSync,
|
|
1158
1432
|
renameSync as renameSync2,
|
|
1159
|
-
statSync as
|
|
1433
|
+
statSync as statSync4
|
|
1160
1434
|
} from "fs";
|
|
1161
|
-
import { join as
|
|
1435
|
+
import { join as join6 } from "path";
|
|
1162
1436
|
var ENGRAM_MARKER_START = "<!-- engram:structural-facts:start -->";
|
|
1163
1437
|
var ENGRAM_MARKER_END = "<!-- engram:structural-facts:end -->";
|
|
1164
1438
|
var MAX_MEMORY_FILE_BYTES = 1e6;
|
|
@@ -1229,11 +1503,11 @@ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
|
|
|
1229
1503
|
if (engramSection.length > MAX_ENGRAM_SECTION_BYTES) {
|
|
1230
1504
|
return false;
|
|
1231
1505
|
}
|
|
1232
|
-
const memoryPath =
|
|
1506
|
+
const memoryPath = join6(projectRoot, "MEMORY.md");
|
|
1233
1507
|
try {
|
|
1234
1508
|
let existing = "";
|
|
1235
|
-
if (
|
|
1236
|
-
const st =
|
|
1509
|
+
if (existsSync6(memoryPath)) {
|
|
1510
|
+
const st = statSync4(memoryPath);
|
|
1237
1511
|
if (st.size > MAX_MEMORY_FILE_BYTES) {
|
|
1238
1512
|
return false;
|
|
1239
1513
|
}
|
|
@@ -1250,11 +1524,14 @@ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
|
|
|
1250
1524
|
}
|
|
1251
1525
|
|
|
1252
1526
|
// src/cli.ts
|
|
1253
|
-
import { basename as
|
|
1527
|
+
import { basename as basename4 } from "path";
|
|
1528
|
+
import { createRequire } from "module";
|
|
1529
|
+
var require2 = createRequire(import.meta.url);
|
|
1530
|
+
var { version: PKG_VERSION } = require2("../package.json");
|
|
1254
1531
|
var program = new Command();
|
|
1255
1532
|
program.name("engram").description(
|
|
1256
1533
|
"Context as infra for AI coding tools \u2014 hook-based Read/Edit interception + structural graph summaries"
|
|
1257
|
-
).version(
|
|
1534
|
+
).version(PKG_VERSION);
|
|
1258
1535
|
program.command("init").description("Scan codebase and build knowledge graph (zero LLM cost)").argument("[path]", "Project directory", ".").option(
|
|
1259
1536
|
"--with-skills [dir]",
|
|
1260
1537
|
"Also index Claude Code skills from ~/.claude/skills/ or a given path"
|
|
@@ -1287,9 +1564,9 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
|
|
|
1287
1564
|
console.log(chalk.green("\n\u2705 Ready. Your AI now has persistent memory."));
|
|
1288
1565
|
console.log(chalk.dim(" Graph stored in .engram/graph.db"));
|
|
1289
1566
|
const resolvedProject = pathResolve(projectPath);
|
|
1290
|
-
const localSettings =
|
|
1291
|
-
const projectSettings =
|
|
1292
|
-
const hasHooks =
|
|
1567
|
+
const localSettings = join7(resolvedProject, ".claude", "settings.local.json");
|
|
1568
|
+
const projectSettings = join7(resolvedProject, ".claude", "settings.json");
|
|
1569
|
+
const hasHooks = existsSync7(localSettings) && readFileSync4(localSettings, "utf-8").includes("engram intercept") || existsSync7(projectSettings) && readFileSync4(projectSettings, "utf-8").includes("engram intercept");
|
|
1293
1570
|
if (!hasHooks) {
|
|
1294
1571
|
console.log(
|
|
1295
1572
|
chalk.yellow("\n\u{1F4A1} Next step: ") + chalk.white("engram install-hook") + chalk.dim(
|
|
@@ -1303,6 +1580,32 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
|
|
|
1303
1580
|
);
|
|
1304
1581
|
}
|
|
1305
1582
|
});
|
|
1583
|
+
program.command("watch").description("Watch project for file changes and re-index incrementally").argument("[path]", "Project directory", ".").action(async (projectPath) => {
|
|
1584
|
+
const resolvedPath = pathResolve(projectPath);
|
|
1585
|
+
console.log(
|
|
1586
|
+
chalk.dim("\u{1F441} Watching ") + chalk.white(resolvedPath) + chalk.dim(" for changes...")
|
|
1587
|
+
);
|
|
1588
|
+
const controller = watchProject(resolvedPath, {
|
|
1589
|
+
onReindex: (filePath, nodeCount) => {
|
|
1590
|
+
console.log(
|
|
1591
|
+
chalk.green(" \u21BB ") + chalk.white(filePath) + chalk.dim(` (${nodeCount} nodes)`)
|
|
1592
|
+
);
|
|
1593
|
+
},
|
|
1594
|
+
onError: (err) => {
|
|
1595
|
+
console.error(chalk.red(" \u2717 ") + err.message);
|
|
1596
|
+
},
|
|
1597
|
+
onReady: () => {
|
|
1598
|
+
console.log(chalk.green(" \u2713 Watcher active.") + chalk.dim(" Press Ctrl+C to stop."));
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
process.on("SIGINT", () => {
|
|
1602
|
+
controller.abort();
|
|
1603
|
+
console.log(chalk.dim("\n Watcher stopped."));
|
|
1604
|
+
process.exit(0);
|
|
1605
|
+
});
|
|
1606
|
+
await new Promise(() => {
|
|
1607
|
+
});
|
|
1608
|
+
});
|
|
1306
1609
|
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) => {
|
|
1307
1610
|
const result = await query(opts.project, question, {
|
|
1308
1611
|
mode: opts.dfs ? "dfs" : "bfs",
|
|
@@ -1428,11 +1731,11 @@ function resolveSettingsPath(scope, projectPath) {
|
|
|
1428
1731
|
const absProject = pathResolve(projectPath);
|
|
1429
1732
|
switch (scope) {
|
|
1430
1733
|
case "local":
|
|
1431
|
-
return
|
|
1734
|
+
return join7(absProject, ".claude", "settings.local.json");
|
|
1432
1735
|
case "project":
|
|
1433
|
-
return
|
|
1736
|
+
return join7(absProject, ".claude", "settings.json");
|
|
1434
1737
|
case "user":
|
|
1435
|
-
return
|
|
1738
|
+
return join7(homedir(), ".claude", "settings.json");
|
|
1436
1739
|
default:
|
|
1437
1740
|
return null;
|
|
1438
1741
|
}
|
|
@@ -1526,7 +1829,7 @@ program.command("install-hook").description("Install engram hook entries into Cl
|
|
|
1526
1829
|
process.exit(1);
|
|
1527
1830
|
}
|
|
1528
1831
|
let existing = {};
|
|
1529
|
-
if (
|
|
1832
|
+
if (existsSync7(settingsPath)) {
|
|
1530
1833
|
try {
|
|
1531
1834
|
const raw = readFileSync4(settingsPath, "utf-8");
|
|
1532
1835
|
existing = raw.trim() ? JSON.parse(raw) : {};
|
|
@@ -1574,7 +1877,7 @@ program.command("install-hook").description("Install engram hook entries into Cl
|
|
|
1574
1877
|
}
|
|
1575
1878
|
try {
|
|
1576
1879
|
mkdirSync(dirname3(settingsPath), { recursive: true });
|
|
1577
|
-
if (
|
|
1880
|
+
if (existsSync7(settingsPath)) {
|
|
1578
1881
|
const backupPath = `${settingsPath}.engram-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
|
|
1579
1882
|
copyFileSync(settingsPath, backupPath);
|
|
1580
1883
|
console.log(chalk.dim(` Backup: ${backupPath}`));
|
|
@@ -1618,7 +1921,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
|
|
|
1618
1921
|
console.error(chalk.red(`Unknown scope: ${opts.scope}`));
|
|
1619
1922
|
process.exit(1);
|
|
1620
1923
|
}
|
|
1621
|
-
if (!
|
|
1924
|
+
if (!existsSync7(settingsPath)) {
|
|
1622
1925
|
console.log(
|
|
1623
1926
|
chalk.yellow(`No settings file at ${settingsPath} \u2014 nothing to remove.`)
|
|
1624
1927
|
);
|
|
@@ -1734,7 +2037,7 @@ program.command("hook-disable").description("Disable engram hooks via kill switc
|
|
|
1734
2037
|
console.error(chalk.dim("Run 'engram init' first."));
|
|
1735
2038
|
process.exit(1);
|
|
1736
2039
|
}
|
|
1737
|
-
const flagPath =
|
|
2040
|
+
const flagPath = join7(projectRoot, ".engram", "hook-disabled");
|
|
1738
2041
|
try {
|
|
1739
2042
|
writeFileSync2(flagPath, (/* @__PURE__ */ new Date()).toISOString());
|
|
1740
2043
|
console.log(
|
|
@@ -1758,8 +2061,8 @@ program.command("hook-enable").description("Re-enable engram hooks (remove kill
|
|
|
1758
2061
|
console.error(chalk.red(`Not an engram project: ${absProject}`));
|
|
1759
2062
|
process.exit(1);
|
|
1760
2063
|
}
|
|
1761
|
-
const flagPath =
|
|
1762
|
-
if (!
|
|
2064
|
+
const flagPath = join7(projectRoot, ".engram", "hook-disabled");
|
|
2065
|
+
if (!existsSync7(flagPath)) {
|
|
1763
2066
|
console.log(
|
|
1764
2067
|
chalk.yellow(`engram hooks already enabled for ${projectRoot}`)
|
|
1765
2068
|
);
|
|
@@ -1801,8 +2104,8 @@ program.command("memory-sync").description(
|
|
|
1801
2104
|
}
|
|
1802
2105
|
let branch = null;
|
|
1803
2106
|
try {
|
|
1804
|
-
const headPath =
|
|
1805
|
-
if (
|
|
2107
|
+
const headPath = join7(projectRoot, ".git", "HEAD");
|
|
2108
|
+
if (existsSync7(headPath)) {
|
|
1806
2109
|
const content = readFileSync4(headPath, "utf-8").trim();
|
|
1807
2110
|
const m = content.match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
1808
2111
|
if (m) branch = m[1];
|
|
@@ -1810,7 +2113,7 @@ program.command("memory-sync").description(
|
|
|
1810
2113
|
} catch {
|
|
1811
2114
|
}
|
|
1812
2115
|
const section = buildEngramSection({
|
|
1813
|
-
projectName:
|
|
2116
|
+
projectName: basename4(projectRoot),
|
|
1814
2117
|
branch,
|
|
1815
2118
|
stats: {
|
|
1816
2119
|
nodes: graphStats.nodes,
|
|
@@ -1829,7 +2132,7 @@ program.command("memory-sync").description(
|
|
|
1829
2132
|
\u{1F4DD} engram memory-sync`)
|
|
1830
2133
|
);
|
|
1831
2134
|
console.log(
|
|
1832
|
-
chalk.dim(` Target: ${
|
|
2135
|
+
chalk.dim(` Target: ${join7(projectRoot, "MEMORY.md")}`)
|
|
1833
2136
|
);
|
|
1834
2137
|
if (opts.dryRun) {
|
|
1835
2138
|
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.1",
|
|
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": {
|