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.
Files changed (35) hide show
  1. package/CHANGELOG.md +271 -0
  2. package/README.md +161 -17
  3. package/dist/{aider-context-BC5R2ZTA.js → aider-context-6IDE3R7U.js} +1 -1
  4. package/dist/check-2Z3MPZEJ.js +12 -0
  5. package/dist/{chunk-PEH54LYC.js → chunk-645NBY6L.js} +42 -5
  6. package/dist/chunk-73IBCRFI.js +215 -0
  7. package/dist/{chunk-SJT7VS2G.js → chunk-B4UOE64J.js} +46 -11
  8. package/dist/chunk-FKY6HIT2.js +99 -0
  9. package/dist/{chunk-533LR4I7.js → chunk-G4U3QOOW.js} +13 -97
  10. package/dist/chunk-RJC6RNXJ.js +1405 -0
  11. package/dist/chunk-RM2TBOVW.js +121 -0
  12. package/dist/chunk-SMU4WR3D.js +187 -0
  13. package/dist/{chunk-C6GBUOAL.js → chunk-VLTWBTQ7.js} +14 -15
  14. package/dist/chunk-XVYE4OX2.js +232 -0
  15. package/dist/chunk-ZUC6OXSL.js +178 -0
  16. package/dist/cli.js +818 -1533
  17. package/dist/{core-6IY5L6II.js → core-77F2BVYV.js} +2 -2
  18. package/dist/{cursor-mdc-GJ7E5LDD.js → cursor-mdc-EEO7PYZ3.js} +1 -1
  19. package/dist/{exporter-GWU2GF23.js → exporter-ZYJ4WM2F.js} +1 -1
  20. package/dist/{importer-V62NGZRK.js → importer-4UWQDH4W.js} +1 -1
  21. package/dist/index.js +3 -3
  22. package/dist/install-YVMVCFQW.js +121 -0
  23. package/dist/mcp-client-ROOJF76V.js +9 -0
  24. package/dist/mcp-config-QD4NPVXB.js +12 -0
  25. package/dist/{migrate-UKCO6BUU.js → migrate-KJ5K5NWO.js} +1 -1
  26. package/dist/notify-5POGKMRX.js +36 -0
  27. package/dist/{plugin-loader-STTGYIL5.js → plugin-loader-SQQB6V74.js} +69 -23
  28. package/dist/report-C3GTM3HY.js +12 -0
  29. package/dist/resolver-H7GXVP73.js +21 -0
  30. package/dist/serve.js +5 -4
  31. package/dist/{server-KUG7U6SG.js → server-2ZQKXJ5M.js} +74 -4
  32. package/dist/{windsurf-rules-C7SVDHBL.js → windsurf-rules-XF7MYF6J.js} +1 -1
  33. package/dist/wizard-UH27IO4I.js +274 -0
  34. package/package.json +3 -2
  35. 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-6IY5L6II.js");
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
- let targetFile;
329
+ const targetFiles = [];
330
330
  if (target === "claude") {
331
- targetFile = join2(projectRoot, "CLAUDE.md");
331
+ targetFiles.push(join2(projectRoot, "CLAUDE.md"));
332
332
  } else if (target === "cursor") {
333
- targetFile = join2(projectRoot, ".cursorrules");
333
+ targetFiles.push(join2(projectRoot, ".cursorrules"));
334
334
  } else if (target === "agents") {
335
- targetFile = join2(projectRoot, "AGENTS.md");
335
+ targetFiles.push(join2(projectRoot, "AGENTS.md"));
336
336
  } else {
337
- if (existsSync2(join2(projectRoot, "CLAUDE.md"))) {
338
- targetFile = join2(projectRoot, "CLAUDE.md");
339
- } else if (existsSync2(join2(projectRoot, ".cursorrules"))) {
340
- targetFile = join2(projectRoot, ".cursorrules");
341
- } else if (existsSync2(join2(projectRoot, "AGENTS.md"))) {
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
- writeToFile(targetFile, summary);
348
- return { file: targetFile, nodesIncluded: stats.nodes, view: view.name };
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
+ };