engramx 0.3.2 → 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 +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 +335 -35
- 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,7 +1524,7 @@ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
|
|
|
1250
1524
|
}
|
|
1251
1525
|
|
|
1252
1526
|
// src/cli.ts
|
|
1253
|
-
import { basename as
|
|
1527
|
+
import { basename as basename4 } from "path";
|
|
1254
1528
|
var program = new Command();
|
|
1255
1529
|
program.name("engram").description(
|
|
1256
1530
|
"Context as infra for AI coding tools \u2014 hook-based Read/Edit interception + structural graph summaries"
|
|
@@ -1287,9 +1561,9 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
|
|
|
1287
1561
|
console.log(chalk.green("\n\u2705 Ready. Your AI now has persistent memory."));
|
|
1288
1562
|
console.log(chalk.dim(" Graph stored in .engram/graph.db"));
|
|
1289
1563
|
const resolvedProject = pathResolve(projectPath);
|
|
1290
|
-
const localSettings =
|
|
1291
|
-
const projectSettings =
|
|
1292
|
-
const hasHooks =
|
|
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");
|
|
1293
1567
|
if (!hasHooks) {
|
|
1294
1568
|
console.log(
|
|
1295
1569
|
chalk.yellow("\n\u{1F4A1} Next step: ") + chalk.white("engram install-hook") + chalk.dim(
|
|
@@ -1303,6 +1577,32 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
|
|
|
1303
1577
|
);
|
|
1304
1578
|
}
|
|
1305
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
|
+
});
|
|
1605
|
+
});
|
|
1306
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) => {
|
|
1307
1607
|
const result = await query(opts.project, question, {
|
|
1308
1608
|
mode: opts.dfs ? "dfs" : "bfs",
|
|
@@ -1428,11 +1728,11 @@ function resolveSettingsPath(scope, projectPath) {
|
|
|
1428
1728
|
const absProject = pathResolve(projectPath);
|
|
1429
1729
|
switch (scope) {
|
|
1430
1730
|
case "local":
|
|
1431
|
-
return
|
|
1731
|
+
return join7(absProject, ".claude", "settings.local.json");
|
|
1432
1732
|
case "project":
|
|
1433
|
-
return
|
|
1733
|
+
return join7(absProject, ".claude", "settings.json");
|
|
1434
1734
|
case "user":
|
|
1435
|
-
return
|
|
1735
|
+
return join7(homedir(), ".claude", "settings.json");
|
|
1436
1736
|
default:
|
|
1437
1737
|
return null;
|
|
1438
1738
|
}
|
|
@@ -1526,7 +1826,7 @@ program.command("install-hook").description("Install engram hook entries into Cl
|
|
|
1526
1826
|
process.exit(1);
|
|
1527
1827
|
}
|
|
1528
1828
|
let existing = {};
|
|
1529
|
-
if (
|
|
1829
|
+
if (existsSync7(settingsPath)) {
|
|
1530
1830
|
try {
|
|
1531
1831
|
const raw = readFileSync4(settingsPath, "utf-8");
|
|
1532
1832
|
existing = raw.trim() ? JSON.parse(raw) : {};
|
|
@@ -1574,7 +1874,7 @@ program.command("install-hook").description("Install engram hook entries into Cl
|
|
|
1574
1874
|
}
|
|
1575
1875
|
try {
|
|
1576
1876
|
mkdirSync(dirname3(settingsPath), { recursive: true });
|
|
1577
|
-
if (
|
|
1877
|
+
if (existsSync7(settingsPath)) {
|
|
1578
1878
|
const backupPath = `${settingsPath}.engram-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
|
|
1579
1879
|
copyFileSync(settingsPath, backupPath);
|
|
1580
1880
|
console.log(chalk.dim(` Backup: ${backupPath}`));
|
|
@@ -1618,7 +1918,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
|
|
|
1618
1918
|
console.error(chalk.red(`Unknown scope: ${opts.scope}`));
|
|
1619
1919
|
process.exit(1);
|
|
1620
1920
|
}
|
|
1621
|
-
if (!
|
|
1921
|
+
if (!existsSync7(settingsPath)) {
|
|
1622
1922
|
console.log(
|
|
1623
1923
|
chalk.yellow(`No settings file at ${settingsPath} \u2014 nothing to remove.`)
|
|
1624
1924
|
);
|
|
@@ -1734,7 +2034,7 @@ program.command("hook-disable").description("Disable engram hooks via kill switc
|
|
|
1734
2034
|
console.error(chalk.dim("Run 'engram init' first."));
|
|
1735
2035
|
process.exit(1);
|
|
1736
2036
|
}
|
|
1737
|
-
const flagPath =
|
|
2037
|
+
const flagPath = join7(projectRoot, ".engram", "hook-disabled");
|
|
1738
2038
|
try {
|
|
1739
2039
|
writeFileSync2(flagPath, (/* @__PURE__ */ new Date()).toISOString());
|
|
1740
2040
|
console.log(
|
|
@@ -1758,8 +2058,8 @@ program.command("hook-enable").description("Re-enable engram hooks (remove kill
|
|
|
1758
2058
|
console.error(chalk.red(`Not an engram project: ${absProject}`));
|
|
1759
2059
|
process.exit(1);
|
|
1760
2060
|
}
|
|
1761
|
-
const flagPath =
|
|
1762
|
-
if (!
|
|
2061
|
+
const flagPath = join7(projectRoot, ".engram", "hook-disabled");
|
|
2062
|
+
if (!existsSync7(flagPath)) {
|
|
1763
2063
|
console.log(
|
|
1764
2064
|
chalk.yellow(`engram hooks already enabled for ${projectRoot}`)
|
|
1765
2065
|
);
|
|
@@ -1801,8 +2101,8 @@ program.command("memory-sync").description(
|
|
|
1801
2101
|
}
|
|
1802
2102
|
let branch = null;
|
|
1803
2103
|
try {
|
|
1804
|
-
const headPath =
|
|
1805
|
-
if (
|
|
2104
|
+
const headPath = join7(projectRoot, ".git", "HEAD");
|
|
2105
|
+
if (existsSync7(headPath)) {
|
|
1806
2106
|
const content = readFileSync4(headPath, "utf-8").trim();
|
|
1807
2107
|
const m = content.match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
1808
2108
|
if (m) branch = m[1];
|
|
@@ -1810,7 +2110,7 @@ program.command("memory-sync").description(
|
|
|
1810
2110
|
} catch {
|
|
1811
2111
|
}
|
|
1812
2112
|
const section = buildEngramSection({
|
|
1813
|
-
projectName:
|
|
2113
|
+
projectName: basename4(projectRoot),
|
|
1814
2114
|
branch,
|
|
1815
2115
|
stats: {
|
|
1816
2116
|
nodes: graphStats.nodes,
|
|
@@ -1829,7 +2129,7 @@ program.command("memory-sync").description(
|
|
|
1829
2129
|
\u{1F4DD} engram memory-sync`)
|
|
1830
2130
|
);
|
|
1831
2131
|
console.log(
|
|
1832
|
-
chalk.dim(` Target: ${
|
|
2132
|
+
chalk.dim(` Target: ${join7(projectRoot, "MEMORY.md")}`)
|
|
1833
2133
|
);
|
|
1834
2134
|
if (opts.dryRun) {
|
|
1835
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": {
|