engramx 2.0.2 → 3.0.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/CHANGELOG.md +271 -0
- package/README.md +161 -17
- package/dist/{aider-context-BC5R2ZTA.js → aider-context-6IDE3R7U.js} +1 -1
- package/dist/check-2Z3MPZEJ.js +12 -0
- package/dist/{chunk-PEH54LYC.js → chunk-645NBY6L.js} +42 -5
- package/dist/chunk-73IBCRFI.js +215 -0
- package/dist/{chunk-SJT7VS2G.js → chunk-B4UOE64J.js} +46 -11
- package/dist/chunk-FKY6HIT2.js +99 -0
- package/dist/{chunk-533LR4I7.js → chunk-G4U3QOOW.js} +13 -97
- package/dist/chunk-RJC6RNXJ.js +1405 -0
- package/dist/chunk-RM2TBOVW.js +121 -0
- package/dist/chunk-SMU4WR3D.js +187 -0
- package/dist/{chunk-C6GBUOAL.js → chunk-VLTWBTQ7.js} +14 -15
- package/dist/chunk-XVYE4OX2.js +232 -0
- package/dist/chunk-ZUC6OXSL.js +178 -0
- package/dist/cli.js +818 -1533
- package/dist/{core-6IY5L6II.js → core-77F2BVYV.js} +2 -2
- package/dist/{cursor-mdc-GJ7E5LDD.js → cursor-mdc-EEO7PYZ3.js} +1 -1
- package/dist/{exporter-GWU2GF23.js → exporter-ZYJ4WM2F.js} +1 -1
- package/dist/{importer-V62NGZRK.js → importer-4UWQDH4W.js} +1 -1
- package/dist/index.js +3 -3
- package/dist/install-YVMVCFQW.js +121 -0
- package/dist/mcp-client-ROOJF76V.js +9 -0
- package/dist/mcp-config-QD4NPVXB.js +12 -0
- package/dist/{migrate-UKCO6BUU.js → migrate-KJ5K5NWO.js} +1 -1
- package/dist/notify-5POGKMRX.js +36 -0
- package/dist/{plugin-loader-STTGYIL5.js → plugin-loader-SQQB6V74.js} +69 -23
- package/dist/report-C3GTM3HY.js +12 -0
- package/dist/resolver-H7GXVP73.js +21 -0
- package/dist/serve.js +5 -4
- package/dist/{server-KUG7U6SG.js → server-2ZQKXJ5M.js} +74 -4
- package/dist/{windsurf-rules-C7SVDHBL.js → windsurf-rules-XF7MYF6J.js} +1 -1
- package/dist/wizard-UH27IO4I.js +274 -0
- package/package.json +3 -2
- package/dist/{tuner-KFNNGKG3.js → tuner-Y2YENAZC.js} +3 -3
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/update/check.ts
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
var REGISTRY_URL = "https://registry.npmjs.org/engramx/latest";
|
|
6
|
+
var CHECK_INTERVAL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
7
|
+
var FETCH_TIMEOUT_MS = 1500;
|
|
8
|
+
function cachePath() {
|
|
9
|
+
return join(homedir(), ".engram", "last-update-check");
|
|
10
|
+
}
|
|
11
|
+
function readCache() {
|
|
12
|
+
const path = cachePath();
|
|
13
|
+
if (!existsSync(path)) return null;
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
16
|
+
if (typeof parsed?.latest === "string" && typeof parsed?.checkedAt === "number") {
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function writeCache(entry) {
|
|
25
|
+
try {
|
|
26
|
+
const path = cachePath();
|
|
27
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
28
|
+
writeFileSync(path, JSON.stringify(entry), "utf-8");
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function isNewer(a, b) {
|
|
33
|
+
const parse = (v) => {
|
|
34
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(v.trim());
|
|
35
|
+
if (!m) return null;
|
|
36
|
+
return {
|
|
37
|
+
major: Number(m[1]),
|
|
38
|
+
minor: Number(m[2]),
|
|
39
|
+
patch: Number(m[3]),
|
|
40
|
+
pre: m[4] ?? null
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const pa = parse(a);
|
|
44
|
+
const pb = parse(b);
|
|
45
|
+
if (!pa || !pb) return false;
|
|
46
|
+
if (pa.major !== pb.major) return pa.major > pb.major;
|
|
47
|
+
if (pa.minor !== pb.minor) return pa.minor > pb.minor;
|
|
48
|
+
if (pa.patch !== pb.patch) return pa.patch > pb.patch;
|
|
49
|
+
if (pa.pre === null && pb.pre !== null) return true;
|
|
50
|
+
if (pa.pre !== null && pb.pre === null) return false;
|
|
51
|
+
if (pa.pre === null && pb.pre === null) return false;
|
|
52
|
+
return (pa.pre ?? "") > (pb.pre ?? "");
|
|
53
|
+
}
|
|
54
|
+
function optedOut() {
|
|
55
|
+
if (process.env.ENGRAM_NO_UPDATE_CHECK === "1") return true;
|
|
56
|
+
if (process.env.CI) return true;
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
async function fetchLatestFromRegistry() {
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(REGISTRY_URL, {
|
|
64
|
+
signal: controller.signal,
|
|
65
|
+
headers: { accept: "application/json" }
|
|
66
|
+
});
|
|
67
|
+
if (!res.ok) return null;
|
|
68
|
+
const body = await res.json();
|
|
69
|
+
if (typeof body?.version !== "string") return null;
|
|
70
|
+
return body.version;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
} finally {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function checkForUpdate(currentVersion, opts = {}) {
|
|
78
|
+
const base = {
|
|
79
|
+
skipped: false,
|
|
80
|
+
current: currentVersion,
|
|
81
|
+
latest: null,
|
|
82
|
+
updateAvailable: false,
|
|
83
|
+
checkedAt: null,
|
|
84
|
+
fromCache: false
|
|
85
|
+
};
|
|
86
|
+
if (!opts.force && optedOut()) {
|
|
87
|
+
return { ...base, skipped: true };
|
|
88
|
+
}
|
|
89
|
+
if (!opts.force) {
|
|
90
|
+
const cached = readCache();
|
|
91
|
+
if (cached && Date.now() - cached.checkedAt < CHECK_INTERVAL_MS) {
|
|
92
|
+
return {
|
|
93
|
+
...base,
|
|
94
|
+
latest: cached.latest,
|
|
95
|
+
updateAvailable: isNewer(cached.latest, currentVersion),
|
|
96
|
+
checkedAt: cached.checkedAt,
|
|
97
|
+
fromCache: true
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const latest = await fetchLatestFromRegistry();
|
|
102
|
+
if (!latest) {
|
|
103
|
+
return { ...base, skipped: !opts.force };
|
|
104
|
+
}
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
writeCache({ latest, checkedAt: now });
|
|
107
|
+
return {
|
|
108
|
+
...base,
|
|
109
|
+
latest,
|
|
110
|
+
updateAvailable: isNewer(latest, currentVersion),
|
|
111
|
+
checkedAt: now,
|
|
112
|
+
fromCache: false
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export {
|
|
117
|
+
cachePath,
|
|
118
|
+
isNewer,
|
|
119
|
+
optedOut,
|
|
120
|
+
checkForUpdate
|
|
121
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// src/intercept/installer.ts
|
|
2
|
+
var ENGRAM_HOOK_EVENTS = [
|
|
3
|
+
"PreToolUse",
|
|
4
|
+
"PostToolUse",
|
|
5
|
+
"SessionStart",
|
|
6
|
+
"UserPromptSubmit",
|
|
7
|
+
"PreCompact",
|
|
8
|
+
"CwdChanged"
|
|
9
|
+
];
|
|
10
|
+
var ENGRAM_PRETOOL_MATCHER = "Read|Edit|Write|Bash";
|
|
11
|
+
var DEFAULT_ENGRAM_COMMAND = "engram intercept";
|
|
12
|
+
var ENGRAM_REINDEX_HOOK_MATCHER = "Edit|Write|MultiEdit";
|
|
13
|
+
var DEFAULT_ENGRAM_REINDEX_HOOK_COMMAND = "engram reindex-hook";
|
|
14
|
+
var DEFAULT_HOOK_TIMEOUT_SEC = 5;
|
|
15
|
+
var DEFAULT_STATUSLINE_COMMAND = "engram hud-label";
|
|
16
|
+
function buildEngramHookEntries(command = DEFAULT_ENGRAM_COMMAND, timeout = DEFAULT_HOOK_TIMEOUT_SEC) {
|
|
17
|
+
const baseCmd = {
|
|
18
|
+
type: "command",
|
|
19
|
+
command,
|
|
20
|
+
timeout
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
PreToolUse: {
|
|
24
|
+
matcher: ENGRAM_PRETOOL_MATCHER,
|
|
25
|
+
hooks: [baseCmd]
|
|
26
|
+
},
|
|
27
|
+
PostToolUse: {
|
|
28
|
+
// Match all tools — PostToolUse is an observer for any completion.
|
|
29
|
+
matcher: ".*",
|
|
30
|
+
hooks: [baseCmd]
|
|
31
|
+
},
|
|
32
|
+
SessionStart: {
|
|
33
|
+
// No matcher — SessionStart has no tool name.
|
|
34
|
+
hooks: [baseCmd]
|
|
35
|
+
},
|
|
36
|
+
UserPromptSubmit: {
|
|
37
|
+
// No matcher — UserPromptSubmit has no tool name.
|
|
38
|
+
hooks: [baseCmd]
|
|
39
|
+
},
|
|
40
|
+
PreCompact: {
|
|
41
|
+
// No matcher — PreCompact has no tool name.
|
|
42
|
+
hooks: [baseCmd]
|
|
43
|
+
},
|
|
44
|
+
CwdChanged: {
|
|
45
|
+
// No matcher — CwdChanged has no tool name.
|
|
46
|
+
hooks: [baseCmd]
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function buildReindexHookEntry(command = DEFAULT_ENGRAM_REINDEX_HOOK_COMMAND, timeout = DEFAULT_HOOK_TIMEOUT_SEC) {
|
|
51
|
+
return {
|
|
52
|
+
matcher: ENGRAM_REINDEX_HOOK_MATCHER,
|
|
53
|
+
hooks: [{ type: "command", command, timeout }]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function isEngramHookEntry(entry) {
|
|
57
|
+
if (entry === null || typeof entry !== "object") return false;
|
|
58
|
+
const e = entry;
|
|
59
|
+
if (!Array.isArray(e.hooks)) return false;
|
|
60
|
+
for (const h of e.hooks) {
|
|
61
|
+
if (h === null || typeof h !== "object") continue;
|
|
62
|
+
const cmd = h.command;
|
|
63
|
+
if (typeof cmd !== "string") continue;
|
|
64
|
+
if (cmd.includes("engram intercept") || cmd.includes("engram reindex-hook")) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
function installEngramHooks(settings, command = DEFAULT_ENGRAM_COMMAND, options = {}) {
|
|
71
|
+
const entries = buildEngramHookEntries(command);
|
|
72
|
+
const added = [];
|
|
73
|
+
const alreadyPresent = [];
|
|
74
|
+
const hooksClone = {};
|
|
75
|
+
const existingHooks = settings.hooks ?? {};
|
|
76
|
+
for (const [key, value] of Object.entries(existingHooks)) {
|
|
77
|
+
if (Array.isArray(value)) {
|
|
78
|
+
hooksClone[key] = value.map((entry) => ({ ...entry }));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const event of ENGRAM_HOOK_EVENTS) {
|
|
82
|
+
const eventArr = hooksClone[event] ?? [];
|
|
83
|
+
const hasIntercept = eventArr.some(
|
|
84
|
+
(e) => entryContainsCommand(e, "engram intercept")
|
|
85
|
+
);
|
|
86
|
+
if (hasIntercept) {
|
|
87
|
+
alreadyPresent.push(event);
|
|
88
|
+
hooksClone[event] = eventArr;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
hooksClone[event] = [...eventArr, entries[event]];
|
|
92
|
+
added.push(event);
|
|
93
|
+
}
|
|
94
|
+
let autoReindexAdded = false;
|
|
95
|
+
if (options.autoReindex) {
|
|
96
|
+
const postToolArr = hooksClone.PostToolUse ?? [];
|
|
97
|
+
const hasReindexHook = postToolArr.some(
|
|
98
|
+
(e) => entryContainsCommand(e, "engram reindex-hook")
|
|
99
|
+
);
|
|
100
|
+
if (!hasReindexHook) {
|
|
101
|
+
hooksClone.PostToolUse = [...postToolArr, buildReindexHookEntry()];
|
|
102
|
+
autoReindexAdded = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const hasStatusLine = settings.statusLine && typeof settings.statusLine === "object" && typeof settings.statusLine.command === "string" && settings.statusLine.command.length > 0;
|
|
106
|
+
const statusLineAdded = !hasStatusLine;
|
|
107
|
+
const statusLine = hasStatusLine ? settings.statusLine : { type: "command", command: DEFAULT_STATUSLINE_COMMAND };
|
|
108
|
+
return {
|
|
109
|
+
updated: { ...settings, hooks: hooksClone, statusLine },
|
|
110
|
+
added,
|
|
111
|
+
alreadyPresent,
|
|
112
|
+
statusLineAdded,
|
|
113
|
+
autoReindexAdded
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function entryContainsCommand(entry, substring) {
|
|
117
|
+
if (!Array.isArray(entry.hooks)) return false;
|
|
118
|
+
for (const h of entry.hooks) {
|
|
119
|
+
if (h === null || typeof h !== "object") continue;
|
|
120
|
+
const cmd = h.command;
|
|
121
|
+
if (typeof cmd === "string" && cmd.includes(substring)) return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
function uninstallEngramHooks(settings) {
|
|
126
|
+
const removed = [];
|
|
127
|
+
const existingHooks = settings.hooks ?? {};
|
|
128
|
+
const hooksClone = {};
|
|
129
|
+
for (const [event, arr] of Object.entries(existingHooks)) {
|
|
130
|
+
if (!Array.isArray(arr)) continue;
|
|
131
|
+
const filtered = arr.filter((entry) => !isEngramHookEntry(entry));
|
|
132
|
+
if (filtered.length !== arr.length && isKnownEngramEvent(event)) {
|
|
133
|
+
removed.push(event);
|
|
134
|
+
}
|
|
135
|
+
if (filtered.length > 0) {
|
|
136
|
+
hooksClone[event] = filtered;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const updatedSettings = { ...settings };
|
|
140
|
+
if (Object.keys(hooksClone).length === 0) {
|
|
141
|
+
delete updatedSettings.hooks;
|
|
142
|
+
} else {
|
|
143
|
+
updatedSettings.hooks = hooksClone;
|
|
144
|
+
}
|
|
145
|
+
const statusLineRemoved = typeof updatedSettings.statusLine?.command === "string" && updatedSettings.statusLine.command.includes("engram hud-label");
|
|
146
|
+
if (statusLineRemoved) {
|
|
147
|
+
delete updatedSettings.statusLine;
|
|
148
|
+
}
|
|
149
|
+
return { updated: updatedSettings, removed, statusLineRemoved };
|
|
150
|
+
}
|
|
151
|
+
function isKnownEngramEvent(event) {
|
|
152
|
+
return ENGRAM_HOOK_EVENTS.includes(event);
|
|
153
|
+
}
|
|
154
|
+
function formatInstallDiff(before, after) {
|
|
155
|
+
const lines = [];
|
|
156
|
+
const beforeHooks = before.hooks ?? {};
|
|
157
|
+
const afterHooks = after.hooks ?? {};
|
|
158
|
+
for (const event of ENGRAM_HOOK_EVENTS) {
|
|
159
|
+
const beforeArr = beforeHooks[event] ?? [];
|
|
160
|
+
const afterArr = afterHooks[event] ?? [];
|
|
161
|
+
if (beforeArr.length === afterArr.length) continue;
|
|
162
|
+
lines.push(`+ ${event}: ${beforeArr.length} \u2192 ${afterArr.length} entries`);
|
|
163
|
+
const added = afterArr.filter((entry) => isEngramHookEntry(entry));
|
|
164
|
+
const beforeHasEngram = beforeArr.some((entry) => isEngramHookEntry(entry));
|
|
165
|
+
if (!beforeHasEngram && added.length > 0) {
|
|
166
|
+
for (const entry of added) {
|
|
167
|
+
const matcher = entry.matcher ? ` matcher=${JSON.stringify(entry.matcher)}` : "";
|
|
168
|
+
const cmds = entry.hooks.map((h) => h.command).join(", ");
|
|
169
|
+
lines.push(` + {${matcher} command="${cmds}"}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const hadStatusLine = before.statusLine?.command;
|
|
174
|
+
const hasStatusLineNow = after.statusLine?.command;
|
|
175
|
+
if (!hadStatusLine && hasStatusLineNow?.includes("engram hud-label")) {
|
|
176
|
+
lines.push(`+ statusLine: engram hud-label (HUD enabled)`);
|
|
177
|
+
} else if (hadStatusLine?.includes("engram hud-label") && !hasStatusLineNow) {
|
|
178
|
+
lines.push(`- statusLine: engram hud-label (HUD removed)`);
|
|
179
|
+
}
|
|
180
|
+
return lines.length > 0 ? lines.join("\n") : "(no changes)";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export {
|
|
184
|
+
installEngramHooks,
|
|
185
|
+
uninstallEngramHooks,
|
|
186
|
+
formatInstallDiff
|
|
187
|
+
};
|
|
@@ -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-77F2BVYV.js");
|
|
314
314
|
const store = await getStore(projectRoot);
|
|
315
315
|
try {
|
|
316
316
|
let view = VIEWS.general;
|
|
@@ -326,26 +326,25 @@ async function autogen(projectRoot, target, task) {
|
|
|
326
326
|
}
|
|
327
327
|
const summary = generateSummary(store, view);
|
|
328
328
|
const stats = store.getStats();
|
|
329
|
-
|
|
329
|
+
const targetFiles = [];
|
|
330
330
|
if (target === "claude") {
|
|
331
|
-
|
|
331
|
+
targetFiles.push(join2(projectRoot, "CLAUDE.md"));
|
|
332
332
|
} else if (target === "cursor") {
|
|
333
|
-
|
|
333
|
+
targetFiles.push(join2(projectRoot, ".cursorrules"));
|
|
334
334
|
} else if (target === "agents") {
|
|
335
|
-
|
|
335
|
+
targetFiles.push(join2(projectRoot, "AGENTS.md"));
|
|
336
336
|
} else {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
targetFile = join2(projectRoot, "AGENTS.md");
|
|
343
|
-
} else {
|
|
344
|
-
targetFile = join2(projectRoot, "CLAUDE.md");
|
|
337
|
+
targetFiles.push(join2(projectRoot, "CLAUDE.md"));
|
|
338
|
+
targetFiles.push(join2(projectRoot, "AGENTS.md"));
|
|
339
|
+
const cursorRules = join2(projectRoot, ".cursorrules");
|
|
340
|
+
if (existsSync2(cursorRules)) {
|
|
341
|
+
targetFiles.push(cursorRules);
|
|
345
342
|
}
|
|
346
343
|
}
|
|
347
|
-
|
|
348
|
-
|
|
344
|
+
for (const f of targetFiles) {
|
|
345
|
+
writeToFile(f, summary);
|
|
346
|
+
}
|
|
347
|
+
return { files: targetFiles, nodesIncluded: stats.nodes, view: view.name };
|
|
349
348
|
} finally {
|
|
350
349
|
store.close();
|
|
351
350
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cachePath,
|
|
3
|
+
isNewer
|
|
4
|
+
} from "./chunk-RM2TBOVW.js";
|
|
5
|
+
import {
|
|
6
|
+
refreshComponentStatus
|
|
7
|
+
} from "./chunk-G4U3QOOW.js";
|
|
8
|
+
|
|
9
|
+
// src/doctor/report.ts
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { homedir, platform, release } from "os";
|
|
14
|
+
function checkGraphDb(projectRoot) {
|
|
15
|
+
const path = join(projectRoot, ".engram", "graph.db");
|
|
16
|
+
if (!existsSync(path)) {
|
|
17
|
+
return {
|
|
18
|
+
name: "graph",
|
|
19
|
+
severity: "fail",
|
|
20
|
+
detail: "No graph at .engram/graph.db",
|
|
21
|
+
remediation: "Run `engram init` (or `engram setup` for the wizard)."
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const size = statSync(path).size;
|
|
26
|
+
const sizeMb = (size / 1024 / 1024).toFixed(2);
|
|
27
|
+
return {
|
|
28
|
+
name: "graph",
|
|
29
|
+
severity: "ok",
|
|
30
|
+
detail: `.engram/graph.db present (${sizeMb} MB)`
|
|
31
|
+
};
|
|
32
|
+
} catch {
|
|
33
|
+
return {
|
|
34
|
+
name: "graph",
|
|
35
|
+
severity: "warn",
|
|
36
|
+
detail: "graph.db exists but stat() failed",
|
|
37
|
+
remediation: "Check file permissions on .engram/graph.db"
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function checkHook(projectRoot) {
|
|
42
|
+
const candidates = [
|
|
43
|
+
join(projectRoot, ".claude", "settings.local.json"),
|
|
44
|
+
join(projectRoot, ".claude", "settings.json"),
|
|
45
|
+
join(homedir(), ".claude", "settings.json")
|
|
46
|
+
];
|
|
47
|
+
for (const path of candidates) {
|
|
48
|
+
if (!existsSync(path)) continue;
|
|
49
|
+
try {
|
|
50
|
+
const content = readFileSync(path, "utf-8");
|
|
51
|
+
if (content.includes("engram intercept")) {
|
|
52
|
+
return {
|
|
53
|
+
name: "hook",
|
|
54
|
+
severity: "ok",
|
|
55
|
+
detail: `Sentinel hook active (via ${path.replace(homedir(), "~")})`
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
name: "hook",
|
|
63
|
+
severity: "warn",
|
|
64
|
+
detail: "Sentinel hook not found in any .claude/settings*.json",
|
|
65
|
+
remediation: "Run `engram install-hook` to enable automatic Read interception."
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function componentToCheck(c) {
|
|
69
|
+
if (c.available) {
|
|
70
|
+
return {
|
|
71
|
+
name: c.name,
|
|
72
|
+
severity: "ok",
|
|
73
|
+
detail: `${c.name.toUpperCase()} provider reachable`
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const remediationByName = {
|
|
77
|
+
http: "Run `engram server --http` to start the local API.",
|
|
78
|
+
lsp: "LSP is best-effort \u2014 install a language server (typescript-language-server, pyright, rust-analyzer).",
|
|
79
|
+
ast: "Tree-sitter grammars missing. Reinstall engram: `engram update` or `npm install -g engramx@latest`."
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
name: c.name,
|
|
83
|
+
severity: c.name === "ast" ? "fail" : "warn",
|
|
84
|
+
detail: `${c.name.toUpperCase()} provider unavailable`,
|
|
85
|
+
remediation: remediationByName[c.name]
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function checkVersion(engramVersion) {
|
|
89
|
+
try {
|
|
90
|
+
const path = cachePath();
|
|
91
|
+
if (!existsSync(path)) {
|
|
92
|
+
return {
|
|
93
|
+
name: "version",
|
|
94
|
+
severity: "ok",
|
|
95
|
+
detail: `engram v${engramVersion} (no update check cached yet)`
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const cached = JSON.parse(readFileSync(path, "utf-8"));
|
|
99
|
+
if (typeof cached?.latest === "string" && cached.latest !== engramVersion && isNewer(cached.latest, engramVersion)) {
|
|
100
|
+
return {
|
|
101
|
+
name: "version",
|
|
102
|
+
severity: "warn",
|
|
103
|
+
detail: `engram v${engramVersion} \u2014 v${cached.latest} is available`,
|
|
104
|
+
remediation: "Run `engram update` to upgrade."
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
name: "version",
|
|
109
|
+
severity: "ok",
|
|
110
|
+
detail: `engram v${engramVersion} (latest)`
|
|
111
|
+
};
|
|
112
|
+
} catch {
|
|
113
|
+
return {
|
|
114
|
+
name: "version",
|
|
115
|
+
severity: "ok",
|
|
116
|
+
detail: `engram v${engramVersion}`
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function checkIdes(ideCount) {
|
|
121
|
+
if (ideCount === 0) {
|
|
122
|
+
return {
|
|
123
|
+
name: "ides",
|
|
124
|
+
severity: "warn",
|
|
125
|
+
detail: "No IDE adapters detected",
|
|
126
|
+
remediation: "Run `engram gen-mdc` (Cursor), `gen-windsurfrules` (Windsurf), or `gen-aider` (Aider) to add IDE adapters."
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
name: "ides",
|
|
131
|
+
severity: "ok",
|
|
132
|
+
detail: `${ideCount} IDE adapter${ideCount > 1 ? "s" : ""} configured`
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function aggregate(checks) {
|
|
136
|
+
if (checks.some((c) => c.severity === "fail")) return "fail";
|
|
137
|
+
if (checks.some((c) => c.severity === "warn")) return "warn";
|
|
138
|
+
return "ok";
|
|
139
|
+
}
|
|
140
|
+
function buildReport(projectRoot, engramVersion) {
|
|
141
|
+
const components = refreshComponentStatus(projectRoot);
|
|
142
|
+
const checks = [
|
|
143
|
+
checkVersion(engramVersion),
|
|
144
|
+
checkGraphDb(projectRoot),
|
|
145
|
+
checkHook(projectRoot),
|
|
146
|
+
...components.components.map(componentToCheck),
|
|
147
|
+
checkIdes(components.ideCount)
|
|
148
|
+
];
|
|
149
|
+
return {
|
|
150
|
+
projectRoot,
|
|
151
|
+
engramVersion,
|
|
152
|
+
nodeVersion: process.version,
|
|
153
|
+
os: `${platform()} ${release()}`,
|
|
154
|
+
checks,
|
|
155
|
+
overallSeverity: aggregate(checks),
|
|
156
|
+
generatedAt: Date.now()
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function icon(sev) {
|
|
160
|
+
switch (sev) {
|
|
161
|
+
case "ok":
|
|
162
|
+
return chalk.green("\u2713");
|
|
163
|
+
case "warn":
|
|
164
|
+
return chalk.yellow("\u26A0");
|
|
165
|
+
case "fail":
|
|
166
|
+
return chalk.red("\u2717");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function formatReport(report, verbose) {
|
|
170
|
+
const lines = [];
|
|
171
|
+
lines.push("");
|
|
172
|
+
lines.push(chalk.bold(`\u{1FA7A} engram doctor \u2014 ${report.projectRoot}`));
|
|
173
|
+
lines.push(
|
|
174
|
+
chalk.dim(
|
|
175
|
+
` engram v${report.engramVersion} \xB7 Node ${report.nodeVersion} \xB7 ${report.os}`
|
|
176
|
+
)
|
|
177
|
+
);
|
|
178
|
+
lines.push("");
|
|
179
|
+
for (const c of report.checks) {
|
|
180
|
+
lines.push(` ${icon(c.severity)} ${chalk.bold(c.name.padEnd(8))} ${c.detail}`);
|
|
181
|
+
if (verbose && c.remediation && c.severity !== "ok") {
|
|
182
|
+
lines.push(` ${chalk.dim("\u2192 " + c.remediation)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
lines.push("");
|
|
186
|
+
switch (report.overallSeverity) {
|
|
187
|
+
case "ok":
|
|
188
|
+
lines.push(chalk.green(" All systems green."));
|
|
189
|
+
break;
|
|
190
|
+
case "warn":
|
|
191
|
+
lines.push(
|
|
192
|
+
chalk.yellow(
|
|
193
|
+
" Working, with warnings. Run `engram doctor --verbose` for remediation."
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
break;
|
|
197
|
+
case "fail":
|
|
198
|
+
lines.push(
|
|
199
|
+
chalk.red(
|
|
200
|
+
" Critical components missing. Run `engram doctor --verbose` for fixes."
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
lines.push("");
|
|
206
|
+
return lines.join("\n");
|
|
207
|
+
}
|
|
208
|
+
function exportReport(report) {
|
|
209
|
+
return JSON.stringify(
|
|
210
|
+
{
|
|
211
|
+
engramVersion: report.engramVersion,
|
|
212
|
+
nodeVersion: report.nodeVersion,
|
|
213
|
+
os: report.os,
|
|
214
|
+
overallSeverity: report.overallSeverity,
|
|
215
|
+
checks: report.checks.map((c) => ({
|
|
216
|
+
name: c.name,
|
|
217
|
+
severity: c.severity,
|
|
218
|
+
detail: c.detail
|
|
219
|
+
})),
|
|
220
|
+
generatedAt: new Date(report.generatedAt).toISOString()
|
|
221
|
+
// NOTE: projectRoot intentionally omitted — can contain usernames.
|
|
222
|
+
},
|
|
223
|
+
null,
|
|
224
|
+
2
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export {
|
|
229
|
+
buildReport,
|
|
230
|
+
formatReport,
|
|
231
|
+
exportReport
|
|
232
|
+
};
|