engrm 0.4.0 → 0.4.1
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/hooks/post-tool-use.js +42 -0
- package/dist/hooks/session-start.js +191 -50
- package/dist/hooks/stop.js +115 -17
- package/dist/server.js +29 -0
- package/package.json +1 -1
|
@@ -2357,6 +2357,24 @@ function extractFilesFromEvent(event) {
|
|
|
2357
2357
|
}
|
|
2358
2358
|
return { files_modified: [filePath] };
|
|
2359
2359
|
}
|
|
2360
|
+
function incrementObserverSaveCount(sessionId) {
|
|
2361
|
+
try {
|
|
2362
|
+
const state = readState(sessionId);
|
|
2363
|
+
if (state) {
|
|
2364
|
+
state.saveCount = (state.saveCount ?? 0) + 1;
|
|
2365
|
+
writeState(sessionId, state);
|
|
2366
|
+
} else {
|
|
2367
|
+
if (!existsSync3(OBSERVER_DIR)) {
|
|
2368
|
+
mkdirSync2(OBSERVER_DIR, { recursive: true });
|
|
2369
|
+
}
|
|
2370
|
+
writeState(sessionId, {
|
|
2371
|
+
observerSessionId: "",
|
|
2372
|
+
eventCount: 0,
|
|
2373
|
+
saveCount: 1
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
} catch {}
|
|
2377
|
+
}
|
|
2360
2378
|
|
|
2361
2379
|
// src/capture/recall.ts
|
|
2362
2380
|
var VEC_DISTANCE_THRESHOLD = 0.25;
|
|
@@ -2663,6 +2681,7 @@ async function main() {
|
|
|
2663
2681
|
const detected = detectProject(event.cwd);
|
|
2664
2682
|
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
2665
2683
|
const recall = await recallPastFix(db, sig, project?.id ?? null);
|
|
2684
|
+
incrementRecallMetrics(event.session_id, recall.found);
|
|
2666
2685
|
if (recall.found) {
|
|
2667
2686
|
const projectLabel = recall.projectName ? ` (from ${recall.projectName})` : "";
|
|
2668
2687
|
console.error(`
|
|
@@ -2693,6 +2712,7 @@ async function main() {
|
|
|
2693
2712
|
});
|
|
2694
2713
|
if (observed) {
|
|
2695
2714
|
await saveObservation(db, config, observed);
|
|
2715
|
+
incrementObserverSaveCount(event.session_id);
|
|
2696
2716
|
saved = true;
|
|
2697
2717
|
}
|
|
2698
2718
|
} catch {}
|
|
@@ -2709,6 +2729,7 @@ async function main() {
|
|
|
2709
2729
|
session_id: event.session_id,
|
|
2710
2730
|
cwd: event.cwd
|
|
2711
2731
|
});
|
|
2732
|
+
incrementObserverSaveCount(event.session_id);
|
|
2712
2733
|
}
|
|
2713
2734
|
}
|
|
2714
2735
|
} finally {
|
|
@@ -2744,6 +2765,27 @@ function extractScanText(event) {
|
|
|
2744
2765
|
return null;
|
|
2745
2766
|
}
|
|
2746
2767
|
}
|
|
2768
|
+
function incrementRecallMetrics(sessionId, hit) {
|
|
2769
|
+
try {
|
|
2770
|
+
const { existsSync: existsSync4, readFileSync: readFileSync4, writeFileSync: writeFileSync3, mkdirSync: mkdirSync3 } = __require("node:fs");
|
|
2771
|
+
const { join: join4 } = __require("node:path");
|
|
2772
|
+
const { homedir: homedir3 } = __require("node:os");
|
|
2773
|
+
const dir = join4(homedir3(), ".engrm", "observer-sessions");
|
|
2774
|
+
const path = join4(dir, `${sessionId}.json`);
|
|
2775
|
+
let state = {};
|
|
2776
|
+
if (existsSync4(path)) {
|
|
2777
|
+
state = JSON.parse(readFileSync4(path, "utf-8"));
|
|
2778
|
+
} else {
|
|
2779
|
+
if (!existsSync4(dir))
|
|
2780
|
+
mkdirSync3(dir, { recursive: true });
|
|
2781
|
+
}
|
|
2782
|
+
state.recallAttempts = (state.recallAttempts || 0) + 1;
|
|
2783
|
+
if (hit) {
|
|
2784
|
+
state.recallHits = (state.recallHits || 0) + 1;
|
|
2785
|
+
}
|
|
2786
|
+
writeFileSync3(path, JSON.stringify(state), "utf-8");
|
|
2787
|
+
} catch {}
|
|
2788
|
+
}
|
|
2747
2789
|
main().catch(() => {
|
|
2748
2790
|
process.exit(0);
|
|
2749
2791
|
});
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
5
5
|
|
|
6
|
+
// hooks/session-start.ts
|
|
7
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
8
|
+
import { join as join7 } from "path";
|
|
9
|
+
import { homedir as homedir4 } from "os";
|
|
10
|
+
|
|
6
11
|
// src/config.ts
|
|
7
12
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
13
|
import { homedir, hostname, networkInterfaces } from "node:os";
|
|
@@ -1493,9 +1498,83 @@ function detectStacksFromProject(projectRoot, filePaths = []) {
|
|
|
1493
1498
|
return { stacks: sorted, primary };
|
|
1494
1499
|
}
|
|
1495
1500
|
|
|
1501
|
+
// src/telemetry/config-fingerprint.ts
|
|
1502
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1503
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync, mkdirSync as mkdirSync2 } from "node:fs";
|
|
1504
|
+
import { join as join4 } from "node:path";
|
|
1505
|
+
import { homedir as homedir2 } from "node:os";
|
|
1506
|
+
var STATE_PATH = join4(homedir2(), ".engrm", "config-fingerprint.json");
|
|
1507
|
+
var CLIENT_VERSION = "0.4.0";
|
|
1508
|
+
function hashFile(filePath) {
|
|
1509
|
+
try {
|
|
1510
|
+
if (!existsSync4(filePath))
|
|
1511
|
+
return null;
|
|
1512
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
1513
|
+
return createHash2("sha256").update(content).digest("hex");
|
|
1514
|
+
} catch {
|
|
1515
|
+
return null;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
function countMemoryFiles(memoryDir) {
|
|
1519
|
+
try {
|
|
1520
|
+
if (!existsSync4(memoryDir))
|
|
1521
|
+
return 0;
|
|
1522
|
+
return readdirSync(memoryDir).filter((f) => f.endsWith(".md")).length;
|
|
1523
|
+
} catch {
|
|
1524
|
+
return 0;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
function readPreviousFingerprint() {
|
|
1528
|
+
try {
|
|
1529
|
+
if (!existsSync4(STATE_PATH))
|
|
1530
|
+
return null;
|
|
1531
|
+
return JSON.parse(readFileSync3(STATE_PATH, "utf-8"));
|
|
1532
|
+
} catch {
|
|
1533
|
+
return null;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
function saveFingerprint(fp) {
|
|
1537
|
+
try {
|
|
1538
|
+
const dir = join4(homedir2(), ".engrm");
|
|
1539
|
+
if (!existsSync4(dir))
|
|
1540
|
+
mkdirSync2(dir, { recursive: true });
|
|
1541
|
+
writeFileSync2(STATE_PATH, JSON.stringify(fp, null, 2) + `
|
|
1542
|
+
`, "utf-8");
|
|
1543
|
+
} catch {}
|
|
1544
|
+
}
|
|
1545
|
+
function computeAndSaveFingerprint(cwd) {
|
|
1546
|
+
const claudeMdHash = hashFile(join4(cwd, "CLAUDE.md"));
|
|
1547
|
+
const engrmJsonHash = hashFile(join4(cwd, ".engrm.json"));
|
|
1548
|
+
const slug = cwd.replace(/\//g, "-");
|
|
1549
|
+
const memoryDir = join4(homedir2(), ".claude", "projects", slug, "memory");
|
|
1550
|
+
const memoryMdHash = hashFile(join4(memoryDir, "MEMORY.md"));
|
|
1551
|
+
const memoryFileCount = countMemoryFiles(memoryDir);
|
|
1552
|
+
const material = [
|
|
1553
|
+
claudeMdHash ?? "null",
|
|
1554
|
+
memoryMdHash ?? "null",
|
|
1555
|
+
engrmJsonHash ?? "null",
|
|
1556
|
+
String(memoryFileCount),
|
|
1557
|
+
CLIENT_VERSION
|
|
1558
|
+
].join("+");
|
|
1559
|
+
const configHash = createHash2("sha256").update(material).digest("hex");
|
|
1560
|
+
const previous = readPreviousFingerprint();
|
|
1561
|
+
const configChanged = previous !== null && previous.config_hash !== configHash;
|
|
1562
|
+
const fingerprint = {
|
|
1563
|
+
config_hash: configHash,
|
|
1564
|
+
config_changed: configChanged,
|
|
1565
|
+
claude_md_hash: claudeMdHash,
|
|
1566
|
+
memory_md_hash: memoryMdHash,
|
|
1567
|
+
engrm_json_hash: engrmJsonHash,
|
|
1568
|
+
memory_file_count: memoryFileCount,
|
|
1569
|
+
client_version: CLIENT_VERSION
|
|
1570
|
+
};
|
|
1571
|
+
saveFingerprint(fingerprint);
|
|
1572
|
+
return fingerprint;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1496
1575
|
// src/packs/recommender.ts
|
|
1497
|
-
import { existsSync as
|
|
1498
|
-
import { join as
|
|
1576
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "node:fs";
|
|
1577
|
+
import { join as join5, basename as basename3, dirname } from "node:path";
|
|
1499
1578
|
import { fileURLToPath } from "node:url";
|
|
1500
1579
|
var STACK_PACK_MAP = {
|
|
1501
1580
|
typescript: ["typescript-patterns"],
|
|
@@ -1508,20 +1587,20 @@ var STACK_PACK_MAP = {
|
|
|
1508
1587
|
};
|
|
1509
1588
|
function getPacksDir() {
|
|
1510
1589
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
1511
|
-
return
|
|
1590
|
+
return join5(thisDir, "../../packs");
|
|
1512
1591
|
}
|
|
1513
1592
|
function listAvailablePacks() {
|
|
1514
1593
|
const dir = getPacksDir();
|
|
1515
|
-
if (!
|
|
1594
|
+
if (!existsSync5(dir))
|
|
1516
1595
|
return [];
|
|
1517
|
-
return
|
|
1596
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".json")).map((f) => basename3(f, ".json"));
|
|
1518
1597
|
}
|
|
1519
1598
|
function loadPack(name) {
|
|
1520
|
-
const packPath =
|
|
1521
|
-
if (!
|
|
1599
|
+
const packPath = join5(getPacksDir(), `${name}.json`);
|
|
1600
|
+
if (!existsSync5(packPath))
|
|
1522
1601
|
return null;
|
|
1523
1602
|
try {
|
|
1524
|
-
const raw =
|
|
1603
|
+
const raw = readFileSync4(packPath, "utf-8");
|
|
1525
1604
|
return JSON.parse(raw);
|
|
1526
1605
|
} catch {
|
|
1527
1606
|
return null;
|
|
@@ -1556,13 +1635,13 @@ function recommendPacks(stacks, installedPacks) {
|
|
|
1556
1635
|
}
|
|
1557
1636
|
|
|
1558
1637
|
// src/config.ts
|
|
1559
|
-
import { existsSync as
|
|
1560
|
-
import { homedir as
|
|
1561
|
-
import { join as
|
|
1562
|
-
import { createHash as
|
|
1563
|
-
var CONFIG_DIR2 =
|
|
1564
|
-
var SETTINGS_PATH2 =
|
|
1565
|
-
var DB_PATH2 =
|
|
1638
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1639
|
+
import { homedir as homedir3, hostname as hostname2, networkInterfaces as networkInterfaces2 } from "node:os";
|
|
1640
|
+
import { join as join6 } from "node:path";
|
|
1641
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1642
|
+
var CONFIG_DIR2 = join6(homedir3(), ".engrm");
|
|
1643
|
+
var SETTINGS_PATH2 = join6(CONFIG_DIR2, "settings.json");
|
|
1644
|
+
var DB_PATH2 = join6(CONFIG_DIR2, "engrm.db");
|
|
1566
1645
|
function getDbPath2() {
|
|
1567
1646
|
return DB_PATH2;
|
|
1568
1647
|
}
|
|
@@ -1583,7 +1662,7 @@ function generateDeviceId2() {
|
|
|
1583
1662
|
break;
|
|
1584
1663
|
}
|
|
1585
1664
|
const material = `${host}:${mac || "no-mac"}`;
|
|
1586
|
-
const suffix =
|
|
1665
|
+
const suffix = createHash3("sha256").update(material).digest("hex").slice(0, 8);
|
|
1587
1666
|
return `${host}-${suffix}`;
|
|
1588
1667
|
}
|
|
1589
1668
|
function createDefaultConfig2() {
|
|
@@ -1633,10 +1712,10 @@ function createDefaultConfig2() {
|
|
|
1633
1712
|
};
|
|
1634
1713
|
}
|
|
1635
1714
|
function loadConfig2() {
|
|
1636
|
-
if (!
|
|
1715
|
+
if (!existsSync6(SETTINGS_PATH2)) {
|
|
1637
1716
|
throw new Error(`Config not found at ${SETTINGS_PATH2}. Run 'engrm init --manual' to configure.`);
|
|
1638
1717
|
}
|
|
1639
|
-
const raw =
|
|
1718
|
+
const raw = readFileSync5(SETTINGS_PATH2, "utf-8");
|
|
1640
1719
|
let parsed;
|
|
1641
1720
|
try {
|
|
1642
1721
|
parsed = JSON.parse(raw);
|
|
@@ -1694,14 +1773,14 @@ function loadConfig2() {
|
|
|
1694
1773
|
};
|
|
1695
1774
|
}
|
|
1696
1775
|
function saveConfig(config) {
|
|
1697
|
-
if (!
|
|
1698
|
-
|
|
1776
|
+
if (!existsSync6(CONFIG_DIR2)) {
|
|
1777
|
+
mkdirSync3(CONFIG_DIR2, { recursive: true });
|
|
1699
1778
|
}
|
|
1700
|
-
|
|
1779
|
+
writeFileSync3(SETTINGS_PATH2, JSON.stringify(config, null, 2) + `
|
|
1701
1780
|
`, "utf-8");
|
|
1702
1781
|
}
|
|
1703
1782
|
function configExists2() {
|
|
1704
|
-
return
|
|
1783
|
+
return existsSync6(SETTINGS_PATH2);
|
|
1705
1784
|
}
|
|
1706
1785
|
function asString2(value, fallback) {
|
|
1707
1786
|
return typeof value === "string" ? value : fallback;
|
|
@@ -2103,58 +2182,120 @@ async function main() {
|
|
|
2103
2182
|
} catch {
|
|
2104
2183
|
process.exit(0);
|
|
2105
2184
|
}
|
|
2185
|
+
let syncedCount = 0;
|
|
2106
2186
|
try {
|
|
2107
2187
|
if (config.sync.enabled && config.candengo_api_key) {
|
|
2108
2188
|
try {
|
|
2109
2189
|
const client = new VectorClient(config);
|
|
2110
2190
|
const pullResult = await pullFromVector(db, client, config, 50);
|
|
2111
|
-
|
|
2112
|
-
console.error(`Engrm: synced ${pullResult.merged} observation(s) from server`);
|
|
2113
|
-
}
|
|
2191
|
+
syncedCount = pullResult.merged;
|
|
2114
2192
|
await pullSettings(client, config);
|
|
2115
2193
|
} catch {}
|
|
2116
2194
|
}
|
|
2195
|
+
try {
|
|
2196
|
+
computeAndSaveFingerprint(event.cwd);
|
|
2197
|
+
} catch {}
|
|
2117
2198
|
const context = buildSessionContext(db, event.cwd, {
|
|
2118
2199
|
tokenBudget: 800,
|
|
2119
2200
|
scope: config.search.scope
|
|
2120
2201
|
});
|
|
2202
|
+
if (context) {
|
|
2203
|
+
try {
|
|
2204
|
+
const dir = join7(homedir4(), ".engrm");
|
|
2205
|
+
if (!existsSync7(dir))
|
|
2206
|
+
mkdirSync4(dir, { recursive: true });
|
|
2207
|
+
writeFileSync4(join7(dir, "hook-session-metrics.json"), JSON.stringify({
|
|
2208
|
+
contextObsInjected: context.observations.length,
|
|
2209
|
+
contextTotalAvailable: context.total_active
|
|
2210
|
+
}), "utf-8");
|
|
2211
|
+
} catch {}
|
|
2212
|
+
}
|
|
2121
2213
|
if (context && context.observations.length > 0) {
|
|
2122
|
-
console.log(formatContextForInjection(context));
|
|
2123
|
-
const parts = [];
|
|
2124
|
-
parts.push(`${context.session_count} observation(s)`);
|
|
2125
|
-
if (context.securityFindings && context.securityFindings.length > 0) {
|
|
2126
|
-
parts.push(`${context.securityFindings.length} security finding(s)`);
|
|
2127
|
-
}
|
|
2128
2214
|
const remaining = context.total_active - context.session_count;
|
|
2129
|
-
|
|
2130
|
-
parts.push(`${remaining} more searchable`);
|
|
2131
|
-
}
|
|
2215
|
+
let msgCount = 0;
|
|
2132
2216
|
try {
|
|
2133
2217
|
const readKey = `messages_read_${config.device_id}`;
|
|
2134
2218
|
const lastReadId = parseInt(db.getSyncState(readKey) ?? "0", 10);
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2219
|
+
msgCount = db.db.query("SELECT COUNT(*) as c FROM observations WHERE type = 'message' AND id > ? AND lifecycle IN ('active', 'pinned')").get(lastReadId)?.c ?? 0;
|
|
2220
|
+
} catch {}
|
|
2221
|
+
const splash = formatSplashScreen({
|
|
2222
|
+
projectName: context.project_name,
|
|
2223
|
+
loaded: context.session_count,
|
|
2224
|
+
available: remaining,
|
|
2225
|
+
securityFindings: context.securityFindings?.length ?? 0,
|
|
2226
|
+
unreadMessages: msgCount,
|
|
2227
|
+
synced: syncedCount
|
|
2228
|
+
});
|
|
2229
|
+
let packLine = "";
|
|
2230
|
+
try {
|
|
2231
|
+
const { stacks } = detectStacksFromProject(event.cwd);
|
|
2232
|
+
if (stacks.length > 0) {
|
|
2233
|
+
const installed = db.getInstalledPacks();
|
|
2234
|
+
const recs = recommendPacks(stacks, installed);
|
|
2235
|
+
if (recs.length > 0) {
|
|
2236
|
+
const names = recs.map((r) => `\`${r.name}\``).join(", ");
|
|
2237
|
+
packLine = `
|
|
2238
|
+
Help packs available for your stack: ${names}. ` + `Use the install_pack tool to load curated observations.`;
|
|
2239
|
+
}
|
|
2138
2240
|
}
|
|
2139
2241
|
} catch {}
|
|
2140
|
-
console.
|
|
2242
|
+
console.log(JSON.stringify({
|
|
2243
|
+
hookSpecificOutput: {
|
|
2244
|
+
hookEventName: "SessionStart",
|
|
2245
|
+
additionalContext: formatContextForInjection(context) + packLine
|
|
2246
|
+
},
|
|
2247
|
+
systemMessage: splash
|
|
2248
|
+
}));
|
|
2141
2249
|
}
|
|
2142
|
-
try {
|
|
2143
|
-
const { stacks } = detectStacksFromProject(event.cwd);
|
|
2144
|
-
if (stacks.length > 0) {
|
|
2145
|
-
const installed = db.getInstalledPacks();
|
|
2146
|
-
const recs = recommendPacks(stacks, installed);
|
|
2147
|
-
if (recs.length > 0) {
|
|
2148
|
-
const names = recs.map((r) => `\`${r.name}\``).join(", ");
|
|
2149
|
-
console.log(`
|
|
2150
|
-
Help packs available for your stack: ${names}. ` + `Use the install_pack tool to load curated observations.`);
|
|
2151
|
-
}
|
|
2152
|
-
}
|
|
2153
|
-
} catch {}
|
|
2154
2250
|
} finally {
|
|
2155
2251
|
db.close();
|
|
2156
2252
|
}
|
|
2157
2253
|
}
|
|
2254
|
+
var c = {
|
|
2255
|
+
reset: "\x1B[0m",
|
|
2256
|
+
dim: "\x1B[2m",
|
|
2257
|
+
bold: "\x1B[1m",
|
|
2258
|
+
cyan: "\x1B[36m",
|
|
2259
|
+
green: "\x1B[32m",
|
|
2260
|
+
yellow: "\x1B[33m",
|
|
2261
|
+
magenta: "\x1B[35m",
|
|
2262
|
+
white: "\x1B[37m"
|
|
2263
|
+
};
|
|
2264
|
+
function formatSplashScreen(data) {
|
|
2265
|
+
const lines = [];
|
|
2266
|
+
lines.push("");
|
|
2267
|
+
lines.push(`${c.cyan}${c.bold} ______ ____ _ ______ _____ ____ __${c.reset}`);
|
|
2268
|
+
lines.push(`${c.cyan}${c.bold} | ___|| \\ | || ___|| | | \\ / |${c.reset}`);
|
|
2269
|
+
lines.push(`${c.cyan}${c.bold} | ___|| \\| || | || \\ | \\/ |${c.reset}`);
|
|
2270
|
+
lines.push(`${c.cyan}${c.bold} |______||__/\\____||______||__|\\__\\|__/\\__/|__|${c.reset}`);
|
|
2271
|
+
lines.push(`${c.dim} memory layer for AI agents${c.reset}`);
|
|
2272
|
+
lines.push("");
|
|
2273
|
+
const dot = `${c.dim} \xB7 ${c.reset}`;
|
|
2274
|
+
const statParts = [];
|
|
2275
|
+
statParts.push(`${c.green}${data.loaded}${c.reset} loaded`);
|
|
2276
|
+
if (data.available > 0) {
|
|
2277
|
+
statParts.push(`${c.dim}${data.available.toLocaleString()} searchable${c.reset}`);
|
|
2278
|
+
}
|
|
2279
|
+
if (data.synced > 0) {
|
|
2280
|
+
statParts.push(`${c.cyan}${data.synced} synced${c.reset}`);
|
|
2281
|
+
}
|
|
2282
|
+
lines.push(` ${c.white}${c.bold}engrm${c.reset}${dot}${statParts.join(dot)}`);
|
|
2283
|
+
const alerts = [];
|
|
2284
|
+
if (data.securityFindings > 0) {
|
|
2285
|
+
alerts.push(`${c.yellow}${data.securityFindings} security finding${data.securityFindings !== 1 ? "s" : ""}${c.reset}`);
|
|
2286
|
+
}
|
|
2287
|
+
if (data.unreadMessages > 0) {
|
|
2288
|
+
alerts.push(`${c.magenta}${data.unreadMessages} unread message${data.unreadMessages !== 1 ? "s" : ""}${c.reset}`);
|
|
2289
|
+
}
|
|
2290
|
+
if (alerts.length > 0) {
|
|
2291
|
+
lines.push(` ${alerts.join(dot)}`);
|
|
2292
|
+
}
|
|
2293
|
+
lines.push("");
|
|
2294
|
+
lines.push(` ${c.dim}Dashboard: https://engrm.dev/dashboard${c.reset}`);
|
|
2295
|
+
lines.push("");
|
|
2296
|
+
return lines.join(`
|
|
2297
|
+
`);
|
|
2298
|
+
}
|
|
2158
2299
|
main().catch(() => {
|
|
2159
2300
|
process.exit(0);
|
|
2160
2301
|
});
|
package/dist/hooks/stop.js
CHANGED
|
@@ -1689,7 +1689,24 @@ function detectStacks(filePaths) {
|
|
|
1689
1689
|
}
|
|
1690
1690
|
|
|
1691
1691
|
// src/telemetry/beacon.ts
|
|
1692
|
-
|
|
1692
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
1693
|
+
import { join as join3 } from "node:path";
|
|
1694
|
+
import { homedir as homedir2 } from "node:os";
|
|
1695
|
+
function readObserverState(sessionId) {
|
|
1696
|
+
try {
|
|
1697
|
+
const statePath = join3(homedir2(), ".engrm", "observer-sessions", `${sessionId}.json`);
|
|
1698
|
+
if (!existsSync2(statePath))
|
|
1699
|
+
return { eventCount: 0, saveCount: 0 };
|
|
1700
|
+
const state = JSON.parse(readFileSync2(statePath, "utf-8"));
|
|
1701
|
+
return {
|
|
1702
|
+
eventCount: typeof state.eventCount === "number" ? state.eventCount : 0,
|
|
1703
|
+
saveCount: typeof state.saveCount === "number" ? state.saveCount : 0
|
|
1704
|
+
};
|
|
1705
|
+
} catch {
|
|
1706
|
+
return { eventCount: 0, saveCount: 0 };
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
function buildBeacon(db, config, sessionId, metrics) {
|
|
1693
1710
|
const session = db.getSessionMetrics(sessionId);
|
|
1694
1711
|
if (!session)
|
|
1695
1712
|
return null;
|
|
@@ -1716,6 +1733,28 @@ function buildBeacon(db, config, sessionId) {
|
|
|
1716
1733
|
const row = db.getSessionMetrics(sessionId);
|
|
1717
1734
|
riskScore = typeof row?.risk_score === "number" ? row.risk_score : 0;
|
|
1718
1735
|
} catch {}
|
|
1736
|
+
let configHash;
|
|
1737
|
+
let configChanged;
|
|
1738
|
+
let configFingerprintDetail;
|
|
1739
|
+
try {
|
|
1740
|
+
const fpPath = join3(homedir2(), ".engrm", "config-fingerprint.json");
|
|
1741
|
+
if (existsSync2(fpPath)) {
|
|
1742
|
+
const fp = JSON.parse(readFileSync2(fpPath, "utf-8"));
|
|
1743
|
+
configHash = fp.config_hash;
|
|
1744
|
+
configChanged = fp.config_changed;
|
|
1745
|
+
configFingerprintDetail = JSON.stringify({
|
|
1746
|
+
claude_md_hash: fp.claude_md_hash,
|
|
1747
|
+
memory_md_hash: fp.memory_md_hash,
|
|
1748
|
+
engrm_json_hash: fp.engrm_json_hash,
|
|
1749
|
+
memory_file_count: fp.memory_file_count,
|
|
1750
|
+
client_version: fp.client_version
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
} catch {}
|
|
1754
|
+
const observerState = readObserverState(sessionId);
|
|
1755
|
+
const observerEvents = observerState.eventCount;
|
|
1756
|
+
const observerObservations = observerState.saveCount;
|
|
1757
|
+
const observerSkips = Math.max(0, observerEvents - observerObservations);
|
|
1719
1758
|
return {
|
|
1720
1759
|
device_id: config.device_id,
|
|
1721
1760
|
agent: session.agent ?? "claude-code",
|
|
@@ -1725,13 +1764,22 @@ function buildBeacon(db, config, sessionId) {
|
|
|
1725
1764
|
tool_calls_count: session.tool_calls_count ?? 0,
|
|
1726
1765
|
files_touched_count: session.files_touched_count ?? 0,
|
|
1727
1766
|
searches_performed: session.searches_performed ?? 0,
|
|
1728
|
-
observer_events:
|
|
1729
|
-
observer_observations:
|
|
1730
|
-
observer_skips:
|
|
1767
|
+
observer_events: observerEvents,
|
|
1768
|
+
observer_observations: observerObservations,
|
|
1769
|
+
observer_skips: observerSkips,
|
|
1731
1770
|
sentinel_used: false,
|
|
1732
1771
|
risk_score: riskScore,
|
|
1733
1772
|
stacks_detected: stacks,
|
|
1734
|
-
client_version: "0.4.0"
|
|
1773
|
+
client_version: "0.4.0",
|
|
1774
|
+
context_observations_injected: metrics?.contextObsInjected ?? 0,
|
|
1775
|
+
context_total_available: metrics?.contextTotalAvailable ?? 0,
|
|
1776
|
+
recall_attempts: metrics?.recallAttempts ?? 0,
|
|
1777
|
+
recall_hits: metrics?.recallHits ?? 0,
|
|
1778
|
+
search_count: metrics?.searchCount ?? 0,
|
|
1779
|
+
search_results_total: metrics?.searchResultsTotal ?? 0,
|
|
1780
|
+
config_hash: configHash,
|
|
1781
|
+
config_changed: configChanged,
|
|
1782
|
+
config_fingerprint_detail: configFingerprintDetail
|
|
1735
1783
|
};
|
|
1736
1784
|
}
|
|
1737
1785
|
async function sendBeacon(config, beacon) {
|
|
@@ -1753,8 +1801,8 @@ async function sendBeacon(config, beacon) {
|
|
|
1753
1801
|
|
|
1754
1802
|
// src/storage/projects.ts
|
|
1755
1803
|
import { execSync } from "node:child_process";
|
|
1756
|
-
import { existsSync as
|
|
1757
|
-
import { basename as basename2, join as
|
|
1804
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
1805
|
+
import { basename as basename2, join as join4 } from "node:path";
|
|
1758
1806
|
function normaliseGitRemoteUrl(remoteUrl) {
|
|
1759
1807
|
let url = remoteUrl.trim();
|
|
1760
1808
|
url = url.replace(/^(?:https?|ssh|git):\/\//, "");
|
|
@@ -1809,11 +1857,11 @@ function getGitRemoteUrl(directory) {
|
|
|
1809
1857
|
}
|
|
1810
1858
|
}
|
|
1811
1859
|
function readProjectConfigFile(directory) {
|
|
1812
|
-
const configPath =
|
|
1813
|
-
if (!
|
|
1860
|
+
const configPath = join4(directory, ".engrm.json");
|
|
1861
|
+
if (!existsSync3(configPath))
|
|
1814
1862
|
return null;
|
|
1815
1863
|
try {
|
|
1816
|
-
const raw =
|
|
1864
|
+
const raw = readFileSync3(configPath, "utf-8");
|
|
1817
1865
|
const parsed = JSON.parse(raw);
|
|
1818
1866
|
if (typeof parsed["project_id"] !== "string" || !parsed["project_id"]) {
|
|
1819
1867
|
return null;
|
|
@@ -1856,9 +1904,9 @@ function detectProject(directory) {
|
|
|
1856
1904
|
}
|
|
1857
1905
|
|
|
1858
1906
|
// src/capture/transcript.ts
|
|
1859
|
-
import { readFileSync as
|
|
1860
|
-
import { join as
|
|
1861
|
-
import { homedir as
|
|
1907
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
|
|
1908
|
+
import { join as join5 } from "node:path";
|
|
1909
|
+
import { homedir as homedir3 } from "node:os";
|
|
1862
1910
|
|
|
1863
1911
|
// src/tools/save.ts
|
|
1864
1912
|
import { relative, isAbsolute } from "node:path";
|
|
@@ -2430,15 +2478,15 @@ function toRelativePath(filePath, projectRoot) {
|
|
|
2430
2478
|
// src/capture/transcript.ts
|
|
2431
2479
|
function resolveTranscriptPath(sessionId, cwd) {
|
|
2432
2480
|
const encodedCwd = cwd.replace(/\//g, "-");
|
|
2433
|
-
return
|
|
2481
|
+
return join5(homedir3(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
|
|
2434
2482
|
}
|
|
2435
2483
|
function readTranscript(sessionId, cwd) {
|
|
2436
2484
|
const path = resolveTranscriptPath(sessionId, cwd);
|
|
2437
|
-
if (!
|
|
2485
|
+
if (!existsSync4(path))
|
|
2438
2486
|
return [];
|
|
2439
2487
|
let raw;
|
|
2440
2488
|
try {
|
|
2441
|
-
raw =
|
|
2489
|
+
raw = readFileSync4(path, "utf-8");
|
|
2442
2490
|
} catch {
|
|
2443
2491
|
return [];
|
|
2444
2492
|
}
|
|
@@ -2679,7 +2727,8 @@ async function main() {
|
|
|
2679
2727
|
await pushOnce(db, config);
|
|
2680
2728
|
try {
|
|
2681
2729
|
if (event.session_id) {
|
|
2682
|
-
const
|
|
2730
|
+
const metrics = readSessionMetrics(event.session_id);
|
|
2731
|
+
const beacon = buildBeacon(db, config, event.session_id, metrics);
|
|
2683
2732
|
if (beacon) {
|
|
2684
2733
|
await sendBeacon(config, beacon);
|
|
2685
2734
|
}
|
|
@@ -2803,6 +2852,55 @@ function detectUnsavedPlans(message) {
|
|
|
2803
2852
|
}
|
|
2804
2853
|
return hints;
|
|
2805
2854
|
}
|
|
2855
|
+
function readSessionMetrics(sessionId) {
|
|
2856
|
+
const { existsSync: existsSync5, readFileSync: readFileSync5, unlinkSync } = __require("node:fs");
|
|
2857
|
+
const { join: join6 } = __require("node:path");
|
|
2858
|
+
const { homedir: homedir4 } = __require("node:os");
|
|
2859
|
+
const result = {};
|
|
2860
|
+
try {
|
|
2861
|
+
const obsPath = join6(homedir4(), ".engrm", "observer-sessions", `${sessionId}.json`);
|
|
2862
|
+
if (existsSync5(obsPath)) {
|
|
2863
|
+
const state = JSON.parse(readFileSync5(obsPath, "utf-8"));
|
|
2864
|
+
if (typeof state.recallAttempts === "number")
|
|
2865
|
+
result.recallAttempts = state.recallAttempts;
|
|
2866
|
+
if (typeof state.recallHits === "number")
|
|
2867
|
+
result.recallHits = state.recallHits;
|
|
2868
|
+
}
|
|
2869
|
+
} catch {}
|
|
2870
|
+
try {
|
|
2871
|
+
const hookPath = join6(homedir4(), ".engrm", "hook-session-metrics.json");
|
|
2872
|
+
if (existsSync5(hookPath)) {
|
|
2873
|
+
const hookMetrics = JSON.parse(readFileSync5(hookPath, "utf-8"));
|
|
2874
|
+
if (typeof hookMetrics.contextObsInjected === "number")
|
|
2875
|
+
result.contextObsInjected = hookMetrics.contextObsInjected;
|
|
2876
|
+
if (typeof hookMetrics.contextTotalAvailable === "number")
|
|
2877
|
+
result.contextTotalAvailable = hookMetrics.contextTotalAvailable;
|
|
2878
|
+
try {
|
|
2879
|
+
unlinkSync(hookPath);
|
|
2880
|
+
} catch {}
|
|
2881
|
+
}
|
|
2882
|
+
} catch {}
|
|
2883
|
+
try {
|
|
2884
|
+
const mcpPath = join6(homedir4(), ".engrm", "mcp-session-metrics.json");
|
|
2885
|
+
if (existsSync5(mcpPath)) {
|
|
2886
|
+
const metrics = JSON.parse(readFileSync5(mcpPath, "utf-8"));
|
|
2887
|
+
if (typeof metrics.contextObsInjected === "number" && metrics.contextObsInjected > 0) {
|
|
2888
|
+
result.contextObsInjected = metrics.contextObsInjected;
|
|
2889
|
+
}
|
|
2890
|
+
if (typeof metrics.contextTotalAvailable === "number" && metrics.contextTotalAvailable > 0) {
|
|
2891
|
+
result.contextTotalAvailable = metrics.contextTotalAvailable;
|
|
2892
|
+
}
|
|
2893
|
+
if (typeof metrics.searchCount === "number")
|
|
2894
|
+
result.searchCount = metrics.searchCount;
|
|
2895
|
+
if (typeof metrics.searchResultsTotal === "number")
|
|
2896
|
+
result.searchResultsTotal = metrics.searchResultsTotal;
|
|
2897
|
+
try {
|
|
2898
|
+
unlinkSync(mcpPath);
|
|
2899
|
+
} catch {}
|
|
2900
|
+
}
|
|
2901
|
+
} catch {}
|
|
2902
|
+
return result;
|
|
2903
|
+
}
|
|
2806
2904
|
main().catch(() => {
|
|
2807
2905
|
process.exit(0);
|
|
2808
2906
|
});
|
package/dist/server.js
CHANGED
|
@@ -16471,6 +16471,9 @@ function loadPack(name) {
|
|
|
16471
16471
|
}
|
|
16472
16472
|
|
|
16473
16473
|
// src/server.ts
|
|
16474
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
16475
|
+
import { join as join4 } from "node:path";
|
|
16476
|
+
import { homedir as homedir2 } from "node:os";
|
|
16474
16477
|
if (!configExists()) {
|
|
16475
16478
|
console.error("Engrm is not configured. Run: engrm init --manual");
|
|
16476
16479
|
process.exit(1);
|
|
@@ -16479,6 +16482,23 @@ var config2 = loadConfig();
|
|
|
16479
16482
|
var db = new MemDatabase(getDbPath());
|
|
16480
16483
|
var contextServed = false;
|
|
16481
16484
|
var _lastSearchSessionId = null;
|
|
16485
|
+
var sessionMetrics = {
|
|
16486
|
+
contextObsInjected: 0,
|
|
16487
|
+
contextTotalAvailable: 0,
|
|
16488
|
+
recallAttempts: 0,
|
|
16489
|
+
recallHits: 0,
|
|
16490
|
+
searchCount: 0,
|
|
16491
|
+
searchResultsTotal: 0
|
|
16492
|
+
};
|
|
16493
|
+
var MCP_METRICS_PATH = join4(homedir2(), ".engrm", "mcp-session-metrics.json");
|
|
16494
|
+
function persistSessionMetrics() {
|
|
16495
|
+
try {
|
|
16496
|
+
const dir = join4(homedir2(), ".engrm");
|
|
16497
|
+
if (!existsSync4(dir))
|
|
16498
|
+
mkdirSync2(dir, { recursive: true });
|
|
16499
|
+
writeFileSync2(MCP_METRICS_PATH, JSON.stringify(sessionMetrics), "utf-8");
|
|
16500
|
+
} catch {}
|
|
16501
|
+
}
|
|
16482
16502
|
var _detectedAgent = null;
|
|
16483
16503
|
function getDetectedAgent() {
|
|
16484
16504
|
if (_detectedAgent)
|
|
@@ -16591,6 +16611,9 @@ server.tool("search", "Search memory for observations", {
|
|
|
16591
16611
|
}, async (params) => {
|
|
16592
16612
|
const result = await searchObservations(db, params);
|
|
16593
16613
|
_lastSearchSessionId = "active";
|
|
16614
|
+
sessionMetrics.searchCount++;
|
|
16615
|
+
sessionMetrics.searchResultsTotal += result.total;
|
|
16616
|
+
persistSessionMetrics();
|
|
16594
16617
|
if (result.total === 0) {
|
|
16595
16618
|
return {
|
|
16596
16619
|
content: [
|
|
@@ -16846,6 +16869,9 @@ server.tool("session_context", "Load project memory for this session", {
|
|
|
16846
16869
|
};
|
|
16847
16870
|
}
|
|
16848
16871
|
contextServed = true;
|
|
16872
|
+
sessionMetrics.contextObsInjected = context.observations.length;
|
|
16873
|
+
sessionMetrics.contextTotalAvailable = context.total_active;
|
|
16874
|
+
persistSessionMetrics();
|
|
16849
16875
|
return {
|
|
16850
16876
|
content: [
|
|
16851
16877
|
{
|
|
@@ -16874,3 +16900,6 @@ main().catch((error48) => {
|
|
|
16874
16900
|
db.close();
|
|
16875
16901
|
process.exit(1);
|
|
16876
16902
|
});
|
|
16903
|
+
export {
|
|
16904
|
+
sessionMetrics
|
|
16905
|
+
};
|