engramx 0.4.1 → 0.4.2

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 (2) hide show
  1. package/dist/cli.js +379 -212
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -24,17 +24,17 @@ import {
24
24
 
25
25
  // src/cli.ts
26
26
  import { Command } from "commander";
27
- import chalk from "chalk";
27
+ import chalk2 from "chalk";
28
28
  import {
29
- existsSync as existsSync7,
30
- readFileSync as readFileSync4,
29
+ existsSync as existsSync8,
30
+ readFileSync as readFileSync5,
31
31
  writeFileSync as writeFileSync2,
32
32
  mkdirSync,
33
33
  unlinkSync,
34
34
  copyFileSync,
35
35
  renameSync as renameSync3
36
36
  } from "fs";
37
- import { dirname as dirname3, join as join7, resolve as pathResolve } from "path";
37
+ import { dirname as dirname3, join as join8, resolve as pathResolve } from "path";
38
38
  import { homedir } from "os";
39
39
 
40
40
  // src/intercept/safety.ts
@@ -44,8 +44,8 @@ var PASSTHROUGH = null;
44
44
  var DEFAULT_HANDLER_TIMEOUT_MS = 2e3;
45
45
  async function withTimeout(promise, ms = DEFAULT_HANDLER_TIMEOUT_MS) {
46
46
  let timer;
47
- const timeout = new Promise((resolve6) => {
48
- timer = setTimeout(() => resolve6(PASSTHROUGH), ms);
47
+ const timeout = new Promise((resolve7) => {
48
+ timer = setTimeout(() => resolve7(PASSTHROUGH), ms);
49
49
  });
50
50
  try {
51
51
  return await Promise.race([promise, timeout]);
@@ -1162,6 +1162,244 @@ function watchProject(projectRoot, options = {}) {
1162
1162
  return controller;
1163
1163
  }
1164
1164
 
1165
+ // src/dashboard.ts
1166
+ import chalk from "chalk";
1167
+ import { existsSync as existsSync6, statSync as statSync4 } from "fs";
1168
+ import { join as join6, resolve as resolve6, basename as basename4 } from "path";
1169
+
1170
+ // src/intercept/stats.ts
1171
+ var ESTIMATED_TOKENS_PER_READ_DENY = 1200;
1172
+ function summarizeHookLog(entries) {
1173
+ const byEvent = {};
1174
+ const byTool = {};
1175
+ const byDecision = {};
1176
+ let readDenyCount = 0;
1177
+ let firstEntryTs = null;
1178
+ let lastEntryTs = null;
1179
+ for (const entry of entries) {
1180
+ const event = entry.event ?? "unknown";
1181
+ byEvent[event] = (byEvent[event] ?? 0) + 1;
1182
+ const tool = entry.tool ?? "unknown";
1183
+ byTool[tool] = (byTool[tool] ?? 0) + 1;
1184
+ if (entry.decision) {
1185
+ byDecision[entry.decision] = (byDecision[entry.decision] ?? 0) + 1;
1186
+ }
1187
+ if (event === "PreToolUse" && tool === "Read" && entry.decision === "deny") {
1188
+ readDenyCount += 1;
1189
+ }
1190
+ const ts = entry.ts;
1191
+ if (typeof ts === "string") {
1192
+ if (firstEntryTs === null || ts < firstEntryTs) firstEntryTs = ts;
1193
+ if (lastEntryTs === null || ts > lastEntryTs) lastEntryTs = ts;
1194
+ }
1195
+ }
1196
+ return {
1197
+ totalInvocations: entries.length,
1198
+ byEvent: Object.freeze(byEvent),
1199
+ byTool: Object.freeze(byTool),
1200
+ byDecision: Object.freeze(byDecision),
1201
+ readDenyCount,
1202
+ estimatedTokensSaved: readDenyCount * ESTIMATED_TOKENS_PER_READ_DENY,
1203
+ firstEntry: firstEntryTs,
1204
+ lastEntry: lastEntryTs
1205
+ };
1206
+ }
1207
+ function formatStatsSummary(summary) {
1208
+ if (summary.totalInvocations === 0) {
1209
+ return "engram hook stats: no log entries yet.\n\nRun engram install-hook in a project, then use Claude Code to see interceptions.";
1210
+ }
1211
+ const lines = [];
1212
+ lines.push(`engram hook stats (${summary.totalInvocations} invocations)`);
1213
+ lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1214
+ if (summary.firstEntry && summary.lastEntry) {
1215
+ lines.push(`Time range: ${summary.firstEntry} \u2192 ${summary.lastEntry}`);
1216
+ lines.push("");
1217
+ }
1218
+ lines.push("By event:");
1219
+ const eventEntries = Object.entries(summary.byEvent).sort(
1220
+ (a, b) => b[1] - a[1]
1221
+ );
1222
+ for (const [event, count] of eventEntries) {
1223
+ const pct = (count / summary.totalInvocations * 100).toFixed(1);
1224
+ lines.push(` ${event.padEnd(18)} ${String(count).padStart(5)} (${pct}%)`);
1225
+ }
1226
+ lines.push("");
1227
+ lines.push("By tool:");
1228
+ const toolEntries = Object.entries(summary.byTool).filter(([k]) => k !== "unknown").sort((a, b) => b[1] - a[1]);
1229
+ for (const [tool, count] of toolEntries) {
1230
+ lines.push(` ${tool.padEnd(18)} ${String(count).padStart(5)}`);
1231
+ }
1232
+ if (toolEntries.length === 0) {
1233
+ lines.push(" (no tool-tagged entries)");
1234
+ }
1235
+ lines.push("");
1236
+ const decisionEntries = Object.entries(summary.byDecision);
1237
+ if (decisionEntries.length > 0) {
1238
+ lines.push("PreToolUse decisions:");
1239
+ for (const [decision, count] of decisionEntries.sort(
1240
+ (a, b) => b[1] - a[1]
1241
+ )) {
1242
+ lines.push(` ${decision.padEnd(18)} ${String(count).padStart(5)}`);
1243
+ }
1244
+ lines.push("");
1245
+ }
1246
+ if (summary.readDenyCount > 0) {
1247
+ lines.push(
1248
+ `Estimated tokens saved: ~${summary.estimatedTokensSaved.toLocaleString()}`
1249
+ );
1250
+ lines.push(
1251
+ ` (${summary.readDenyCount} Read denies \xD7 ${ESTIMATED_TOKENS_PER_READ_DENY} tok/deny avg)`
1252
+ );
1253
+ } else {
1254
+ lines.push("Estimated tokens saved: 0");
1255
+ lines.push(" (no PreToolUse:Read denies recorded yet)");
1256
+ }
1257
+ return lines.join("\n");
1258
+ }
1259
+
1260
+ // src/dashboard.ts
1261
+ var AMBER = chalk.hex("#d97706");
1262
+ var DIM = chalk.dim;
1263
+ var GREEN = chalk.green;
1264
+ var RED = chalk.red;
1265
+ var BOLD = chalk.bold;
1266
+ var WHITE = chalk.white;
1267
+ function bar(pct, width = 20) {
1268
+ const filled = Math.round(pct / 100 * width);
1269
+ const empty = width - filled;
1270
+ return AMBER("\u2588".repeat(filled)) + DIM("\u2591".repeat(empty));
1271
+ }
1272
+ function fmt(n) {
1273
+ return n.toLocaleString();
1274
+ }
1275
+ function topFiles(entries, n) {
1276
+ const counts = /* @__PURE__ */ new Map();
1277
+ for (const e of entries) {
1278
+ if (e.event === "PreToolUse" && e.decision === "deny" && e.path) {
1279
+ counts.set(e.path, (counts.get(e.path) ?? 0) + 1);
1280
+ }
1281
+ }
1282
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, n).map(([path2, count]) => ({ path: path2, count }));
1283
+ }
1284
+ function recentActivity(entries, n) {
1285
+ return entries.slice(-n);
1286
+ }
1287
+ function formatActivity(entry) {
1288
+ const tool = entry.tool ?? "?";
1289
+ const decision = entry.decision ?? "?";
1290
+ const path2 = entry.path ? entry.path.length > 40 ? "..." + entry.path.slice(-37) : entry.path : "";
1291
+ const icon = decision === "deny" ? GREEN("\u2713") : decision === "allow" ? DIM("\u2192") : DIM("\xB7");
1292
+ const decLabel = decision === "deny" ? GREEN("intercepted") : decision === "allow" ? DIM("allowed") : DIM("passthrough");
1293
+ return ` ${icon} ${WHITE(tool.padEnd(6))} ${decLabel.padEnd(22)} ${DIM(path2)}`;
1294
+ }
1295
+ function clearScreen() {
1296
+ process.stdout.write("\x1B[2J\x1B[H");
1297
+ }
1298
+ function render(projectRoot, entries) {
1299
+ const summary = summarizeHookLog(entries);
1300
+ const projectName = basename4(resolve6(projectRoot));
1301
+ const totalReads = (summary.byDecision["deny"] ?? 0) + (summary.byDecision["allow"] ?? 0) + (summary.byDecision["passthrough"] ?? 0);
1302
+ const intercepted = summary.readDenyCount;
1303
+ const hitRate = totalReads > 0 ? intercepted / totalReads * 100 : 0;
1304
+ const tokensSaved = summary.estimatedTokensSaved;
1305
+ const landmines = entries.filter(
1306
+ (e) => e.event === "PreToolUse" && e.tool === "Edit" && e.decision === "allow" && e.injection
1307
+ ).length;
1308
+ clearScreen();
1309
+ console.log(
1310
+ AMBER(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557")
1311
+ );
1312
+ console.log(
1313
+ AMBER(" \u2551") + WHITE(
1314
+ ` engram dashboard \u2014 ${projectName}`.padEnd(54)
1315
+ ) + AMBER("\u2551")
1316
+ );
1317
+ console.log(
1318
+ AMBER(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D")
1319
+ );
1320
+ console.log();
1321
+ console.log(
1322
+ ` ${AMBER("TOKENS SAVED")} ${GREEN(BOLD(fmt(tokensSaved)))} ${DIM(`(~${fmt(intercepted)} reads \xD7 ${fmt(ESTIMATED_TOKENS_PER_READ_DENY)} tokens)`)}`
1323
+ );
1324
+ console.log();
1325
+ console.log(
1326
+ ` ${AMBER("HIT RATE")} ${bar(hitRate)} ${WHITE(BOLD(hitRate.toFixed(1) + "%"))} ${DIM(`(${intercepted}/${totalReads} tool calls)`)}`
1327
+ );
1328
+ console.log();
1329
+ const denied = summary.byDecision["deny"] ?? 0;
1330
+ const allowed = summary.byDecision["allow"] ?? 0;
1331
+ const passthrough = summary.byDecision["passthrough"] ?? 0;
1332
+ console.log(
1333
+ ` ${AMBER("DECISIONS")} ${GREEN("\u25A0")} intercepted ${GREEN(BOLD(String(denied)))} ${DIM("\u25A0")} allowed ${DIM(String(allowed))} ${DIM("\u25A0")} passthrough ${DIM(String(passthrough))}`
1334
+ );
1335
+ if (landmines > 0) {
1336
+ console.log(
1337
+ ` ${RED("\u25B2")} landmine warnings ${RED(BOLD(String(landmines)))}`
1338
+ );
1339
+ }
1340
+ console.log();
1341
+ const events = summary.byEvent;
1342
+ const eventLine = Object.entries(events).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${DIM(k)} ${WHITE(String(v))}`).join(" ");
1343
+ console.log(` ${AMBER("EVENTS")} ${eventLine}`);
1344
+ console.log();
1345
+ const top = topFiles(entries, 5);
1346
+ if (top.length > 0) {
1347
+ console.log(` ${AMBER("TOP FILES")} ${DIM("(most intercepted)")}`);
1348
+ for (const f of top) {
1349
+ const barLen = Math.min(
1350
+ Math.round(f.count / (top[0]?.count ?? 1) * 15),
1351
+ 15
1352
+ );
1353
+ console.log(
1354
+ ` ${AMBER("\u2588".repeat(barLen))} ${WHITE(String(f.count).padStart(3))} ${DIM(f.path)}`
1355
+ );
1356
+ }
1357
+ console.log();
1358
+ }
1359
+ const recent = recentActivity(entries, 8);
1360
+ if (recent.length > 0) {
1361
+ console.log(` ${AMBER("RECENT")} ${DIM("(last 8 events)")}`);
1362
+ for (const e of recent) {
1363
+ console.log(formatActivity(e));
1364
+ }
1365
+ console.log();
1366
+ }
1367
+ console.log(
1368
+ DIM(
1369
+ ` Total invocations: ${summary.totalInvocations}` + (summary.firstEntry ? ` | Since: ${summary.firstEntry}` : "") + ` | Press Ctrl+C to exit`
1370
+ )
1371
+ );
1372
+ }
1373
+ function startDashboard(projectRoot, options = {}) {
1374
+ const root = resolve6(projectRoot);
1375
+ const interval = options.interval ?? 1e3;
1376
+ const controller = new AbortController();
1377
+ let lastSize = 0;
1378
+ let cachedEntries = [];
1379
+ const tick = () => {
1380
+ if (controller.signal.aborted) return;
1381
+ try {
1382
+ const logPath = join6(root, ".engram", "hook-log.jsonl");
1383
+ if (existsSync6(logPath)) {
1384
+ const currentSize = statSync4(logPath).size;
1385
+ if (currentSize !== lastSize) {
1386
+ cachedEntries = readHookLog(root);
1387
+ lastSize = currentSize;
1388
+ }
1389
+ }
1390
+ render(root, cachedEntries);
1391
+ } catch {
1392
+ }
1393
+ };
1394
+ tick();
1395
+ const timer = setInterval(tick, interval);
1396
+ timer.unref();
1397
+ controller.signal.addEventListener("abort", () => {
1398
+ clearInterval(timer);
1399
+ });
1400
+ return controller;
1401
+ }
1402
+
1165
1403
  // src/intercept/cursor-adapter.ts
1166
1404
  var ALLOW = { permission: "allow" };
1167
1405
  function toClaudeReadPayload(cursorPayload) {
@@ -1334,105 +1572,15 @@ function formatInstallDiff(before, after) {
1334
1572
  return lines.length > 0 ? lines.join("\n") : "(no changes)";
1335
1573
  }
1336
1574
 
1337
- // src/intercept/stats.ts
1338
- var ESTIMATED_TOKENS_PER_READ_DENY = 1200;
1339
- function summarizeHookLog(entries) {
1340
- const byEvent = {};
1341
- const byTool = {};
1342
- const byDecision = {};
1343
- let readDenyCount = 0;
1344
- let firstEntryTs = null;
1345
- let lastEntryTs = null;
1346
- for (const entry of entries) {
1347
- const event = entry.event ?? "unknown";
1348
- byEvent[event] = (byEvent[event] ?? 0) + 1;
1349
- const tool = entry.tool ?? "unknown";
1350
- byTool[tool] = (byTool[tool] ?? 0) + 1;
1351
- if (entry.decision) {
1352
- byDecision[entry.decision] = (byDecision[entry.decision] ?? 0) + 1;
1353
- }
1354
- if (event === "PreToolUse" && tool === "Read" && entry.decision === "deny") {
1355
- readDenyCount += 1;
1356
- }
1357
- const ts = entry.ts;
1358
- if (typeof ts === "string") {
1359
- if (firstEntryTs === null || ts < firstEntryTs) firstEntryTs = ts;
1360
- if (lastEntryTs === null || ts > lastEntryTs) lastEntryTs = ts;
1361
- }
1362
- }
1363
- return {
1364
- totalInvocations: entries.length,
1365
- byEvent: Object.freeze(byEvent),
1366
- byTool: Object.freeze(byTool),
1367
- byDecision: Object.freeze(byDecision),
1368
- readDenyCount,
1369
- estimatedTokensSaved: readDenyCount * ESTIMATED_TOKENS_PER_READ_DENY,
1370
- firstEntry: firstEntryTs,
1371
- lastEntry: lastEntryTs
1372
- };
1373
- }
1374
- function formatStatsSummary(summary) {
1375
- if (summary.totalInvocations === 0) {
1376
- return "engram hook stats: no log entries yet.\n\nRun engram install-hook in a project, then use Claude Code to see interceptions.";
1377
- }
1378
- const lines = [];
1379
- lines.push(`engram hook stats (${summary.totalInvocations} invocations)`);
1380
- lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1381
- if (summary.firstEntry && summary.lastEntry) {
1382
- lines.push(`Time range: ${summary.firstEntry} \u2192 ${summary.lastEntry}`);
1383
- lines.push("");
1384
- }
1385
- lines.push("By event:");
1386
- const eventEntries = Object.entries(summary.byEvent).sort(
1387
- (a, b) => b[1] - a[1]
1388
- );
1389
- for (const [event, count] of eventEntries) {
1390
- const pct = (count / summary.totalInvocations * 100).toFixed(1);
1391
- lines.push(` ${event.padEnd(18)} ${String(count).padStart(5)} (${pct}%)`);
1392
- }
1393
- lines.push("");
1394
- lines.push("By tool:");
1395
- const toolEntries = Object.entries(summary.byTool).filter(([k]) => k !== "unknown").sort((a, b) => b[1] - a[1]);
1396
- for (const [tool, count] of toolEntries) {
1397
- lines.push(` ${tool.padEnd(18)} ${String(count).padStart(5)}`);
1398
- }
1399
- if (toolEntries.length === 0) {
1400
- lines.push(" (no tool-tagged entries)");
1401
- }
1402
- lines.push("");
1403
- const decisionEntries = Object.entries(summary.byDecision);
1404
- if (decisionEntries.length > 0) {
1405
- lines.push("PreToolUse decisions:");
1406
- for (const [decision, count] of decisionEntries.sort(
1407
- (a, b) => b[1] - a[1]
1408
- )) {
1409
- lines.push(` ${decision.padEnd(18)} ${String(count).padStart(5)}`);
1410
- }
1411
- lines.push("");
1412
- }
1413
- if (summary.readDenyCount > 0) {
1414
- lines.push(
1415
- `Estimated tokens saved: ~${summary.estimatedTokensSaved.toLocaleString()}`
1416
- );
1417
- lines.push(
1418
- ` (${summary.readDenyCount} Read denies \xD7 ${ESTIMATED_TOKENS_PER_READ_DENY} tok/deny avg)`
1419
- );
1420
- } else {
1421
- lines.push("Estimated tokens saved: 0");
1422
- lines.push(" (no PreToolUse:Read denies recorded yet)");
1423
- }
1424
- return lines.join("\n");
1425
- }
1426
-
1427
1575
  // src/intercept/memory-md.ts
1428
1576
  import {
1429
- existsSync as existsSync6,
1430
- readFileSync as readFileSync3,
1577
+ existsSync as existsSync7,
1578
+ readFileSync as readFileSync4,
1431
1579
  writeFileSync,
1432
1580
  renameSync as renameSync2,
1433
- statSync as statSync4
1581
+ statSync as statSync5
1434
1582
  } from "fs";
1435
- import { join as join6 } from "path";
1583
+ import { join as join7 } from "path";
1436
1584
  var ENGRAM_MARKER_START = "<!-- engram:structural-facts:start -->";
1437
1585
  var ENGRAM_MARKER_END = "<!-- engram:structural-facts:end -->";
1438
1586
  var MAX_MEMORY_FILE_BYTES = 1e6;
@@ -1503,15 +1651,15 @@ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
1503
1651
  if (engramSection.length > MAX_ENGRAM_SECTION_BYTES) {
1504
1652
  return false;
1505
1653
  }
1506
- const memoryPath = join6(projectRoot, "MEMORY.md");
1654
+ const memoryPath = join7(projectRoot, "MEMORY.md");
1507
1655
  try {
1508
1656
  let existing = "";
1509
- if (existsSync6(memoryPath)) {
1510
- const st = statSync4(memoryPath);
1657
+ if (existsSync7(memoryPath)) {
1658
+ const st = statSync5(memoryPath);
1511
1659
  if (st.size > MAX_MEMORY_FILE_BYTES) {
1512
1660
  return false;
1513
1661
  }
1514
- existing = readFileSync3(memoryPath, "utf-8");
1662
+ existing = readFileSync4(memoryPath, "utf-8");
1515
1663
  }
1516
1664
  const updated = upsertEngramSection(existing, engramSection);
1517
1665
  const tmpPath = memoryPath + ".engram-tmp";
@@ -1524,7 +1672,7 @@ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
1524
1672
  }
1525
1673
 
1526
1674
  // src/cli.ts
1527
- import { basename as basename4 } from "path";
1675
+ import { basename as basename5 } from "path";
1528
1676
  import { createRequire } from "module";
1529
1677
  var require2 = createRequire(import.meta.url);
1530
1678
  var { version: PKG_VERSION } = require2("../package.json");
@@ -1536,46 +1684,46 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
1536
1684
  "--with-skills [dir]",
1537
1685
  "Also index Claude Code skills from ~/.claude/skills/ or a given path"
1538
1686
  ).action(async (projectPath, opts) => {
1539
- console.log(chalk.dim("\u{1F50D} Scanning codebase..."));
1687
+ console.log(chalk2.dim("\u{1F50D} Scanning codebase..."));
1540
1688
  const result = await init(projectPath, {
1541
1689
  withSkills: opts.withSkills
1542
1690
  });
1543
1691
  console.log(
1544
- chalk.green("\u{1F333} AST extraction complete") + chalk.dim(` (${result.timeMs}ms, 0 tokens used)`)
1692
+ chalk2.green("\u{1F333} AST extraction complete") + chalk2.dim(` (${result.timeMs}ms, 0 tokens used)`)
1545
1693
  );
1546
1694
  console.log(
1547
- ` ${chalk.bold(String(result.nodes))} nodes, ${chalk.bold(String(result.edges))} edges from ${chalk.bold(String(result.fileCount))} files (${result.totalLines.toLocaleString()} lines)`
1695
+ ` ${chalk2.bold(String(result.nodes))} nodes, ${chalk2.bold(String(result.edges))} edges from ${chalk2.bold(String(result.fileCount))} files (${result.totalLines.toLocaleString()} lines)`
1548
1696
  );
1549
1697
  if (result.skillCount && result.skillCount > 0) {
1550
1698
  console.log(
1551
- chalk.cyan(` ${chalk.bold(String(result.skillCount))} skills indexed`)
1699
+ chalk2.cyan(` ${chalk2.bold(String(result.skillCount))} skills indexed`)
1552
1700
  );
1553
1701
  }
1554
1702
  const bench = await benchmark(projectPath);
1555
1703
  if (bench.naiveFullCorpus > 0 && bench.reductionVsRelevant > 1) {
1556
1704
  console.log(
1557
- chalk.cyan(`
1558
- \u{1F4CA} Token savings: ${chalk.bold(bench.reductionVsRelevant + "x")} fewer tokens vs relevant files (${bench.reductionVsFull}x vs full corpus)`)
1705
+ chalk2.cyan(`
1706
+ \u{1F4CA} Token savings: ${chalk2.bold(bench.reductionVsRelevant + "x")} fewer tokens vs relevant files (${bench.reductionVsFull}x vs full corpus)`)
1559
1707
  );
1560
1708
  console.log(
1561
- chalk.dim(` Full corpus: ~${bench.naiveFullCorpus.toLocaleString()} tokens | Graph query: ~${bench.avgQueryTokens.toLocaleString()} tokens`)
1709
+ chalk2.dim(` Full corpus: ~${bench.naiveFullCorpus.toLocaleString()} tokens | Graph query: ~${bench.avgQueryTokens.toLocaleString()} tokens`)
1562
1710
  );
1563
1711
  }
1564
- console.log(chalk.green("\n\u2705 Ready. Your AI now has persistent memory."));
1565
- console.log(chalk.dim(" Graph stored in .engram/graph.db"));
1712
+ console.log(chalk2.green("\n\u2705 Ready. Your AI now has persistent memory."));
1713
+ console.log(chalk2.dim(" Graph stored in .engram/graph.db"));
1566
1714
  const resolvedProject = pathResolve(projectPath);
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");
1715
+ const localSettings = join8(resolvedProject, ".claude", "settings.local.json");
1716
+ const projectSettings = join8(resolvedProject, ".claude", "settings.json");
1717
+ const hasHooks = existsSync8(localSettings) && readFileSync5(localSettings, "utf-8").includes("engram intercept") || existsSync8(projectSettings) && readFileSync5(projectSettings, "utf-8").includes("engram intercept");
1570
1718
  if (!hasHooks) {
1571
1719
  console.log(
1572
- chalk.yellow("\n\u{1F4A1} Next step: ") + chalk.white("engram install-hook") + chalk.dim(
1720
+ chalk2.yellow("\n\u{1F4A1} Next step: ") + chalk2.white("engram install-hook") + chalk2.dim(
1573
1721
  " \u2014 enables automatic Read interception (82% token savings)"
1574
1722
  )
1575
1723
  );
1576
1724
  console.log(
1577
- chalk.dim(
1578
- " Also recommended: " + chalk.white("engram hooks install") + " \u2014 auto-rebuild graph on git commit"
1725
+ chalk2.dim(
1726
+ " Also recommended: " + chalk2.white("engram hooks install") + " \u2014 auto-rebuild graph on git commit"
1579
1727
  )
1580
1728
  );
1581
1729
  }
@@ -1583,24 +1731,43 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
1583
1731
  program.command("watch").description("Watch project for file changes and re-index incrementally").argument("[path]", "Project directory", ".").action(async (projectPath) => {
1584
1732
  const resolvedPath = pathResolve(projectPath);
1585
1733
  console.log(
1586
- chalk.dim("\u{1F441} Watching ") + chalk.white(resolvedPath) + chalk.dim(" for changes...")
1734
+ chalk2.dim("\u{1F441} Watching ") + chalk2.white(resolvedPath) + chalk2.dim(" for changes...")
1587
1735
  );
1588
1736
  const controller = watchProject(resolvedPath, {
1589
1737
  onReindex: (filePath, nodeCount) => {
1590
1738
  console.log(
1591
- chalk.green(" \u21BB ") + chalk.white(filePath) + chalk.dim(` (${nodeCount} nodes)`)
1739
+ chalk2.green(" \u21BB ") + chalk2.white(filePath) + chalk2.dim(` (${nodeCount} nodes)`)
1592
1740
  );
1593
1741
  },
1594
1742
  onError: (err) => {
1595
- console.error(chalk.red(" \u2717 ") + err.message);
1743
+ console.error(chalk2.red(" \u2717 ") + err.message);
1596
1744
  },
1597
1745
  onReady: () => {
1598
- console.log(chalk.green(" \u2713 Watcher active.") + chalk.dim(" Press Ctrl+C to stop."));
1746
+ console.log(chalk2.green(" \u2713 Watcher active.") + chalk2.dim(" Press Ctrl+C to stop."));
1599
1747
  }
1600
1748
  });
1601
1749
  process.on("SIGINT", () => {
1602
1750
  controller.abort();
1603
- console.log(chalk.dim("\n Watcher stopped."));
1751
+ console.log(chalk2.dim("\n Watcher stopped."));
1752
+ process.exit(0);
1753
+ });
1754
+ await new Promise(() => {
1755
+ });
1756
+ });
1757
+ program.command("dashboard").alias("hud").description("Live terminal dashboard showing hook activity and token savings").argument("[path]", "Project directory", ".").action(async (projectPath) => {
1758
+ const resolvedPath = pathResolve(projectPath);
1759
+ const dbPath = join8(resolvedPath, ".engram", "graph.db");
1760
+ if (!existsSync8(dbPath)) {
1761
+ console.error(
1762
+ chalk2.red("No engram graph found at ") + chalk2.white(resolvedPath)
1763
+ );
1764
+ console.error(chalk2.dim("Run 'engram init' first."));
1765
+ process.exit(1);
1766
+ }
1767
+ const controller = startDashboard(resolvedPath);
1768
+ process.on("SIGINT", () => {
1769
+ controller.abort();
1770
+ console.log(chalk2.dim("\n Dashboard closed."));
1604
1771
  process.exit(0);
1605
1772
  });
1606
1773
  await new Promise(() => {
@@ -1613,10 +1780,10 @@ program.command("query").description("Query the knowledge graph").argument("<que
1613
1780
  tokenBudget: Number(opts.budget)
1614
1781
  });
1615
1782
  if (result.nodesFound === 0) {
1616
- console.log(chalk.yellow("No matching nodes found."));
1783
+ console.log(chalk2.yellow("No matching nodes found."));
1617
1784
  return;
1618
1785
  }
1619
- console.log(chalk.dim(`Found ${result.nodesFound} nodes (~${result.estimatedTokens} tokens)
1786
+ console.log(chalk2.dim(`Found ${result.nodesFound} nodes (~${result.estimatedTokens} tokens)
1620
1787
  `));
1621
1788
  console.log(result.text);
1622
1789
  });
@@ -1627,25 +1794,25 @@ program.command("path").description("Find shortest path between two concepts").a
1627
1794
  program.command("gods").description("Show most connected entities (god nodes)").option("-n, --top <n>", "Number of nodes", "10").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
1628
1795
  const gods = await godNodes(opts.project, Number(opts.top));
1629
1796
  if (gods.length === 0) {
1630
- console.log(chalk.yellow("No nodes found. Run `engram init` first."));
1797
+ console.log(chalk2.yellow("No nodes found. Run `engram init` first."));
1631
1798
  return;
1632
1799
  }
1633
- console.log(chalk.bold("God nodes (most connected):\n"));
1800
+ console.log(chalk2.bold("God nodes (most connected):\n"));
1634
1801
  for (let i = 0; i < gods.length; i++) {
1635
1802
  const g = gods[i];
1636
1803
  console.log(
1637
- ` ${chalk.dim(String(i + 1) + ".")} ${chalk.bold(g.label)} ${chalk.dim(`[${g.kind}]`)} \u2014 ${g.degree} edges ${chalk.dim(g.sourceFile)}`
1804
+ ` ${chalk2.dim(String(i + 1) + ".")} ${chalk2.bold(g.label)} ${chalk2.dim(`[${g.kind}]`)} \u2014 ${g.degree} edges ${chalk2.dim(g.sourceFile)}`
1638
1805
  );
1639
1806
  }
1640
1807
  });
1641
1808
  program.command("stats").description("Show knowledge graph statistics and token savings").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
1642
1809
  const s = await stats(opts.project);
1643
1810
  const bench = await benchmark(opts.project);
1644
- console.log(chalk.bold("\n\u{1F4CA} engram stats\n"));
1645
- console.log(` Nodes: ${chalk.bold(String(s.nodes))}`);
1646
- console.log(` Edges: ${chalk.bold(String(s.edges))}`);
1811
+ console.log(chalk2.bold("\n\u{1F4CA} engram stats\n"));
1812
+ console.log(` Nodes: ${chalk2.bold(String(s.nodes))}`);
1813
+ console.log(` Edges: ${chalk2.bold(String(s.edges))}`);
1647
1814
  console.log(
1648
- ` Confidence: ${chalk.green(s.extractedPct + "% EXTRACTED")} \xB7 ${chalk.yellow(s.inferredPct + "% INFERRED")} \xB7 ${chalk.red(s.ambiguousPct + "% AMBIGUOUS")}`
1815
+ ` Confidence: ${chalk2.green(s.extractedPct + "% EXTRACTED")} \xB7 ${chalk2.yellow(s.inferredPct + "% INFERRED")} \xB7 ${chalk2.red(s.ambiguousPct + "% AMBIGUOUS")}`
1649
1816
  );
1650
1817
  if (s.lastMined > 0) {
1651
1818
  const ago = Math.round((Date.now() - s.lastMined) / 6e4);
@@ -1653,20 +1820,20 @@ program.command("stats").description("Show knowledge graph statistics and token
1653
1820
  }
1654
1821
  if (bench.naiveFullCorpus > 0) {
1655
1822
  console.log(`
1656
- ${chalk.cyan("Token savings:")}`);
1823
+ ${chalk2.cyan("Token savings:")}`);
1657
1824
  console.log(` Full corpus: ~${bench.naiveFullCorpus.toLocaleString()} tokens`);
1658
1825
  console.log(` Avg query: ~${bench.avgQueryTokens.toLocaleString()} tokens`);
1659
- console.log(` vs relevant: ${chalk.bold.cyan(bench.reductionVsRelevant + "x")} fewer tokens`);
1660
- console.log(` vs full: ${chalk.bold.cyan(bench.reductionVsFull + "x")} fewer tokens`);
1826
+ console.log(` vs relevant: ${chalk2.bold.cyan(bench.reductionVsRelevant + "x")} fewer tokens`);
1827
+ console.log(` vs full: ${chalk2.bold.cyan(bench.reductionVsFull + "x")} fewer tokens`);
1661
1828
  }
1662
1829
  console.log();
1663
1830
  });
1664
1831
  program.command("learn").description("Teach engram a decision, pattern, or lesson").argument("<text>", "What to remember (e.g., 'We chose JWT over sessions for horizontal scaling')").option("-p, --project <path>", "Project directory", ".").action(async (text, opts) => {
1665
1832
  const result = await learn(opts.project, text);
1666
1833
  if (result.nodesAdded > 0) {
1667
- console.log(chalk.green(`\u{1F9E0} Learned ${result.nodesAdded} new insight(s).`));
1834
+ console.log(chalk2.green(`\u{1F9E0} Learned ${result.nodesAdded} new insight(s).`));
1668
1835
  } else {
1669
- console.log(chalk.yellow("No patterns extracted. Try a more specific statement."));
1836
+ console.log(chalk2.yellow("No patterns extracted. Try a more specific statement."));
1670
1837
  }
1671
1838
  });
1672
1839
  program.command("mistakes").description("List known mistakes extracted from past sessions").option("-p, --project <path>", "Project directory", ".").option("-l, --limit <n>", "Max entries to display", "20").option("--since <days>", "Only mistakes from the last N days").action(
@@ -1676,11 +1843,11 @@ program.command("mistakes").description("List known mistakes extracted from past
1676
1843
  sinceDays: opts.since ? Number(opts.since) : void 0
1677
1844
  });
1678
1845
  if (result.length === 0) {
1679
- console.log(chalk.yellow("No mistakes recorded."));
1846
+ console.log(chalk2.yellow("No mistakes recorded."));
1680
1847
  return;
1681
1848
  }
1682
1849
  console.log(
1683
- chalk.bold(`
1850
+ chalk2.bold(`
1684
1851
  \u26A0\uFE0F ${result.length} mistake(s) recorded:
1685
1852
  `)
1686
1853
  );
@@ -1690,7 +1857,7 @@ program.command("mistakes").description("List known mistakes extracted from past
1690
1857
  Math.round((Date.now() - m.lastVerified) / 864e5)
1691
1858
  );
1692
1859
  console.log(
1693
- ` ${chalk.dim(`[${m.sourceFile}, ${ago}d ago]`)} ${m.label}`
1860
+ ` ${chalk2.dim(`[${m.sourceFile}, ${ago}d ago]`)} ${m.label}`
1694
1861
  );
1695
1862
  }
1696
1863
  console.log();
@@ -1698,14 +1865,14 @@ program.command("mistakes").description("List known mistakes extracted from past
1698
1865
  );
1699
1866
  program.command("bench").description("Run token reduction benchmark").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
1700
1867
  const result = await benchmark(opts.project);
1701
- console.log(chalk.bold("\n\u26A1 engram token reduction benchmark\n"));
1868
+ console.log(chalk2.bold("\n\u26A1 engram token reduction benchmark\n"));
1702
1869
  console.log(` Full corpus: ~${result.naiveFullCorpus.toLocaleString()} tokens`);
1703
1870
  console.log(` Avg graph query: ~${result.avgQueryTokens.toLocaleString()} tokens`);
1704
- console.log(` vs relevant: ${chalk.bold.green(result.reductionVsRelevant + "x")} fewer tokens`);
1705
- console.log(` vs full corpus: ${chalk.bold.green(result.reductionVsFull + "x")} fewer tokens
1871
+ console.log(` vs relevant: ${chalk2.bold.green(result.reductionVsRelevant + "x")} fewer tokens`);
1872
+ console.log(` vs full corpus: ${chalk2.bold.green(result.reductionVsFull + "x")} fewer tokens
1706
1873
  `);
1707
1874
  for (const pq of result.perQuestion) {
1708
- console.log(` ${chalk.dim(`[${pq.reductionRelevant}x relevant / ${pq.reductionFull}x full]`)} ${pq.question}`);
1875
+ console.log(` ${chalk2.dim(`[${pq.reductionRelevant}x relevant / ${pq.reductionFull}x full]`)} ${pq.question}`);
1709
1876
  }
1710
1877
  console.log();
1711
1878
  });
@@ -1721,7 +1888,7 @@ program.command("gen").description("Generate CLAUDE.md / .cursorrules section fr
1721
1888
  const target = opts.target;
1722
1889
  const result = await autogen(opts.project, target, opts.task);
1723
1890
  console.log(
1724
- chalk.green(
1891
+ chalk2.green(
1725
1892
  `\u2705 Updated ${result.file} (${result.nodesIncluded} nodes, view: ${result.view})`
1726
1893
  )
1727
1894
  );
@@ -1731,11 +1898,11 @@ function resolveSettingsPath(scope, projectPath) {
1731
1898
  const absProject = pathResolve(projectPath);
1732
1899
  switch (scope) {
1733
1900
  case "local":
1734
- return join7(absProject, ".claude", "settings.local.json");
1901
+ return join8(absProject, ".claude", "settings.local.json");
1735
1902
  case "project":
1736
- return join7(absProject, ".claude", "settings.json");
1903
+ return join8(absProject, ".claude", "settings.json");
1737
1904
  case "user":
1738
- return join7(homedir(), ".claude", "settings.json");
1905
+ return join8(homedir(), ".claude", "settings.json");
1739
1906
  default:
1740
1907
  return null;
1741
1908
  }
@@ -1822,25 +1989,25 @@ program.command("install-hook").description("Install engram hook entries into Cl
1822
1989
  const settingsPath = resolveSettingsPath(opts.scope, opts.project);
1823
1990
  if (!settingsPath) {
1824
1991
  console.error(
1825
- chalk.red(
1992
+ chalk2.red(
1826
1993
  `Unknown scope: ${opts.scope} (expected: local | project | user)`
1827
1994
  )
1828
1995
  );
1829
1996
  process.exit(1);
1830
1997
  }
1831
1998
  let existing = {};
1832
- if (existsSync7(settingsPath)) {
1999
+ if (existsSync8(settingsPath)) {
1833
2000
  try {
1834
- const raw = readFileSync4(settingsPath, "utf-8");
2001
+ const raw = readFileSync5(settingsPath, "utf-8");
1835
2002
  existing = raw.trim() ? JSON.parse(raw) : {};
1836
2003
  } catch (err) {
1837
2004
  console.error(
1838
- chalk.red(
2005
+ chalk2.red(
1839
2006
  `Failed to parse ${settingsPath}: ${err.message}`
1840
2007
  )
1841
2008
  );
1842
2009
  console.error(
1843
- chalk.dim(
2010
+ chalk2.dim(
1844
2011
  "Fix the JSON syntax and re-run install-hook, or remove the file and start fresh."
1845
2012
  )
1846
2013
  );
@@ -1849,38 +2016,38 @@ program.command("install-hook").description("Install engram hook entries into Cl
1849
2016
  }
1850
2017
  const result = installEngramHooks(existing);
1851
2018
  console.log(
1852
- chalk.bold(`
2019
+ chalk2.bold(`
1853
2020
  \u{1F4CC} engram install-hook (scope: ${opts.scope})`)
1854
2021
  );
1855
- console.log(chalk.dim(` Target: ${settingsPath}`));
2022
+ console.log(chalk2.dim(` Target: ${settingsPath}`));
1856
2023
  if (result.added.length === 0) {
1857
2024
  console.log(
1858
- chalk.yellow(
2025
+ chalk2.yellow(
1859
2026
  `
1860
2027
  All engram hooks already installed (${result.alreadyPresent.join(", ")}).`
1861
2028
  )
1862
2029
  );
1863
2030
  console.log(
1864
- chalk.dim(
2031
+ chalk2.dim(
1865
2032
  " Run 'engram uninstall-hook' first if you want to reinstall."
1866
2033
  )
1867
2034
  );
1868
2035
  return;
1869
2036
  }
1870
- console.log(chalk.cyan("\n Changes:"));
2037
+ console.log(chalk2.cyan("\n Changes:"));
1871
2038
  console.log(
1872
2039
  formatInstallDiff(existing, result.updated).split("\n").map((l) => " " + l).join("\n")
1873
2040
  );
1874
2041
  if (opts.dryRun) {
1875
- console.log(chalk.dim("\n (dry-run \u2014 no changes written)"));
2042
+ console.log(chalk2.dim("\n (dry-run \u2014 no changes written)"));
1876
2043
  return;
1877
2044
  }
1878
2045
  try {
1879
2046
  mkdirSync(dirname3(settingsPath), { recursive: true });
1880
- if (existsSync7(settingsPath)) {
2047
+ if (existsSync8(settingsPath)) {
1881
2048
  const backupPath = `${settingsPath}.engram-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
1882
2049
  copyFileSync(settingsPath, backupPath);
1883
- console.log(chalk.dim(` Backup: ${backupPath}`));
2050
+ console.log(chalk2.dim(` Backup: ${backupPath}`));
1884
2051
  }
1885
2052
  const tmpPath = settingsPath + ".engram-tmp";
1886
2053
  writeFileSync2(
@@ -1890,26 +2057,26 @@ program.command("install-hook").description("Install engram hook entries into Cl
1890
2057
  renameSync3(tmpPath, settingsPath);
1891
2058
  } catch (err) {
1892
2059
  console.error(
1893
- chalk.red(`
2060
+ chalk2.red(`
1894
2061
  \u274C Write failed: ${err.message}`)
1895
2062
  );
1896
2063
  process.exit(1);
1897
2064
  }
1898
2065
  console.log(
1899
- chalk.green(
2066
+ chalk2.green(
1900
2067
  `
1901
2068
  \u2705 Installed ${result.added.length} hook event${result.added.length === 1 ? "" : "s"}: ${result.added.join(", ")}`
1902
2069
  )
1903
2070
  );
1904
2071
  if (result.alreadyPresent.length > 0) {
1905
2072
  console.log(
1906
- chalk.dim(
2073
+ chalk2.dim(
1907
2074
  ` Already present: ${result.alreadyPresent.join(", ")}`
1908
2075
  )
1909
2076
  );
1910
2077
  }
1911
2078
  console.log(
1912
- chalk.dim(
2079
+ chalk2.dim(
1913
2080
  "\n Next: open a Claude Code session and engram will start intercepting tool calls."
1914
2081
  )
1915
2082
  );
@@ -1918,29 +2085,29 @@ program.command("install-hook").description("Install engram hook entries into Cl
1918
2085
  program.command("uninstall-hook").description("Remove engram hook entries from Claude Code settings").option("--scope <scope>", "local | project | user", "local").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
1919
2086
  const settingsPath = resolveSettingsPath(opts.scope, opts.project);
1920
2087
  if (!settingsPath) {
1921
- console.error(chalk.red(`Unknown scope: ${opts.scope}`));
2088
+ console.error(chalk2.red(`Unknown scope: ${opts.scope}`));
1922
2089
  process.exit(1);
1923
2090
  }
1924
- if (!existsSync7(settingsPath)) {
2091
+ if (!existsSync8(settingsPath)) {
1925
2092
  console.log(
1926
- chalk.yellow(`No settings file at ${settingsPath} \u2014 nothing to remove.`)
2093
+ chalk2.yellow(`No settings file at ${settingsPath} \u2014 nothing to remove.`)
1927
2094
  );
1928
2095
  return;
1929
2096
  }
1930
2097
  let existing;
1931
2098
  try {
1932
- const raw = readFileSync4(settingsPath, "utf-8");
2099
+ const raw = readFileSync5(settingsPath, "utf-8");
1933
2100
  existing = raw.trim() ? JSON.parse(raw) : {};
1934
2101
  } catch (err) {
1935
2102
  console.error(
1936
- chalk.red(`Failed to parse ${settingsPath}: ${err.message}`)
2103
+ chalk2.red(`Failed to parse ${settingsPath}: ${err.message}`)
1937
2104
  );
1938
2105
  process.exit(1);
1939
2106
  }
1940
2107
  const result = uninstallEngramHooks(existing);
1941
2108
  if (result.removed.length === 0) {
1942
2109
  console.log(
1943
- chalk.yellow(`
2110
+ chalk2.yellow(`
1944
2111
  No engram hooks found in ${settingsPath}.`)
1945
2112
  );
1946
2113
  return;
@@ -1952,15 +2119,15 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
1952
2119
  writeFileSync2(tmpPath, JSON.stringify(result.updated, null, 2) + "\n");
1953
2120
  renameSync3(tmpPath, settingsPath);
1954
2121
  console.log(
1955
- chalk.green(
2122
+ chalk2.green(
1956
2123
  `
1957
2124
  \u2705 Removed engram hooks from ${result.removed.length} event${result.removed.length === 1 ? "" : "s"}: ${result.removed.join(", ")}`
1958
2125
  )
1959
2126
  );
1960
- console.log(chalk.dim(` Backup: ${backupPath}`));
2127
+ console.log(chalk2.dim(` Backup: ${backupPath}`));
1961
2128
  } catch (err) {
1962
2129
  console.error(
1963
- chalk.red(`
2130
+ chalk2.red(`
1964
2131
  \u274C Write failed: ${err.message}`)
1965
2132
  );
1966
2133
  process.exit(1);
@@ -1987,16 +2154,16 @@ program.command("hook-preview").description("Show what the Read handler would do
1987
2154
  tool_input: { file_path: absFile }
1988
2155
  };
1989
2156
  const result = await dispatchHook(payload);
1990
- console.log(chalk.bold(`
2157
+ console.log(chalk2.bold(`
1991
2158
  \u{1F4CB} Hook preview: ${absFile}`));
1992
- console.log(chalk.dim(` Project: ${absProject}`));
2159
+ console.log(chalk2.dim(` Project: ${absProject}`));
1993
2160
  console.log();
1994
2161
  if (result === null || result === void 0) {
1995
2162
  console.log(
1996
- chalk.yellow(" Decision: PASSTHROUGH (Read would execute normally)")
2163
+ chalk2.yellow(" Decision: PASSTHROUGH (Read would execute normally)")
1997
2164
  );
1998
2165
  console.log(
1999
- chalk.dim(
2166
+ chalk2.dim(
2000
2167
  " Possible reasons: file not in graph, confidence below threshold, content unsafe, outside project, stale graph."
2001
2168
  )
2002
2169
  );
@@ -2005,8 +2172,8 @@ program.command("hook-preview").description("Show what the Read handler would do
2005
2172
  const wrapped = result;
2006
2173
  const decision = wrapped.hookSpecificOutput?.permissionDecision;
2007
2174
  if (decision === "deny") {
2008
- console.log(chalk.green(" Decision: DENY (Read would be replaced)"));
2009
- console.log(chalk.dim(" Summary (would be delivered to Claude):"));
2175
+ console.log(chalk2.green(" Decision: DENY (Read would be replaced)"));
2176
+ console.log(chalk2.dim(" Summary (would be delivered to Claude):"));
2010
2177
  console.log();
2011
2178
  const reason = wrapped.hookSpecificOutput?.permissionDecisionReason ?? "";
2012
2179
  console.log(
@@ -2015,41 +2182,41 @@ program.command("hook-preview").description("Show what the Read handler would do
2015
2182
  return;
2016
2183
  }
2017
2184
  if (decision === "allow") {
2018
- console.log(chalk.cyan(" Decision: ALLOW (with additionalContext)"));
2185
+ console.log(chalk2.cyan(" Decision: ALLOW (with additionalContext)"));
2019
2186
  const ctx = wrapped.hookSpecificOutput?.additionalContext ?? "";
2020
2187
  if (ctx) {
2021
- console.log(chalk.dim(" Additional context that would be injected:"));
2188
+ console.log(chalk2.dim(" Additional context that would be injected:"));
2022
2189
  console.log(
2023
2190
  ctx.split("\n").map((l) => " " + l).join("\n")
2024
2191
  );
2025
2192
  }
2026
2193
  return;
2027
2194
  }
2028
- console.log(chalk.yellow(` Decision: ${decision ?? "unknown"}`));
2195
+ console.log(chalk2.yellow(` Decision: ${decision ?? "unknown"}`));
2029
2196
  });
2030
2197
  program.command("hook-disable").description("Disable engram hooks via kill switch (does not uninstall)").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
2031
2198
  const absProject = pathResolve(opts.project);
2032
2199
  const projectRoot = findProjectRoot(absProject);
2033
2200
  if (!projectRoot) {
2034
2201
  console.error(
2035
- chalk.red(`Not an engram project: ${absProject}`)
2202
+ chalk2.red(`Not an engram project: ${absProject}`)
2036
2203
  );
2037
- console.error(chalk.dim("Run 'engram init' first."));
2204
+ console.error(chalk2.dim("Run 'engram init' first."));
2038
2205
  process.exit(1);
2039
2206
  }
2040
- const flagPath = join7(projectRoot, ".engram", "hook-disabled");
2207
+ const flagPath = join8(projectRoot, ".engram", "hook-disabled");
2041
2208
  try {
2042
2209
  writeFileSync2(flagPath, (/* @__PURE__ */ new Date()).toISOString());
2043
2210
  console.log(
2044
- chalk.green(`\u2705 engram hooks disabled for ${projectRoot}`)
2211
+ chalk2.green(`\u2705 engram hooks disabled for ${projectRoot}`)
2045
2212
  );
2046
- console.log(chalk.dim(` Flag: ${flagPath}`));
2213
+ console.log(chalk2.dim(` Flag: ${flagPath}`));
2047
2214
  console.log(
2048
- chalk.dim(" Run 'engram hook-enable' to re-enable.")
2215
+ chalk2.dim(" Run 'engram hook-enable' to re-enable.")
2049
2216
  );
2050
2217
  } catch (err) {
2051
2218
  console.error(
2052
- chalk.red(`Failed to create flag: ${err.message}`)
2219
+ chalk2.red(`Failed to create flag: ${err.message}`)
2053
2220
  );
2054
2221
  process.exit(1);
2055
2222
  }
@@ -2058,24 +2225,24 @@ program.command("hook-enable").description("Re-enable engram hooks (remove kill
2058
2225
  const absProject = pathResolve(opts.project);
2059
2226
  const projectRoot = findProjectRoot(absProject);
2060
2227
  if (!projectRoot) {
2061
- console.error(chalk.red(`Not an engram project: ${absProject}`));
2228
+ console.error(chalk2.red(`Not an engram project: ${absProject}`));
2062
2229
  process.exit(1);
2063
2230
  }
2064
- const flagPath = join7(projectRoot, ".engram", "hook-disabled");
2065
- if (!existsSync7(flagPath)) {
2231
+ const flagPath = join8(projectRoot, ".engram", "hook-disabled");
2232
+ if (!existsSync8(flagPath)) {
2066
2233
  console.log(
2067
- chalk.yellow(`engram hooks already enabled for ${projectRoot}`)
2234
+ chalk2.yellow(`engram hooks already enabled for ${projectRoot}`)
2068
2235
  );
2069
2236
  return;
2070
2237
  }
2071
2238
  try {
2072
2239
  unlinkSync(flagPath);
2073
2240
  console.log(
2074
- chalk.green(`\u2705 engram hooks re-enabled for ${projectRoot}`)
2241
+ chalk2.green(`\u2705 engram hooks re-enabled for ${projectRoot}`)
2075
2242
  );
2076
2243
  } catch (err) {
2077
2244
  console.error(
2078
- chalk.red(`Failed to remove flag: ${err.message}`)
2245
+ chalk2.red(`Failed to remove flag: ${err.message}`)
2079
2246
  );
2080
2247
  process.exit(1);
2081
2248
  }
@@ -2088,9 +2255,9 @@ program.command("memory-sync").description(
2088
2255
  const projectRoot = findProjectRoot(absProject);
2089
2256
  if (!projectRoot) {
2090
2257
  console.error(
2091
- chalk.red(`Not an engram project: ${absProject}`)
2258
+ chalk2.red(`Not an engram project: ${absProject}`)
2092
2259
  );
2093
- console.error(chalk.dim("Run 'engram init' first."));
2260
+ console.error(chalk2.dim("Run 'engram init' first."));
2094
2261
  process.exit(1);
2095
2262
  }
2096
2263
  const [gods, mistakeList, graphStats] = await Promise.all([
@@ -2099,21 +2266,21 @@ program.command("memory-sync").description(
2099
2266
  stats(projectRoot).catch(() => null)
2100
2267
  ]);
2101
2268
  if (!graphStats) {
2102
- console.error(chalk.red("Failed to read graph stats."));
2269
+ console.error(chalk2.red("Failed to read graph stats."));
2103
2270
  process.exit(1);
2104
2271
  }
2105
2272
  let branch = null;
2106
2273
  try {
2107
- const headPath = join7(projectRoot, ".git", "HEAD");
2108
- if (existsSync7(headPath)) {
2109
- const content = readFileSync4(headPath, "utf-8").trim();
2274
+ const headPath = join8(projectRoot, ".git", "HEAD");
2275
+ if (existsSync8(headPath)) {
2276
+ const content = readFileSync5(headPath, "utf-8").trim();
2110
2277
  const m = content.match(/^ref:\s+refs\/heads\/(.+)$/);
2111
2278
  if (m) branch = m[1];
2112
2279
  }
2113
2280
  } catch {
2114
2281
  }
2115
2282
  const section = buildEngramSection({
2116
- projectName: basename4(projectRoot),
2283
+ projectName: basename5(projectRoot),
2117
2284
  branch,
2118
2285
  stats: {
2119
2286
  nodes: graphStats.nodes,
@@ -2128,37 +2295,37 @@ program.command("memory-sync").description(
2128
2295
  lastMined: graphStats.lastMined
2129
2296
  });
2130
2297
  console.log(
2131
- chalk.bold(`
2298
+ chalk2.bold(`
2132
2299
  \u{1F4DD} engram memory-sync`)
2133
2300
  );
2134
2301
  console.log(
2135
- chalk.dim(` Target: ${join7(projectRoot, "MEMORY.md")}`)
2302
+ chalk2.dim(` Target: ${join8(projectRoot, "MEMORY.md")}`)
2136
2303
  );
2137
2304
  if (opts.dryRun) {
2138
- console.log(chalk.cyan("\n Section to write (dry-run):\n"));
2305
+ console.log(chalk2.cyan("\n Section to write (dry-run):\n"));
2139
2306
  console.log(
2140
2307
  section.split("\n").map((l) => " " + l).join("\n")
2141
2308
  );
2142
- console.log(chalk.dim("\n (dry-run \u2014 no changes written)"));
2309
+ console.log(chalk2.dim("\n (dry-run \u2014 no changes written)"));
2143
2310
  return;
2144
2311
  }
2145
2312
  const ok = writeEngramSectionToMemoryMd(projectRoot, section);
2146
2313
  if (!ok) {
2147
2314
  console.error(
2148
- chalk.red(
2315
+ chalk2.red(
2149
2316
  "\n \u274C Write failed. MEMORY.md may be too large, or the engram section exceeded its size cap."
2150
2317
  )
2151
2318
  );
2152
2319
  process.exit(1);
2153
2320
  }
2154
2321
  console.log(
2155
- chalk.green(
2322
+ chalk2.green(
2156
2323
  `
2157
2324
  \u2705 Synced ${gods.length} god nodes${mistakeList.length > 0 ? ` and ${mistakeList.length} landmines` : ""} to MEMORY.md`
2158
2325
  )
2159
2326
  );
2160
2327
  console.log(
2161
- chalk.dim(
2328
+ chalk2.dim(
2162
2329
  `
2163
2330
  Next: Anthropic's Auto-Dream will consolidate this alongside its prose entries.
2164
2331
  `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engramx",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
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": {