engramx 0.4.1 → 0.4.3
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 +415 -212
- 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,7 +1672,7 @@ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
|
|
|
1524
1672
|
}
|
|
1525
1673
|
|
|
1526
1674
|
// src/cli.ts
|
|
1527
|
-
import { basename as
|
|
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(
|
|
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
|
-
|
|
1692
|
+
chalk2.green("\u{1F333} AST extraction complete") + chalk2.dim(` (${result.timeMs}ms, 0 tokens used)`)
|
|
1545
1693
|
);
|
|
1546
1694
|
console.log(
|
|
1547
|
-
` ${
|
|
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
|
-
|
|
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
|
-
|
|
1558
|
-
\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)`)
|
|
1559
1707
|
);
|
|
1560
1708
|
console.log(
|
|
1561
|
-
|
|
1709
|
+
chalk2.dim(` Full corpus: ~${bench.naiveFullCorpus.toLocaleString()} tokens | Graph query: ~${bench.avgQueryTokens.toLocaleString()} tokens`)
|
|
1562
1710
|
);
|
|
1563
1711
|
}
|
|
1564
|
-
console.log(
|
|
1565
|
-
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"));
|
|
1566
1714
|
const resolvedProject = pathResolve(projectPath);
|
|
1567
|
-
const localSettings =
|
|
1568
|
-
const projectSettings =
|
|
1569
|
-
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");
|
|
1570
1718
|
if (!hasHooks) {
|
|
1571
1719
|
console.log(
|
|
1572
|
-
|
|
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
|
-
|
|
1578
|
-
" Also recommended: " +
|
|
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,29 +1731,84 @@ 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
|
-
|
|
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
|
-
|
|
1739
|
+
chalk2.green(" \u21BB ") + chalk2.white(filePath) + chalk2.dim(` (${nodeCount} nodes)`)
|
|
1592
1740
|
);
|
|
1593
1741
|
},
|
|
1594
1742
|
onError: (err) => {
|
|
1595
|
-
console.error(
|
|
1743
|
+
console.error(chalk2.red(" \u2717 ") + err.message);
|
|
1596
1744
|
},
|
|
1597
1745
|
onReady: () => {
|
|
1598
|
-
console.log(
|
|
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(
|
|
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(() => {
|
|
1607
1774
|
});
|
|
1608
1775
|
});
|
|
1776
|
+
program.command("hud-label").description("Output JSON label for Claude HUD --extra-cmd (fast, <20ms)").argument("[path]", "Project directory", ".").action(async (projectPath) => {
|
|
1777
|
+
const resolvedPath = pathResolve(projectPath);
|
|
1778
|
+
const logPath = join8(resolvedPath, ".engram", "hook-log.jsonl");
|
|
1779
|
+
if (!existsSync8(join8(resolvedPath, ".engram", "graph.db"))) {
|
|
1780
|
+
console.log('{"label":""}');
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
if (!existsSync8(logPath)) {
|
|
1784
|
+
console.log('{"label":"\u26A1engram \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 ready"}');
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
try {
|
|
1788
|
+
const entries = readHookLog(resolvedPath);
|
|
1789
|
+
const summary = summarizeHookLog(entries);
|
|
1790
|
+
if (summary.totalInvocations === 0) {
|
|
1791
|
+
console.log('{"label":"\u26A1engram \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 listening..."}');
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
const totalPreTool = (summary.byDecision["deny"] ?? 0) + (summary.byDecision["allow"] ?? 0) + (summary.byDecision["passthrough"] ?? 0);
|
|
1795
|
+
const denied = summary.readDenyCount;
|
|
1796
|
+
const hitRate = totalPreTool > 0 ? Math.round(denied / totalPreTool * 100) : 0;
|
|
1797
|
+
const tokens = summary.estimatedTokensSaved;
|
|
1798
|
+
let formatted;
|
|
1799
|
+
if (tokens >= 1e6) formatted = (tokens / 1e6).toFixed(1) + "M";
|
|
1800
|
+
else if (tokens >= 1e3) formatted = (tokens / 1e3).toFixed(1) + "K";
|
|
1801
|
+
else formatted = String(tokens);
|
|
1802
|
+
const barWidth = 10;
|
|
1803
|
+
let filled = Math.round(hitRate / 100 * barWidth);
|
|
1804
|
+
if (filled > barWidth) filled = barWidth;
|
|
1805
|
+
if (denied > 0 && filled === 0) filled = 1;
|
|
1806
|
+
const bar2 = "\u25B0".repeat(filled) + "\u25B1".repeat(barWidth - filled);
|
|
1807
|
+
console.log(JSON.stringify({ label: `\u26A1engram ${formatted} saved ${bar2} ${hitRate}%` }));
|
|
1808
|
+
} catch {
|
|
1809
|
+
console.log('{"label":"\u26A1engram"}');
|
|
1810
|
+
}
|
|
1811
|
+
});
|
|
1609
1812
|
program.command("query").description("Query the knowledge graph").argument("<question>", "Natural language question or keywords").option("--dfs", "Use DFS traversal", false).option("-d, --depth <n>", "Traversal depth", "3").option("-b, --budget <n>", "Token budget", "2000").option("-p, --project <path>", "Project directory", ".").action(async (question, opts) => {
|
|
1610
1813
|
const result = await query(opts.project, question, {
|
|
1611
1814
|
mode: opts.dfs ? "dfs" : "bfs",
|
|
@@ -1613,10 +1816,10 @@ program.command("query").description("Query the knowledge graph").argument("<que
|
|
|
1613
1816
|
tokenBudget: Number(opts.budget)
|
|
1614
1817
|
});
|
|
1615
1818
|
if (result.nodesFound === 0) {
|
|
1616
|
-
console.log(
|
|
1819
|
+
console.log(chalk2.yellow("No matching nodes found."));
|
|
1617
1820
|
return;
|
|
1618
1821
|
}
|
|
1619
|
-
console.log(
|
|
1822
|
+
console.log(chalk2.dim(`Found ${result.nodesFound} nodes (~${result.estimatedTokens} tokens)
|
|
1620
1823
|
`));
|
|
1621
1824
|
console.log(result.text);
|
|
1622
1825
|
});
|
|
@@ -1627,25 +1830,25 @@ program.command("path").description("Find shortest path between two concepts").a
|
|
|
1627
1830
|
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
1831
|
const gods = await godNodes(opts.project, Number(opts.top));
|
|
1629
1832
|
if (gods.length === 0) {
|
|
1630
|
-
console.log(
|
|
1833
|
+
console.log(chalk2.yellow("No nodes found. Run `engram init` first."));
|
|
1631
1834
|
return;
|
|
1632
1835
|
}
|
|
1633
|
-
console.log(
|
|
1836
|
+
console.log(chalk2.bold("God nodes (most connected):\n"));
|
|
1634
1837
|
for (let i = 0; i < gods.length; i++) {
|
|
1635
1838
|
const g = gods[i];
|
|
1636
1839
|
console.log(
|
|
1637
|
-
` ${
|
|
1840
|
+
` ${chalk2.dim(String(i + 1) + ".")} ${chalk2.bold(g.label)} ${chalk2.dim(`[${g.kind}]`)} \u2014 ${g.degree} edges ${chalk2.dim(g.sourceFile)}`
|
|
1638
1841
|
);
|
|
1639
1842
|
}
|
|
1640
1843
|
});
|
|
1641
1844
|
program.command("stats").description("Show knowledge graph statistics and token savings").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
|
|
1642
1845
|
const s = await stats(opts.project);
|
|
1643
1846
|
const bench = await benchmark(opts.project);
|
|
1644
|
-
console.log(
|
|
1645
|
-
console.log(` Nodes: ${
|
|
1646
|
-
console.log(` Edges: ${
|
|
1847
|
+
console.log(chalk2.bold("\n\u{1F4CA} engram stats\n"));
|
|
1848
|
+
console.log(` Nodes: ${chalk2.bold(String(s.nodes))}`);
|
|
1849
|
+
console.log(` Edges: ${chalk2.bold(String(s.edges))}`);
|
|
1647
1850
|
console.log(
|
|
1648
|
-
` Confidence: ${
|
|
1851
|
+
` Confidence: ${chalk2.green(s.extractedPct + "% EXTRACTED")} \xB7 ${chalk2.yellow(s.inferredPct + "% INFERRED")} \xB7 ${chalk2.red(s.ambiguousPct + "% AMBIGUOUS")}`
|
|
1649
1852
|
);
|
|
1650
1853
|
if (s.lastMined > 0) {
|
|
1651
1854
|
const ago = Math.round((Date.now() - s.lastMined) / 6e4);
|
|
@@ -1653,20 +1856,20 @@ program.command("stats").description("Show knowledge graph statistics and token
|
|
|
1653
1856
|
}
|
|
1654
1857
|
if (bench.naiveFullCorpus > 0) {
|
|
1655
1858
|
console.log(`
|
|
1656
|
-
${
|
|
1859
|
+
${chalk2.cyan("Token savings:")}`);
|
|
1657
1860
|
console.log(` Full corpus: ~${bench.naiveFullCorpus.toLocaleString()} tokens`);
|
|
1658
1861
|
console.log(` Avg query: ~${bench.avgQueryTokens.toLocaleString()} tokens`);
|
|
1659
|
-
console.log(` vs relevant: ${
|
|
1660
|
-
console.log(` vs full: ${
|
|
1862
|
+
console.log(` vs relevant: ${chalk2.bold.cyan(bench.reductionVsRelevant + "x")} fewer tokens`);
|
|
1863
|
+
console.log(` vs full: ${chalk2.bold.cyan(bench.reductionVsFull + "x")} fewer tokens`);
|
|
1661
1864
|
}
|
|
1662
1865
|
console.log();
|
|
1663
1866
|
});
|
|
1664
1867
|
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
1868
|
const result = await learn(opts.project, text);
|
|
1666
1869
|
if (result.nodesAdded > 0) {
|
|
1667
|
-
console.log(
|
|
1870
|
+
console.log(chalk2.green(`\u{1F9E0} Learned ${result.nodesAdded} new insight(s).`));
|
|
1668
1871
|
} else {
|
|
1669
|
-
console.log(
|
|
1872
|
+
console.log(chalk2.yellow("No patterns extracted. Try a more specific statement."));
|
|
1670
1873
|
}
|
|
1671
1874
|
});
|
|
1672
1875
|
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 +1879,11 @@ program.command("mistakes").description("List known mistakes extracted from past
|
|
|
1676
1879
|
sinceDays: opts.since ? Number(opts.since) : void 0
|
|
1677
1880
|
});
|
|
1678
1881
|
if (result.length === 0) {
|
|
1679
|
-
console.log(
|
|
1882
|
+
console.log(chalk2.yellow("No mistakes recorded."));
|
|
1680
1883
|
return;
|
|
1681
1884
|
}
|
|
1682
1885
|
console.log(
|
|
1683
|
-
|
|
1886
|
+
chalk2.bold(`
|
|
1684
1887
|
\u26A0\uFE0F ${result.length} mistake(s) recorded:
|
|
1685
1888
|
`)
|
|
1686
1889
|
);
|
|
@@ -1690,7 +1893,7 @@ program.command("mistakes").description("List known mistakes extracted from past
|
|
|
1690
1893
|
Math.round((Date.now() - m.lastVerified) / 864e5)
|
|
1691
1894
|
);
|
|
1692
1895
|
console.log(
|
|
1693
|
-
` ${
|
|
1896
|
+
` ${chalk2.dim(`[${m.sourceFile}, ${ago}d ago]`)} ${m.label}`
|
|
1694
1897
|
);
|
|
1695
1898
|
}
|
|
1696
1899
|
console.log();
|
|
@@ -1698,14 +1901,14 @@ program.command("mistakes").description("List known mistakes extracted from past
|
|
|
1698
1901
|
);
|
|
1699
1902
|
program.command("bench").description("Run token reduction benchmark").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
|
|
1700
1903
|
const result = await benchmark(opts.project);
|
|
1701
|
-
console.log(
|
|
1904
|
+
console.log(chalk2.bold("\n\u26A1 engram token reduction benchmark\n"));
|
|
1702
1905
|
console.log(` Full corpus: ~${result.naiveFullCorpus.toLocaleString()} tokens`);
|
|
1703
1906
|
console.log(` Avg graph query: ~${result.avgQueryTokens.toLocaleString()} tokens`);
|
|
1704
|
-
console.log(` vs relevant: ${
|
|
1705
|
-
console.log(` vs full corpus: ${
|
|
1907
|
+
console.log(` vs relevant: ${chalk2.bold.green(result.reductionVsRelevant + "x")} fewer tokens`);
|
|
1908
|
+
console.log(` vs full corpus: ${chalk2.bold.green(result.reductionVsFull + "x")} fewer tokens
|
|
1706
1909
|
`);
|
|
1707
1910
|
for (const pq of result.perQuestion) {
|
|
1708
|
-
console.log(` ${
|
|
1911
|
+
console.log(` ${chalk2.dim(`[${pq.reductionRelevant}x relevant / ${pq.reductionFull}x full]`)} ${pq.question}`);
|
|
1709
1912
|
}
|
|
1710
1913
|
console.log();
|
|
1711
1914
|
});
|
|
@@ -1721,7 +1924,7 @@ program.command("gen").description("Generate CLAUDE.md / .cursorrules section fr
|
|
|
1721
1924
|
const target = opts.target;
|
|
1722
1925
|
const result = await autogen(opts.project, target, opts.task);
|
|
1723
1926
|
console.log(
|
|
1724
|
-
|
|
1927
|
+
chalk2.green(
|
|
1725
1928
|
`\u2705 Updated ${result.file} (${result.nodesIncluded} nodes, view: ${result.view})`
|
|
1726
1929
|
)
|
|
1727
1930
|
);
|
|
@@ -1731,11 +1934,11 @@ function resolveSettingsPath(scope, projectPath) {
|
|
|
1731
1934
|
const absProject = pathResolve(projectPath);
|
|
1732
1935
|
switch (scope) {
|
|
1733
1936
|
case "local":
|
|
1734
|
-
return
|
|
1937
|
+
return join8(absProject, ".claude", "settings.local.json");
|
|
1735
1938
|
case "project":
|
|
1736
|
-
return
|
|
1939
|
+
return join8(absProject, ".claude", "settings.json");
|
|
1737
1940
|
case "user":
|
|
1738
|
-
return
|
|
1941
|
+
return join8(homedir(), ".claude", "settings.json");
|
|
1739
1942
|
default:
|
|
1740
1943
|
return null;
|
|
1741
1944
|
}
|
|
@@ -1822,25 +2025,25 @@ program.command("install-hook").description("Install engram hook entries into Cl
|
|
|
1822
2025
|
const settingsPath = resolveSettingsPath(opts.scope, opts.project);
|
|
1823
2026
|
if (!settingsPath) {
|
|
1824
2027
|
console.error(
|
|
1825
|
-
|
|
2028
|
+
chalk2.red(
|
|
1826
2029
|
`Unknown scope: ${opts.scope} (expected: local | project | user)`
|
|
1827
2030
|
)
|
|
1828
2031
|
);
|
|
1829
2032
|
process.exit(1);
|
|
1830
2033
|
}
|
|
1831
2034
|
let existing = {};
|
|
1832
|
-
if (
|
|
2035
|
+
if (existsSync8(settingsPath)) {
|
|
1833
2036
|
try {
|
|
1834
|
-
const raw =
|
|
2037
|
+
const raw = readFileSync5(settingsPath, "utf-8");
|
|
1835
2038
|
existing = raw.trim() ? JSON.parse(raw) : {};
|
|
1836
2039
|
} catch (err) {
|
|
1837
2040
|
console.error(
|
|
1838
|
-
|
|
2041
|
+
chalk2.red(
|
|
1839
2042
|
`Failed to parse ${settingsPath}: ${err.message}`
|
|
1840
2043
|
)
|
|
1841
2044
|
);
|
|
1842
2045
|
console.error(
|
|
1843
|
-
|
|
2046
|
+
chalk2.dim(
|
|
1844
2047
|
"Fix the JSON syntax and re-run install-hook, or remove the file and start fresh."
|
|
1845
2048
|
)
|
|
1846
2049
|
);
|
|
@@ -1849,38 +2052,38 @@ program.command("install-hook").description("Install engram hook entries into Cl
|
|
|
1849
2052
|
}
|
|
1850
2053
|
const result = installEngramHooks(existing);
|
|
1851
2054
|
console.log(
|
|
1852
|
-
|
|
2055
|
+
chalk2.bold(`
|
|
1853
2056
|
\u{1F4CC} engram install-hook (scope: ${opts.scope})`)
|
|
1854
2057
|
);
|
|
1855
|
-
console.log(
|
|
2058
|
+
console.log(chalk2.dim(` Target: ${settingsPath}`));
|
|
1856
2059
|
if (result.added.length === 0) {
|
|
1857
2060
|
console.log(
|
|
1858
|
-
|
|
2061
|
+
chalk2.yellow(
|
|
1859
2062
|
`
|
|
1860
2063
|
All engram hooks already installed (${result.alreadyPresent.join(", ")}).`
|
|
1861
2064
|
)
|
|
1862
2065
|
);
|
|
1863
2066
|
console.log(
|
|
1864
|
-
|
|
2067
|
+
chalk2.dim(
|
|
1865
2068
|
" Run 'engram uninstall-hook' first if you want to reinstall."
|
|
1866
2069
|
)
|
|
1867
2070
|
);
|
|
1868
2071
|
return;
|
|
1869
2072
|
}
|
|
1870
|
-
console.log(
|
|
2073
|
+
console.log(chalk2.cyan("\n Changes:"));
|
|
1871
2074
|
console.log(
|
|
1872
2075
|
formatInstallDiff(existing, result.updated).split("\n").map((l) => " " + l).join("\n")
|
|
1873
2076
|
);
|
|
1874
2077
|
if (opts.dryRun) {
|
|
1875
|
-
console.log(
|
|
2078
|
+
console.log(chalk2.dim("\n (dry-run \u2014 no changes written)"));
|
|
1876
2079
|
return;
|
|
1877
2080
|
}
|
|
1878
2081
|
try {
|
|
1879
2082
|
mkdirSync(dirname3(settingsPath), { recursive: true });
|
|
1880
|
-
if (
|
|
2083
|
+
if (existsSync8(settingsPath)) {
|
|
1881
2084
|
const backupPath = `${settingsPath}.engram-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
|
|
1882
2085
|
copyFileSync(settingsPath, backupPath);
|
|
1883
|
-
console.log(
|
|
2086
|
+
console.log(chalk2.dim(` Backup: ${backupPath}`));
|
|
1884
2087
|
}
|
|
1885
2088
|
const tmpPath = settingsPath + ".engram-tmp";
|
|
1886
2089
|
writeFileSync2(
|
|
@@ -1890,26 +2093,26 @@ program.command("install-hook").description("Install engram hook entries into Cl
|
|
|
1890
2093
|
renameSync3(tmpPath, settingsPath);
|
|
1891
2094
|
} catch (err) {
|
|
1892
2095
|
console.error(
|
|
1893
|
-
|
|
2096
|
+
chalk2.red(`
|
|
1894
2097
|
\u274C Write failed: ${err.message}`)
|
|
1895
2098
|
);
|
|
1896
2099
|
process.exit(1);
|
|
1897
2100
|
}
|
|
1898
2101
|
console.log(
|
|
1899
|
-
|
|
2102
|
+
chalk2.green(
|
|
1900
2103
|
`
|
|
1901
2104
|
\u2705 Installed ${result.added.length} hook event${result.added.length === 1 ? "" : "s"}: ${result.added.join(", ")}`
|
|
1902
2105
|
)
|
|
1903
2106
|
);
|
|
1904
2107
|
if (result.alreadyPresent.length > 0) {
|
|
1905
2108
|
console.log(
|
|
1906
|
-
|
|
2109
|
+
chalk2.dim(
|
|
1907
2110
|
` Already present: ${result.alreadyPresent.join(", ")}`
|
|
1908
2111
|
)
|
|
1909
2112
|
);
|
|
1910
2113
|
}
|
|
1911
2114
|
console.log(
|
|
1912
|
-
|
|
2115
|
+
chalk2.dim(
|
|
1913
2116
|
"\n Next: open a Claude Code session and engram will start intercepting tool calls."
|
|
1914
2117
|
)
|
|
1915
2118
|
);
|
|
@@ -1918,29 +2121,29 @@ program.command("install-hook").description("Install engram hook entries into Cl
|
|
|
1918
2121
|
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
2122
|
const settingsPath = resolveSettingsPath(opts.scope, opts.project);
|
|
1920
2123
|
if (!settingsPath) {
|
|
1921
|
-
console.error(
|
|
2124
|
+
console.error(chalk2.red(`Unknown scope: ${opts.scope}`));
|
|
1922
2125
|
process.exit(1);
|
|
1923
2126
|
}
|
|
1924
|
-
if (!
|
|
2127
|
+
if (!existsSync8(settingsPath)) {
|
|
1925
2128
|
console.log(
|
|
1926
|
-
|
|
2129
|
+
chalk2.yellow(`No settings file at ${settingsPath} \u2014 nothing to remove.`)
|
|
1927
2130
|
);
|
|
1928
2131
|
return;
|
|
1929
2132
|
}
|
|
1930
2133
|
let existing;
|
|
1931
2134
|
try {
|
|
1932
|
-
const raw =
|
|
2135
|
+
const raw = readFileSync5(settingsPath, "utf-8");
|
|
1933
2136
|
existing = raw.trim() ? JSON.parse(raw) : {};
|
|
1934
2137
|
} catch (err) {
|
|
1935
2138
|
console.error(
|
|
1936
|
-
|
|
2139
|
+
chalk2.red(`Failed to parse ${settingsPath}: ${err.message}`)
|
|
1937
2140
|
);
|
|
1938
2141
|
process.exit(1);
|
|
1939
2142
|
}
|
|
1940
2143
|
const result = uninstallEngramHooks(existing);
|
|
1941
2144
|
if (result.removed.length === 0) {
|
|
1942
2145
|
console.log(
|
|
1943
|
-
|
|
2146
|
+
chalk2.yellow(`
|
|
1944
2147
|
No engram hooks found in ${settingsPath}.`)
|
|
1945
2148
|
);
|
|
1946
2149
|
return;
|
|
@@ -1952,15 +2155,15 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
|
|
|
1952
2155
|
writeFileSync2(tmpPath, JSON.stringify(result.updated, null, 2) + "\n");
|
|
1953
2156
|
renameSync3(tmpPath, settingsPath);
|
|
1954
2157
|
console.log(
|
|
1955
|
-
|
|
2158
|
+
chalk2.green(
|
|
1956
2159
|
`
|
|
1957
2160
|
\u2705 Removed engram hooks from ${result.removed.length} event${result.removed.length === 1 ? "" : "s"}: ${result.removed.join(", ")}`
|
|
1958
2161
|
)
|
|
1959
2162
|
);
|
|
1960
|
-
console.log(
|
|
2163
|
+
console.log(chalk2.dim(` Backup: ${backupPath}`));
|
|
1961
2164
|
} catch (err) {
|
|
1962
2165
|
console.error(
|
|
1963
|
-
|
|
2166
|
+
chalk2.red(`
|
|
1964
2167
|
\u274C Write failed: ${err.message}`)
|
|
1965
2168
|
);
|
|
1966
2169
|
process.exit(1);
|
|
@@ -1987,16 +2190,16 @@ program.command("hook-preview").description("Show what the Read handler would do
|
|
|
1987
2190
|
tool_input: { file_path: absFile }
|
|
1988
2191
|
};
|
|
1989
2192
|
const result = await dispatchHook(payload);
|
|
1990
|
-
console.log(
|
|
2193
|
+
console.log(chalk2.bold(`
|
|
1991
2194
|
\u{1F4CB} Hook preview: ${absFile}`));
|
|
1992
|
-
console.log(
|
|
2195
|
+
console.log(chalk2.dim(` Project: ${absProject}`));
|
|
1993
2196
|
console.log();
|
|
1994
2197
|
if (result === null || result === void 0) {
|
|
1995
2198
|
console.log(
|
|
1996
|
-
|
|
2199
|
+
chalk2.yellow(" Decision: PASSTHROUGH (Read would execute normally)")
|
|
1997
2200
|
);
|
|
1998
2201
|
console.log(
|
|
1999
|
-
|
|
2202
|
+
chalk2.dim(
|
|
2000
2203
|
" Possible reasons: file not in graph, confidence below threshold, content unsafe, outside project, stale graph."
|
|
2001
2204
|
)
|
|
2002
2205
|
);
|
|
@@ -2005,8 +2208,8 @@ program.command("hook-preview").description("Show what the Read handler would do
|
|
|
2005
2208
|
const wrapped = result;
|
|
2006
2209
|
const decision = wrapped.hookSpecificOutput?.permissionDecision;
|
|
2007
2210
|
if (decision === "deny") {
|
|
2008
|
-
console.log(
|
|
2009
|
-
console.log(
|
|
2211
|
+
console.log(chalk2.green(" Decision: DENY (Read would be replaced)"));
|
|
2212
|
+
console.log(chalk2.dim(" Summary (would be delivered to Claude):"));
|
|
2010
2213
|
console.log();
|
|
2011
2214
|
const reason = wrapped.hookSpecificOutput?.permissionDecisionReason ?? "";
|
|
2012
2215
|
console.log(
|
|
@@ -2015,41 +2218,41 @@ program.command("hook-preview").description("Show what the Read handler would do
|
|
|
2015
2218
|
return;
|
|
2016
2219
|
}
|
|
2017
2220
|
if (decision === "allow") {
|
|
2018
|
-
console.log(
|
|
2221
|
+
console.log(chalk2.cyan(" Decision: ALLOW (with additionalContext)"));
|
|
2019
2222
|
const ctx = wrapped.hookSpecificOutput?.additionalContext ?? "";
|
|
2020
2223
|
if (ctx) {
|
|
2021
|
-
console.log(
|
|
2224
|
+
console.log(chalk2.dim(" Additional context that would be injected:"));
|
|
2022
2225
|
console.log(
|
|
2023
2226
|
ctx.split("\n").map((l) => " " + l).join("\n")
|
|
2024
2227
|
);
|
|
2025
2228
|
}
|
|
2026
2229
|
return;
|
|
2027
2230
|
}
|
|
2028
|
-
console.log(
|
|
2231
|
+
console.log(chalk2.yellow(` Decision: ${decision ?? "unknown"}`));
|
|
2029
2232
|
});
|
|
2030
2233
|
program.command("hook-disable").description("Disable engram hooks via kill switch (does not uninstall)").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
|
|
2031
2234
|
const absProject = pathResolve(opts.project);
|
|
2032
2235
|
const projectRoot = findProjectRoot(absProject);
|
|
2033
2236
|
if (!projectRoot) {
|
|
2034
2237
|
console.error(
|
|
2035
|
-
|
|
2238
|
+
chalk2.red(`Not an engram project: ${absProject}`)
|
|
2036
2239
|
);
|
|
2037
|
-
console.error(
|
|
2240
|
+
console.error(chalk2.dim("Run 'engram init' first."));
|
|
2038
2241
|
process.exit(1);
|
|
2039
2242
|
}
|
|
2040
|
-
const flagPath =
|
|
2243
|
+
const flagPath = join8(projectRoot, ".engram", "hook-disabled");
|
|
2041
2244
|
try {
|
|
2042
2245
|
writeFileSync2(flagPath, (/* @__PURE__ */ new Date()).toISOString());
|
|
2043
2246
|
console.log(
|
|
2044
|
-
|
|
2247
|
+
chalk2.green(`\u2705 engram hooks disabled for ${projectRoot}`)
|
|
2045
2248
|
);
|
|
2046
|
-
console.log(
|
|
2249
|
+
console.log(chalk2.dim(` Flag: ${flagPath}`));
|
|
2047
2250
|
console.log(
|
|
2048
|
-
|
|
2251
|
+
chalk2.dim(" Run 'engram hook-enable' to re-enable.")
|
|
2049
2252
|
);
|
|
2050
2253
|
} catch (err) {
|
|
2051
2254
|
console.error(
|
|
2052
|
-
|
|
2255
|
+
chalk2.red(`Failed to create flag: ${err.message}`)
|
|
2053
2256
|
);
|
|
2054
2257
|
process.exit(1);
|
|
2055
2258
|
}
|
|
@@ -2058,24 +2261,24 @@ program.command("hook-enable").description("Re-enable engram hooks (remove kill
|
|
|
2058
2261
|
const absProject = pathResolve(opts.project);
|
|
2059
2262
|
const projectRoot = findProjectRoot(absProject);
|
|
2060
2263
|
if (!projectRoot) {
|
|
2061
|
-
console.error(
|
|
2264
|
+
console.error(chalk2.red(`Not an engram project: ${absProject}`));
|
|
2062
2265
|
process.exit(1);
|
|
2063
2266
|
}
|
|
2064
|
-
const flagPath =
|
|
2065
|
-
if (!
|
|
2267
|
+
const flagPath = join8(projectRoot, ".engram", "hook-disabled");
|
|
2268
|
+
if (!existsSync8(flagPath)) {
|
|
2066
2269
|
console.log(
|
|
2067
|
-
|
|
2270
|
+
chalk2.yellow(`engram hooks already enabled for ${projectRoot}`)
|
|
2068
2271
|
);
|
|
2069
2272
|
return;
|
|
2070
2273
|
}
|
|
2071
2274
|
try {
|
|
2072
2275
|
unlinkSync(flagPath);
|
|
2073
2276
|
console.log(
|
|
2074
|
-
|
|
2277
|
+
chalk2.green(`\u2705 engram hooks re-enabled for ${projectRoot}`)
|
|
2075
2278
|
);
|
|
2076
2279
|
} catch (err) {
|
|
2077
2280
|
console.error(
|
|
2078
|
-
|
|
2281
|
+
chalk2.red(`Failed to remove flag: ${err.message}`)
|
|
2079
2282
|
);
|
|
2080
2283
|
process.exit(1);
|
|
2081
2284
|
}
|
|
@@ -2088,9 +2291,9 @@ program.command("memory-sync").description(
|
|
|
2088
2291
|
const projectRoot = findProjectRoot(absProject);
|
|
2089
2292
|
if (!projectRoot) {
|
|
2090
2293
|
console.error(
|
|
2091
|
-
|
|
2294
|
+
chalk2.red(`Not an engram project: ${absProject}`)
|
|
2092
2295
|
);
|
|
2093
|
-
console.error(
|
|
2296
|
+
console.error(chalk2.dim("Run 'engram init' first."));
|
|
2094
2297
|
process.exit(1);
|
|
2095
2298
|
}
|
|
2096
2299
|
const [gods, mistakeList, graphStats] = await Promise.all([
|
|
@@ -2099,21 +2302,21 @@ program.command("memory-sync").description(
|
|
|
2099
2302
|
stats(projectRoot).catch(() => null)
|
|
2100
2303
|
]);
|
|
2101
2304
|
if (!graphStats) {
|
|
2102
|
-
console.error(
|
|
2305
|
+
console.error(chalk2.red("Failed to read graph stats."));
|
|
2103
2306
|
process.exit(1);
|
|
2104
2307
|
}
|
|
2105
2308
|
let branch = null;
|
|
2106
2309
|
try {
|
|
2107
|
-
const headPath =
|
|
2108
|
-
if (
|
|
2109
|
-
const content =
|
|
2310
|
+
const headPath = join8(projectRoot, ".git", "HEAD");
|
|
2311
|
+
if (existsSync8(headPath)) {
|
|
2312
|
+
const content = readFileSync5(headPath, "utf-8").trim();
|
|
2110
2313
|
const m = content.match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
2111
2314
|
if (m) branch = m[1];
|
|
2112
2315
|
}
|
|
2113
2316
|
} catch {
|
|
2114
2317
|
}
|
|
2115
2318
|
const section = buildEngramSection({
|
|
2116
|
-
projectName:
|
|
2319
|
+
projectName: basename5(projectRoot),
|
|
2117
2320
|
branch,
|
|
2118
2321
|
stats: {
|
|
2119
2322
|
nodes: graphStats.nodes,
|
|
@@ -2128,37 +2331,37 @@ program.command("memory-sync").description(
|
|
|
2128
2331
|
lastMined: graphStats.lastMined
|
|
2129
2332
|
});
|
|
2130
2333
|
console.log(
|
|
2131
|
-
|
|
2334
|
+
chalk2.bold(`
|
|
2132
2335
|
\u{1F4DD} engram memory-sync`)
|
|
2133
2336
|
);
|
|
2134
2337
|
console.log(
|
|
2135
|
-
|
|
2338
|
+
chalk2.dim(` Target: ${join8(projectRoot, "MEMORY.md")}`)
|
|
2136
2339
|
);
|
|
2137
2340
|
if (opts.dryRun) {
|
|
2138
|
-
console.log(
|
|
2341
|
+
console.log(chalk2.cyan("\n Section to write (dry-run):\n"));
|
|
2139
2342
|
console.log(
|
|
2140
2343
|
section.split("\n").map((l) => " " + l).join("\n")
|
|
2141
2344
|
);
|
|
2142
|
-
console.log(
|
|
2345
|
+
console.log(chalk2.dim("\n (dry-run \u2014 no changes written)"));
|
|
2143
2346
|
return;
|
|
2144
2347
|
}
|
|
2145
2348
|
const ok = writeEngramSectionToMemoryMd(projectRoot, section);
|
|
2146
2349
|
if (!ok) {
|
|
2147
2350
|
console.error(
|
|
2148
|
-
|
|
2351
|
+
chalk2.red(
|
|
2149
2352
|
"\n \u274C Write failed. MEMORY.md may be too large, or the engram section exceeded its size cap."
|
|
2150
2353
|
)
|
|
2151
2354
|
);
|
|
2152
2355
|
process.exit(1);
|
|
2153
2356
|
}
|
|
2154
2357
|
console.log(
|
|
2155
|
-
|
|
2358
|
+
chalk2.green(
|
|
2156
2359
|
`
|
|
2157
2360
|
\u2705 Synced ${gods.length} god nodes${mistakeList.length > 0 ? ` and ${mistakeList.length} landmines` : ""} to MEMORY.md`
|
|
2158
2361
|
)
|
|
2159
2362
|
);
|
|
2160
2363
|
console.log(
|
|
2161
|
-
|
|
2364
|
+
chalk2.dim(
|
|
2162
2365
|
`
|
|
2163
2366
|
Next: Anthropic's Auto-Dream will consolidate this alongside its prose entries.
|
|
2164
2367
|
`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engramx",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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": {
|