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.
@@ -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 existsSync4, readdirSync, readFileSync as readFileSync3 } from "node:fs";
1498
- import { join as join4, basename as basename3, dirname } from "node:path";
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 join4(thisDir, "../../packs");
1590
+ return join5(thisDir, "../../packs");
1512
1591
  }
1513
1592
  function listAvailablePacks() {
1514
1593
  const dir = getPacksDir();
1515
- if (!existsSync4(dir))
1594
+ if (!existsSync5(dir))
1516
1595
  return [];
1517
- return readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => basename3(f, ".json"));
1596
+ return readdirSync2(dir).filter((f) => f.endsWith(".json")).map((f) => basename3(f, ".json"));
1518
1597
  }
1519
1598
  function loadPack(name) {
1520
- const packPath = join4(getPacksDir(), `${name}.json`);
1521
- if (!existsSync4(packPath))
1599
+ const packPath = join5(getPacksDir(), `${name}.json`);
1600
+ if (!existsSync5(packPath))
1522
1601
  return null;
1523
1602
  try {
1524
- const raw = readFileSync3(packPath, "utf-8");
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 existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
1560
- import { homedir as homedir2, hostname as hostname2, networkInterfaces as networkInterfaces2 } from "node:os";
1561
- import { join as join5 } from "node:path";
1562
- import { createHash as createHash2 } from "node:crypto";
1563
- var CONFIG_DIR2 = join5(homedir2(), ".engrm");
1564
- var SETTINGS_PATH2 = join5(CONFIG_DIR2, "settings.json");
1565
- var DB_PATH2 = join5(CONFIG_DIR2, "engrm.db");
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 = createHash2("sha256").update(material).digest("hex").slice(0, 8);
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 (!existsSync5(SETTINGS_PATH2)) {
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 = readFileSync4(SETTINGS_PATH2, "utf-8");
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 (!existsSync5(CONFIG_DIR2)) {
1698
- mkdirSync2(CONFIG_DIR2, { recursive: true });
1776
+ if (!existsSync6(CONFIG_DIR2)) {
1777
+ mkdirSync3(CONFIG_DIR2, { recursive: true });
1699
1778
  }
1700
- writeFileSync2(SETTINGS_PATH2, JSON.stringify(config, null, 2) + `
1779
+ writeFileSync3(SETTINGS_PATH2, JSON.stringify(config, null, 2) + `
1701
1780
  `, "utf-8");
1702
1781
  }
1703
1782
  function configExists2() {
1704
- return existsSync5(SETTINGS_PATH2);
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
- if (pullResult.merged > 0) {
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
- if (remaining > 0) {
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
- const msgCount = db.db.query("SELECT COUNT(*) as c FROM observations WHERE type = 'message' AND id > ? AND lifecycle IN ('active', 'pinned')").get(lastReadId)?.c ?? 0;
2136
- if (msgCount > 0) {
2137
- parts.push(`${msgCount} unread message(s)`);
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.error(`Engrm: ${parts.join(" \xB7 ")} \u2014 memory loaded`);
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
  });
@@ -1689,7 +1689,24 @@ function detectStacks(filePaths) {
1689
1689
  }
1690
1690
 
1691
1691
  // src/telemetry/beacon.ts
1692
- function buildBeacon(db, config, sessionId) {
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: 0,
1729
- observer_observations: 0,
1730
- observer_skips: 0,
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 existsSync2, readFileSync as readFileSync2 } from "node:fs";
1757
- import { basename as basename2, join as join3 } from "node:path";
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 = join3(directory, ".engrm.json");
1813
- if (!existsSync2(configPath))
1860
+ const configPath = join4(directory, ".engrm.json");
1861
+ if (!existsSync3(configPath))
1814
1862
  return null;
1815
1863
  try {
1816
- const raw = readFileSync2(configPath, "utf-8");
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 readFileSync3, existsSync as existsSync3 } from "node:fs";
1860
- import { join as join4 } from "node:path";
1861
- import { homedir as homedir2 } from "node:os";
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 join4(homedir2(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
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 (!existsSync3(path))
2485
+ if (!existsSync4(path))
2438
2486
  return [];
2439
2487
  let raw;
2440
2488
  try {
2441
- raw = readFileSync3(path, "utf-8");
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 beacon = buildBeacon(db, config, event.session_id);
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Cross-device, team-shared memory layer for AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",