engramx 0.4.0 → 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.
- package/dist/cli.js +383 -213
- 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
|
|
27
|
+
import chalk2 from "chalk";
|
|
28
28
|
import {
|
|
29
|
-
existsSync as
|
|
30
|
-
readFileSync as
|
|
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
|
|
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((
|
|
48
|
-
timer = setTimeout(() =>
|
|
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
|
|
1430
|
-
readFileSync as
|
|
1577
|
+
existsSync as existsSync7,
|
|
1578
|
+
readFileSync as readFileSync4,
|
|
1431
1579
|
writeFileSync,
|
|
1432
1580
|
renameSync as renameSync2,
|
|
1433
|
-
statSync as
|
|
1581
|
+
statSync as statSync5
|
|
1434
1582
|
} from "fs";
|
|
1435
|
-
import { join as
|
|
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 =
|
|
1654
|
+
const memoryPath = join7(projectRoot, "MEMORY.md");
|
|
1507
1655
|
try {
|
|
1508
1656
|
let existing = "";
|
|
1509
|
-
if (
|
|
1510
|
-
const st =
|
|
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 =
|
|
1662
|
+
existing = readFileSync4(memoryPath, "utf-8");
|
|
1515
1663
|
}
|
|
1516
1664
|
const updated = upsertEngramSection(existing, engramSection);
|
|
1517
1665
|
const tmpPath = memoryPath + ".engram-tmp";
|
|
@@ -1524,55 +1672,58 @@ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
|
|
|
1524
1672
|
}
|
|
1525
1673
|
|
|
1526
1674
|
// src/cli.ts
|
|
1527
|
-
import { basename as
|
|
1675
|
+
import { basename as basename5 } from "path";
|
|
1676
|
+
import { createRequire } from "module";
|
|
1677
|
+
var require2 = createRequire(import.meta.url);
|
|
1678
|
+
var { version: PKG_VERSION } = require2("../package.json");
|
|
1528
1679
|
var program = new Command();
|
|
1529
1680
|
program.name("engram").description(
|
|
1530
1681
|
"Context as infra for AI coding tools \u2014 hook-based Read/Edit interception + structural graph summaries"
|
|
1531
|
-
).version(
|
|
1682
|
+
).version(PKG_VERSION);
|
|
1532
1683
|
program.command("init").description("Scan codebase and build knowledge graph (zero LLM cost)").argument("[path]", "Project directory", ".").option(
|
|
1533
1684
|
"--with-skills [dir]",
|
|
1534
1685
|
"Also index Claude Code skills from ~/.claude/skills/ or a given path"
|
|
1535
1686
|
).action(async (projectPath, opts) => {
|
|
1536
|
-
console.log(
|
|
1687
|
+
console.log(chalk2.dim("\u{1F50D} Scanning codebase..."));
|
|
1537
1688
|
const result = await init(projectPath, {
|
|
1538
1689
|
withSkills: opts.withSkills
|
|
1539
1690
|
});
|
|
1540
1691
|
console.log(
|
|
1541
|
-
|
|
1692
|
+
chalk2.green("\u{1F333} AST extraction complete") + chalk2.dim(` (${result.timeMs}ms, 0 tokens used)`)
|
|
1542
1693
|
);
|
|
1543
1694
|
console.log(
|
|
1544
|
-
` ${
|
|
1695
|
+
` ${chalk2.bold(String(result.nodes))} nodes, ${chalk2.bold(String(result.edges))} edges from ${chalk2.bold(String(result.fileCount))} files (${result.totalLines.toLocaleString()} lines)`
|
|
1545
1696
|
);
|
|
1546
1697
|
if (result.skillCount && result.skillCount > 0) {
|
|
1547
1698
|
console.log(
|
|
1548
|
-
|
|
1699
|
+
chalk2.cyan(` ${chalk2.bold(String(result.skillCount))} skills indexed`)
|
|
1549
1700
|
);
|
|
1550
1701
|
}
|
|
1551
1702
|
const bench = await benchmark(projectPath);
|
|
1552
1703
|
if (bench.naiveFullCorpus > 0 && bench.reductionVsRelevant > 1) {
|
|
1553
1704
|
console.log(
|
|
1554
|
-
|
|
1555
|
-
\u{1F4CA} Token savings: ${
|
|
1705
|
+
chalk2.cyan(`
|
|
1706
|
+
\u{1F4CA} Token savings: ${chalk2.bold(bench.reductionVsRelevant + "x")} fewer tokens vs relevant files (${bench.reductionVsFull}x vs full corpus)`)
|
|
1556
1707
|
);
|
|
1557
1708
|
console.log(
|
|
1558
|
-
|
|
1709
|
+
chalk2.dim(` Full corpus: ~${bench.naiveFullCorpus.toLocaleString()} tokens | Graph query: ~${bench.avgQueryTokens.toLocaleString()} tokens`)
|
|
1559
1710
|
);
|
|
1560
1711
|
}
|
|
1561
|
-
console.log(
|
|
1562
|
-
console.log(
|
|
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"));
|
|
1563
1714
|
const resolvedProject = pathResolve(projectPath);
|
|
1564
|
-
const localSettings =
|
|
1565
|
-
const projectSettings =
|
|
1566
|
-
const hasHooks =
|
|
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");
|
|
1567
1718
|
if (!hasHooks) {
|
|
1568
1719
|
console.log(
|
|
1569
|
-
|
|
1720
|
+
chalk2.yellow("\n\u{1F4A1} Next step: ") + chalk2.white("engram install-hook") + chalk2.dim(
|
|
1570
1721
|
" \u2014 enables automatic Read interception (82% token savings)"
|
|
1571
1722
|
)
|
|
1572
1723
|
);
|
|
1573
1724
|
console.log(
|
|
1574
|
-
|
|
1575
|
-
" Also recommended: " +
|
|
1725
|
+
chalk2.dim(
|
|
1726
|
+
" Also recommended: " + chalk2.white("engram hooks install") + " \u2014 auto-rebuild graph on git commit"
|
|
1576
1727
|
)
|
|
1577
1728
|
);
|
|
1578
1729
|
}
|
|
@@ -1580,24 +1731,43 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
|
|
|
1580
1731
|
program.command("watch").description("Watch project for file changes and re-index incrementally").argument("[path]", "Project directory", ".").action(async (projectPath) => {
|
|
1581
1732
|
const resolvedPath = pathResolve(projectPath);
|
|
1582
1733
|
console.log(
|
|
1583
|
-
|
|
1734
|
+
chalk2.dim("\u{1F441} Watching ") + chalk2.white(resolvedPath) + chalk2.dim(" for changes...")
|
|
1584
1735
|
);
|
|
1585
1736
|
const controller = watchProject(resolvedPath, {
|
|
1586
1737
|
onReindex: (filePath, nodeCount) => {
|
|
1587
1738
|
console.log(
|
|
1588
|
-
|
|
1739
|
+
chalk2.green(" \u21BB ") + chalk2.white(filePath) + chalk2.dim(` (${nodeCount} nodes)`)
|
|
1589
1740
|
);
|
|
1590
1741
|
},
|
|
1591
1742
|
onError: (err) => {
|
|
1592
|
-
console.error(
|
|
1743
|
+
console.error(chalk2.red(" \u2717 ") + err.message);
|
|
1593
1744
|
},
|
|
1594
1745
|
onReady: () => {
|
|
1595
|
-
console.log(
|
|
1746
|
+
console.log(chalk2.green(" \u2713 Watcher active.") + chalk2.dim(" Press Ctrl+C to stop."));
|
|
1596
1747
|
}
|
|
1597
1748
|
});
|
|
1598
1749
|
process.on("SIGINT", () => {
|
|
1599
1750
|
controller.abort();
|
|
1600
|
-
console.log(
|
|
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."));
|
|
1601
1771
|
process.exit(0);
|
|
1602
1772
|
});
|
|
1603
1773
|
await new Promise(() => {
|
|
@@ -1610,10 +1780,10 @@ program.command("query").description("Query the knowledge graph").argument("<que
|
|
|
1610
1780
|
tokenBudget: Number(opts.budget)
|
|
1611
1781
|
});
|
|
1612
1782
|
if (result.nodesFound === 0) {
|
|
1613
|
-
console.log(
|
|
1783
|
+
console.log(chalk2.yellow("No matching nodes found."));
|
|
1614
1784
|
return;
|
|
1615
1785
|
}
|
|
1616
|
-
console.log(
|
|
1786
|
+
console.log(chalk2.dim(`Found ${result.nodesFound} nodes (~${result.estimatedTokens} tokens)
|
|
1617
1787
|
`));
|
|
1618
1788
|
console.log(result.text);
|
|
1619
1789
|
});
|
|
@@ -1624,25 +1794,25 @@ program.command("path").description("Find shortest path between two concepts").a
|
|
|
1624
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) => {
|
|
1625
1795
|
const gods = await godNodes(opts.project, Number(opts.top));
|
|
1626
1796
|
if (gods.length === 0) {
|
|
1627
|
-
console.log(
|
|
1797
|
+
console.log(chalk2.yellow("No nodes found. Run `engram init` first."));
|
|
1628
1798
|
return;
|
|
1629
1799
|
}
|
|
1630
|
-
console.log(
|
|
1800
|
+
console.log(chalk2.bold("God nodes (most connected):\n"));
|
|
1631
1801
|
for (let i = 0; i < gods.length; i++) {
|
|
1632
1802
|
const g = gods[i];
|
|
1633
1803
|
console.log(
|
|
1634
|
-
` ${
|
|
1804
|
+
` ${chalk2.dim(String(i + 1) + ".")} ${chalk2.bold(g.label)} ${chalk2.dim(`[${g.kind}]`)} \u2014 ${g.degree} edges ${chalk2.dim(g.sourceFile)}`
|
|
1635
1805
|
);
|
|
1636
1806
|
}
|
|
1637
1807
|
});
|
|
1638
1808
|
program.command("stats").description("Show knowledge graph statistics and token savings").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
|
|
1639
1809
|
const s = await stats(opts.project);
|
|
1640
1810
|
const bench = await benchmark(opts.project);
|
|
1641
|
-
console.log(
|
|
1642
|
-
console.log(` Nodes: ${
|
|
1643
|
-
console.log(` 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))}`);
|
|
1644
1814
|
console.log(
|
|
1645
|
-
` Confidence: ${
|
|
1815
|
+
` Confidence: ${chalk2.green(s.extractedPct + "% EXTRACTED")} \xB7 ${chalk2.yellow(s.inferredPct + "% INFERRED")} \xB7 ${chalk2.red(s.ambiguousPct + "% AMBIGUOUS")}`
|
|
1646
1816
|
);
|
|
1647
1817
|
if (s.lastMined > 0) {
|
|
1648
1818
|
const ago = Math.round((Date.now() - s.lastMined) / 6e4);
|
|
@@ -1650,20 +1820,20 @@ program.command("stats").description("Show knowledge graph statistics and token
|
|
|
1650
1820
|
}
|
|
1651
1821
|
if (bench.naiveFullCorpus > 0) {
|
|
1652
1822
|
console.log(`
|
|
1653
|
-
${
|
|
1823
|
+
${chalk2.cyan("Token savings:")}`);
|
|
1654
1824
|
console.log(` Full corpus: ~${bench.naiveFullCorpus.toLocaleString()} tokens`);
|
|
1655
1825
|
console.log(` Avg query: ~${bench.avgQueryTokens.toLocaleString()} tokens`);
|
|
1656
|
-
console.log(` vs relevant: ${
|
|
1657
|
-
console.log(` vs full: ${
|
|
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`);
|
|
1658
1828
|
}
|
|
1659
1829
|
console.log();
|
|
1660
1830
|
});
|
|
1661
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) => {
|
|
1662
1832
|
const result = await learn(opts.project, text);
|
|
1663
1833
|
if (result.nodesAdded > 0) {
|
|
1664
|
-
console.log(
|
|
1834
|
+
console.log(chalk2.green(`\u{1F9E0} Learned ${result.nodesAdded} new insight(s).`));
|
|
1665
1835
|
} else {
|
|
1666
|
-
console.log(
|
|
1836
|
+
console.log(chalk2.yellow("No patterns extracted. Try a more specific statement."));
|
|
1667
1837
|
}
|
|
1668
1838
|
});
|
|
1669
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(
|
|
@@ -1673,11 +1843,11 @@ program.command("mistakes").description("List known mistakes extracted from past
|
|
|
1673
1843
|
sinceDays: opts.since ? Number(opts.since) : void 0
|
|
1674
1844
|
});
|
|
1675
1845
|
if (result.length === 0) {
|
|
1676
|
-
console.log(
|
|
1846
|
+
console.log(chalk2.yellow("No mistakes recorded."));
|
|
1677
1847
|
return;
|
|
1678
1848
|
}
|
|
1679
1849
|
console.log(
|
|
1680
|
-
|
|
1850
|
+
chalk2.bold(`
|
|
1681
1851
|
\u26A0\uFE0F ${result.length} mistake(s) recorded:
|
|
1682
1852
|
`)
|
|
1683
1853
|
);
|
|
@@ -1687,7 +1857,7 @@ program.command("mistakes").description("List known mistakes extracted from past
|
|
|
1687
1857
|
Math.round((Date.now() - m.lastVerified) / 864e5)
|
|
1688
1858
|
);
|
|
1689
1859
|
console.log(
|
|
1690
|
-
` ${
|
|
1860
|
+
` ${chalk2.dim(`[${m.sourceFile}, ${ago}d ago]`)} ${m.label}`
|
|
1691
1861
|
);
|
|
1692
1862
|
}
|
|
1693
1863
|
console.log();
|
|
@@ -1695,14 +1865,14 @@ program.command("mistakes").description("List known mistakes extracted from past
|
|
|
1695
1865
|
);
|
|
1696
1866
|
program.command("bench").description("Run token reduction benchmark").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
|
|
1697
1867
|
const result = await benchmark(opts.project);
|
|
1698
|
-
console.log(
|
|
1868
|
+
console.log(chalk2.bold("\n\u26A1 engram token reduction benchmark\n"));
|
|
1699
1869
|
console.log(` Full corpus: ~${result.naiveFullCorpus.toLocaleString()} tokens`);
|
|
1700
1870
|
console.log(` Avg graph query: ~${result.avgQueryTokens.toLocaleString()} tokens`);
|
|
1701
|
-
console.log(` vs relevant: ${
|
|
1702
|
-
console.log(` vs full corpus: ${
|
|
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
|
|
1703
1873
|
`);
|
|
1704
1874
|
for (const pq of result.perQuestion) {
|
|
1705
|
-
console.log(` ${
|
|
1875
|
+
console.log(` ${chalk2.dim(`[${pq.reductionRelevant}x relevant / ${pq.reductionFull}x full]`)} ${pq.question}`);
|
|
1706
1876
|
}
|
|
1707
1877
|
console.log();
|
|
1708
1878
|
});
|
|
@@ -1718,7 +1888,7 @@ program.command("gen").description("Generate CLAUDE.md / .cursorrules section fr
|
|
|
1718
1888
|
const target = opts.target;
|
|
1719
1889
|
const result = await autogen(opts.project, target, opts.task);
|
|
1720
1890
|
console.log(
|
|
1721
|
-
|
|
1891
|
+
chalk2.green(
|
|
1722
1892
|
`\u2705 Updated ${result.file} (${result.nodesIncluded} nodes, view: ${result.view})`
|
|
1723
1893
|
)
|
|
1724
1894
|
);
|
|
@@ -1728,11 +1898,11 @@ function resolveSettingsPath(scope, projectPath) {
|
|
|
1728
1898
|
const absProject = pathResolve(projectPath);
|
|
1729
1899
|
switch (scope) {
|
|
1730
1900
|
case "local":
|
|
1731
|
-
return
|
|
1901
|
+
return join8(absProject, ".claude", "settings.local.json");
|
|
1732
1902
|
case "project":
|
|
1733
|
-
return
|
|
1903
|
+
return join8(absProject, ".claude", "settings.json");
|
|
1734
1904
|
case "user":
|
|
1735
|
-
return
|
|
1905
|
+
return join8(homedir(), ".claude", "settings.json");
|
|
1736
1906
|
default:
|
|
1737
1907
|
return null;
|
|
1738
1908
|
}
|
|
@@ -1819,25 +1989,25 @@ program.command("install-hook").description("Install engram hook entries into Cl
|
|
|
1819
1989
|
const settingsPath = resolveSettingsPath(opts.scope, opts.project);
|
|
1820
1990
|
if (!settingsPath) {
|
|
1821
1991
|
console.error(
|
|
1822
|
-
|
|
1992
|
+
chalk2.red(
|
|
1823
1993
|
`Unknown scope: ${opts.scope} (expected: local | project | user)`
|
|
1824
1994
|
)
|
|
1825
1995
|
);
|
|
1826
1996
|
process.exit(1);
|
|
1827
1997
|
}
|
|
1828
1998
|
let existing = {};
|
|
1829
|
-
if (
|
|
1999
|
+
if (existsSync8(settingsPath)) {
|
|
1830
2000
|
try {
|
|
1831
|
-
const raw =
|
|
2001
|
+
const raw = readFileSync5(settingsPath, "utf-8");
|
|
1832
2002
|
existing = raw.trim() ? JSON.parse(raw) : {};
|
|
1833
2003
|
} catch (err) {
|
|
1834
2004
|
console.error(
|
|
1835
|
-
|
|
2005
|
+
chalk2.red(
|
|
1836
2006
|
`Failed to parse ${settingsPath}: ${err.message}`
|
|
1837
2007
|
)
|
|
1838
2008
|
);
|
|
1839
2009
|
console.error(
|
|
1840
|
-
|
|
2010
|
+
chalk2.dim(
|
|
1841
2011
|
"Fix the JSON syntax and re-run install-hook, or remove the file and start fresh."
|
|
1842
2012
|
)
|
|
1843
2013
|
);
|
|
@@ -1846,38 +2016,38 @@ program.command("install-hook").description("Install engram hook entries into Cl
|
|
|
1846
2016
|
}
|
|
1847
2017
|
const result = installEngramHooks(existing);
|
|
1848
2018
|
console.log(
|
|
1849
|
-
|
|
2019
|
+
chalk2.bold(`
|
|
1850
2020
|
\u{1F4CC} engram install-hook (scope: ${opts.scope})`)
|
|
1851
2021
|
);
|
|
1852
|
-
console.log(
|
|
2022
|
+
console.log(chalk2.dim(` Target: ${settingsPath}`));
|
|
1853
2023
|
if (result.added.length === 0) {
|
|
1854
2024
|
console.log(
|
|
1855
|
-
|
|
2025
|
+
chalk2.yellow(
|
|
1856
2026
|
`
|
|
1857
2027
|
All engram hooks already installed (${result.alreadyPresent.join(", ")}).`
|
|
1858
2028
|
)
|
|
1859
2029
|
);
|
|
1860
2030
|
console.log(
|
|
1861
|
-
|
|
2031
|
+
chalk2.dim(
|
|
1862
2032
|
" Run 'engram uninstall-hook' first if you want to reinstall."
|
|
1863
2033
|
)
|
|
1864
2034
|
);
|
|
1865
2035
|
return;
|
|
1866
2036
|
}
|
|
1867
|
-
console.log(
|
|
2037
|
+
console.log(chalk2.cyan("\n Changes:"));
|
|
1868
2038
|
console.log(
|
|
1869
2039
|
formatInstallDiff(existing, result.updated).split("\n").map((l) => " " + l).join("\n")
|
|
1870
2040
|
);
|
|
1871
2041
|
if (opts.dryRun) {
|
|
1872
|
-
console.log(
|
|
2042
|
+
console.log(chalk2.dim("\n (dry-run \u2014 no changes written)"));
|
|
1873
2043
|
return;
|
|
1874
2044
|
}
|
|
1875
2045
|
try {
|
|
1876
2046
|
mkdirSync(dirname3(settingsPath), { recursive: true });
|
|
1877
|
-
if (
|
|
2047
|
+
if (existsSync8(settingsPath)) {
|
|
1878
2048
|
const backupPath = `${settingsPath}.engram-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
|
|
1879
2049
|
copyFileSync(settingsPath, backupPath);
|
|
1880
|
-
console.log(
|
|
2050
|
+
console.log(chalk2.dim(` Backup: ${backupPath}`));
|
|
1881
2051
|
}
|
|
1882
2052
|
const tmpPath = settingsPath + ".engram-tmp";
|
|
1883
2053
|
writeFileSync2(
|
|
@@ -1887,26 +2057,26 @@ program.command("install-hook").description("Install engram hook entries into Cl
|
|
|
1887
2057
|
renameSync3(tmpPath, settingsPath);
|
|
1888
2058
|
} catch (err) {
|
|
1889
2059
|
console.error(
|
|
1890
|
-
|
|
2060
|
+
chalk2.red(`
|
|
1891
2061
|
\u274C Write failed: ${err.message}`)
|
|
1892
2062
|
);
|
|
1893
2063
|
process.exit(1);
|
|
1894
2064
|
}
|
|
1895
2065
|
console.log(
|
|
1896
|
-
|
|
2066
|
+
chalk2.green(
|
|
1897
2067
|
`
|
|
1898
2068
|
\u2705 Installed ${result.added.length} hook event${result.added.length === 1 ? "" : "s"}: ${result.added.join(", ")}`
|
|
1899
2069
|
)
|
|
1900
2070
|
);
|
|
1901
2071
|
if (result.alreadyPresent.length > 0) {
|
|
1902
2072
|
console.log(
|
|
1903
|
-
|
|
2073
|
+
chalk2.dim(
|
|
1904
2074
|
` Already present: ${result.alreadyPresent.join(", ")}`
|
|
1905
2075
|
)
|
|
1906
2076
|
);
|
|
1907
2077
|
}
|
|
1908
2078
|
console.log(
|
|
1909
|
-
|
|
2079
|
+
chalk2.dim(
|
|
1910
2080
|
"\n Next: open a Claude Code session and engram will start intercepting tool calls."
|
|
1911
2081
|
)
|
|
1912
2082
|
);
|
|
@@ -1915,29 +2085,29 @@ program.command("install-hook").description("Install engram hook entries into Cl
|
|
|
1915
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) => {
|
|
1916
2086
|
const settingsPath = resolveSettingsPath(opts.scope, opts.project);
|
|
1917
2087
|
if (!settingsPath) {
|
|
1918
|
-
console.error(
|
|
2088
|
+
console.error(chalk2.red(`Unknown scope: ${opts.scope}`));
|
|
1919
2089
|
process.exit(1);
|
|
1920
2090
|
}
|
|
1921
|
-
if (!
|
|
2091
|
+
if (!existsSync8(settingsPath)) {
|
|
1922
2092
|
console.log(
|
|
1923
|
-
|
|
2093
|
+
chalk2.yellow(`No settings file at ${settingsPath} \u2014 nothing to remove.`)
|
|
1924
2094
|
);
|
|
1925
2095
|
return;
|
|
1926
2096
|
}
|
|
1927
2097
|
let existing;
|
|
1928
2098
|
try {
|
|
1929
|
-
const raw =
|
|
2099
|
+
const raw = readFileSync5(settingsPath, "utf-8");
|
|
1930
2100
|
existing = raw.trim() ? JSON.parse(raw) : {};
|
|
1931
2101
|
} catch (err) {
|
|
1932
2102
|
console.error(
|
|
1933
|
-
|
|
2103
|
+
chalk2.red(`Failed to parse ${settingsPath}: ${err.message}`)
|
|
1934
2104
|
);
|
|
1935
2105
|
process.exit(1);
|
|
1936
2106
|
}
|
|
1937
2107
|
const result = uninstallEngramHooks(existing);
|
|
1938
2108
|
if (result.removed.length === 0) {
|
|
1939
2109
|
console.log(
|
|
1940
|
-
|
|
2110
|
+
chalk2.yellow(`
|
|
1941
2111
|
No engram hooks found in ${settingsPath}.`)
|
|
1942
2112
|
);
|
|
1943
2113
|
return;
|
|
@@ -1949,15 +2119,15 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
|
|
|
1949
2119
|
writeFileSync2(tmpPath, JSON.stringify(result.updated, null, 2) + "\n");
|
|
1950
2120
|
renameSync3(tmpPath, settingsPath);
|
|
1951
2121
|
console.log(
|
|
1952
|
-
|
|
2122
|
+
chalk2.green(
|
|
1953
2123
|
`
|
|
1954
2124
|
\u2705 Removed engram hooks from ${result.removed.length} event${result.removed.length === 1 ? "" : "s"}: ${result.removed.join(", ")}`
|
|
1955
2125
|
)
|
|
1956
2126
|
);
|
|
1957
|
-
console.log(
|
|
2127
|
+
console.log(chalk2.dim(` Backup: ${backupPath}`));
|
|
1958
2128
|
} catch (err) {
|
|
1959
2129
|
console.error(
|
|
1960
|
-
|
|
2130
|
+
chalk2.red(`
|
|
1961
2131
|
\u274C Write failed: ${err.message}`)
|
|
1962
2132
|
);
|
|
1963
2133
|
process.exit(1);
|
|
@@ -1984,16 +2154,16 @@ program.command("hook-preview").description("Show what the Read handler would do
|
|
|
1984
2154
|
tool_input: { file_path: absFile }
|
|
1985
2155
|
};
|
|
1986
2156
|
const result = await dispatchHook(payload);
|
|
1987
|
-
console.log(
|
|
2157
|
+
console.log(chalk2.bold(`
|
|
1988
2158
|
\u{1F4CB} Hook preview: ${absFile}`));
|
|
1989
|
-
console.log(
|
|
2159
|
+
console.log(chalk2.dim(` Project: ${absProject}`));
|
|
1990
2160
|
console.log();
|
|
1991
2161
|
if (result === null || result === void 0) {
|
|
1992
2162
|
console.log(
|
|
1993
|
-
|
|
2163
|
+
chalk2.yellow(" Decision: PASSTHROUGH (Read would execute normally)")
|
|
1994
2164
|
);
|
|
1995
2165
|
console.log(
|
|
1996
|
-
|
|
2166
|
+
chalk2.dim(
|
|
1997
2167
|
" Possible reasons: file not in graph, confidence below threshold, content unsafe, outside project, stale graph."
|
|
1998
2168
|
)
|
|
1999
2169
|
);
|
|
@@ -2002,8 +2172,8 @@ program.command("hook-preview").description("Show what the Read handler would do
|
|
|
2002
2172
|
const wrapped = result;
|
|
2003
2173
|
const decision = wrapped.hookSpecificOutput?.permissionDecision;
|
|
2004
2174
|
if (decision === "deny") {
|
|
2005
|
-
console.log(
|
|
2006
|
-
console.log(
|
|
2175
|
+
console.log(chalk2.green(" Decision: DENY (Read would be replaced)"));
|
|
2176
|
+
console.log(chalk2.dim(" Summary (would be delivered to Claude):"));
|
|
2007
2177
|
console.log();
|
|
2008
2178
|
const reason = wrapped.hookSpecificOutput?.permissionDecisionReason ?? "";
|
|
2009
2179
|
console.log(
|
|
@@ -2012,41 +2182,41 @@ program.command("hook-preview").description("Show what the Read handler would do
|
|
|
2012
2182
|
return;
|
|
2013
2183
|
}
|
|
2014
2184
|
if (decision === "allow") {
|
|
2015
|
-
console.log(
|
|
2185
|
+
console.log(chalk2.cyan(" Decision: ALLOW (with additionalContext)"));
|
|
2016
2186
|
const ctx = wrapped.hookSpecificOutput?.additionalContext ?? "";
|
|
2017
2187
|
if (ctx) {
|
|
2018
|
-
console.log(
|
|
2188
|
+
console.log(chalk2.dim(" Additional context that would be injected:"));
|
|
2019
2189
|
console.log(
|
|
2020
2190
|
ctx.split("\n").map((l) => " " + l).join("\n")
|
|
2021
2191
|
);
|
|
2022
2192
|
}
|
|
2023
2193
|
return;
|
|
2024
2194
|
}
|
|
2025
|
-
console.log(
|
|
2195
|
+
console.log(chalk2.yellow(` Decision: ${decision ?? "unknown"}`));
|
|
2026
2196
|
});
|
|
2027
2197
|
program.command("hook-disable").description("Disable engram hooks via kill switch (does not uninstall)").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
|
|
2028
2198
|
const absProject = pathResolve(opts.project);
|
|
2029
2199
|
const projectRoot = findProjectRoot(absProject);
|
|
2030
2200
|
if (!projectRoot) {
|
|
2031
2201
|
console.error(
|
|
2032
|
-
|
|
2202
|
+
chalk2.red(`Not an engram project: ${absProject}`)
|
|
2033
2203
|
);
|
|
2034
|
-
console.error(
|
|
2204
|
+
console.error(chalk2.dim("Run 'engram init' first."));
|
|
2035
2205
|
process.exit(1);
|
|
2036
2206
|
}
|
|
2037
|
-
const flagPath =
|
|
2207
|
+
const flagPath = join8(projectRoot, ".engram", "hook-disabled");
|
|
2038
2208
|
try {
|
|
2039
2209
|
writeFileSync2(flagPath, (/* @__PURE__ */ new Date()).toISOString());
|
|
2040
2210
|
console.log(
|
|
2041
|
-
|
|
2211
|
+
chalk2.green(`\u2705 engram hooks disabled for ${projectRoot}`)
|
|
2042
2212
|
);
|
|
2043
|
-
console.log(
|
|
2213
|
+
console.log(chalk2.dim(` Flag: ${flagPath}`));
|
|
2044
2214
|
console.log(
|
|
2045
|
-
|
|
2215
|
+
chalk2.dim(" Run 'engram hook-enable' to re-enable.")
|
|
2046
2216
|
);
|
|
2047
2217
|
} catch (err) {
|
|
2048
2218
|
console.error(
|
|
2049
|
-
|
|
2219
|
+
chalk2.red(`Failed to create flag: ${err.message}`)
|
|
2050
2220
|
);
|
|
2051
2221
|
process.exit(1);
|
|
2052
2222
|
}
|
|
@@ -2055,24 +2225,24 @@ program.command("hook-enable").description("Re-enable engram hooks (remove kill
|
|
|
2055
2225
|
const absProject = pathResolve(opts.project);
|
|
2056
2226
|
const projectRoot = findProjectRoot(absProject);
|
|
2057
2227
|
if (!projectRoot) {
|
|
2058
|
-
console.error(
|
|
2228
|
+
console.error(chalk2.red(`Not an engram project: ${absProject}`));
|
|
2059
2229
|
process.exit(1);
|
|
2060
2230
|
}
|
|
2061
|
-
const flagPath =
|
|
2062
|
-
if (!
|
|
2231
|
+
const flagPath = join8(projectRoot, ".engram", "hook-disabled");
|
|
2232
|
+
if (!existsSync8(flagPath)) {
|
|
2063
2233
|
console.log(
|
|
2064
|
-
|
|
2234
|
+
chalk2.yellow(`engram hooks already enabled for ${projectRoot}`)
|
|
2065
2235
|
);
|
|
2066
2236
|
return;
|
|
2067
2237
|
}
|
|
2068
2238
|
try {
|
|
2069
2239
|
unlinkSync(flagPath);
|
|
2070
2240
|
console.log(
|
|
2071
|
-
|
|
2241
|
+
chalk2.green(`\u2705 engram hooks re-enabled for ${projectRoot}`)
|
|
2072
2242
|
);
|
|
2073
2243
|
} catch (err) {
|
|
2074
2244
|
console.error(
|
|
2075
|
-
|
|
2245
|
+
chalk2.red(`Failed to remove flag: ${err.message}`)
|
|
2076
2246
|
);
|
|
2077
2247
|
process.exit(1);
|
|
2078
2248
|
}
|
|
@@ -2085,9 +2255,9 @@ program.command("memory-sync").description(
|
|
|
2085
2255
|
const projectRoot = findProjectRoot(absProject);
|
|
2086
2256
|
if (!projectRoot) {
|
|
2087
2257
|
console.error(
|
|
2088
|
-
|
|
2258
|
+
chalk2.red(`Not an engram project: ${absProject}`)
|
|
2089
2259
|
);
|
|
2090
|
-
console.error(
|
|
2260
|
+
console.error(chalk2.dim("Run 'engram init' first."));
|
|
2091
2261
|
process.exit(1);
|
|
2092
2262
|
}
|
|
2093
2263
|
const [gods, mistakeList, graphStats] = await Promise.all([
|
|
@@ -2096,21 +2266,21 @@ program.command("memory-sync").description(
|
|
|
2096
2266
|
stats(projectRoot).catch(() => null)
|
|
2097
2267
|
]);
|
|
2098
2268
|
if (!graphStats) {
|
|
2099
|
-
console.error(
|
|
2269
|
+
console.error(chalk2.red("Failed to read graph stats."));
|
|
2100
2270
|
process.exit(1);
|
|
2101
2271
|
}
|
|
2102
2272
|
let branch = null;
|
|
2103
2273
|
try {
|
|
2104
|
-
const headPath =
|
|
2105
|
-
if (
|
|
2106
|
-
const content =
|
|
2274
|
+
const headPath = join8(projectRoot, ".git", "HEAD");
|
|
2275
|
+
if (existsSync8(headPath)) {
|
|
2276
|
+
const content = readFileSync5(headPath, "utf-8").trim();
|
|
2107
2277
|
const m = content.match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
2108
2278
|
if (m) branch = m[1];
|
|
2109
2279
|
}
|
|
2110
2280
|
} catch {
|
|
2111
2281
|
}
|
|
2112
2282
|
const section = buildEngramSection({
|
|
2113
|
-
projectName:
|
|
2283
|
+
projectName: basename5(projectRoot),
|
|
2114
2284
|
branch,
|
|
2115
2285
|
stats: {
|
|
2116
2286
|
nodes: graphStats.nodes,
|
|
@@ -2125,37 +2295,37 @@ program.command("memory-sync").description(
|
|
|
2125
2295
|
lastMined: graphStats.lastMined
|
|
2126
2296
|
});
|
|
2127
2297
|
console.log(
|
|
2128
|
-
|
|
2298
|
+
chalk2.bold(`
|
|
2129
2299
|
\u{1F4DD} engram memory-sync`)
|
|
2130
2300
|
);
|
|
2131
2301
|
console.log(
|
|
2132
|
-
|
|
2302
|
+
chalk2.dim(` Target: ${join8(projectRoot, "MEMORY.md")}`)
|
|
2133
2303
|
);
|
|
2134
2304
|
if (opts.dryRun) {
|
|
2135
|
-
console.log(
|
|
2305
|
+
console.log(chalk2.cyan("\n Section to write (dry-run):\n"));
|
|
2136
2306
|
console.log(
|
|
2137
2307
|
section.split("\n").map((l) => " " + l).join("\n")
|
|
2138
2308
|
);
|
|
2139
|
-
console.log(
|
|
2309
|
+
console.log(chalk2.dim("\n (dry-run \u2014 no changes written)"));
|
|
2140
2310
|
return;
|
|
2141
2311
|
}
|
|
2142
2312
|
const ok = writeEngramSectionToMemoryMd(projectRoot, section);
|
|
2143
2313
|
if (!ok) {
|
|
2144
2314
|
console.error(
|
|
2145
|
-
|
|
2315
|
+
chalk2.red(
|
|
2146
2316
|
"\n \u274C Write failed. MEMORY.md may be too large, or the engram section exceeded its size cap."
|
|
2147
2317
|
)
|
|
2148
2318
|
);
|
|
2149
2319
|
process.exit(1);
|
|
2150
2320
|
}
|
|
2151
2321
|
console.log(
|
|
2152
|
-
|
|
2322
|
+
chalk2.green(
|
|
2153
2323
|
`
|
|
2154
2324
|
\u2705 Synced ${gods.length} god nodes${mistakeList.length > 0 ? ` and ${mistakeList.length} landmines` : ""} to MEMORY.md`
|
|
2155
2325
|
)
|
|
2156
2326
|
);
|
|
2157
2327
|
console.log(
|
|
2158
|
-
|
|
2328
|
+
chalk2.dim(
|
|
2159
2329
|
`
|
|
2160
2330
|
Next: Anthropic's Auto-Dream will consolidate this alongside its prose entries.
|
|
2161
2331
|
`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engramx",
|
|
3
|
-
"version": "0.4.
|
|
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": {
|