engrm 0.4.0 → 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.
@@ -2,6 +2,100 @@
2
2
  import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
+ // src/capture/retrospective.ts
6
+ function extractRetrospective(observations, sessionId, projectId, userId) {
7
+ if (observations.length === 0)
8
+ return null;
9
+ const request = extractRequest(observations);
10
+ const investigated = extractInvestigated(observations);
11
+ const learned = extractLearned(observations);
12
+ const completed = extractCompleted(observations);
13
+ const nextSteps = extractNextSteps(observations);
14
+ if (!request && !investigated && !learned && !completed && !nextSteps) {
15
+ return null;
16
+ }
17
+ return {
18
+ session_id: sessionId,
19
+ project_id: projectId,
20
+ user_id: userId,
21
+ request,
22
+ investigated,
23
+ learned,
24
+ completed,
25
+ next_steps: nextSteps
26
+ };
27
+ }
28
+ function extractRequest(observations) {
29
+ const first = observations[0];
30
+ if (!first)
31
+ return null;
32
+ return first.title;
33
+ }
34
+ function extractInvestigated(observations) {
35
+ const discoveries = observations.filter((o) => o.type === "discovery");
36
+ if (discoveries.length === 0)
37
+ return null;
38
+ return discoveries.slice(0, 5).map((o) => {
39
+ const facts = extractTopFacts(o, 2);
40
+ return facts ? `- ${o.title}
41
+ ${facts}` : `- ${o.title}`;
42
+ }).join(`
43
+ `);
44
+ }
45
+ function extractLearned(observations) {
46
+ const learnTypes = new Set(["bugfix", "decision", "pattern"]);
47
+ const learned = observations.filter((o) => learnTypes.has(o.type));
48
+ if (learned.length === 0)
49
+ return null;
50
+ return learned.slice(0, 5).map((o) => {
51
+ const facts = extractTopFacts(o, 2);
52
+ return facts ? `- ${o.title}
53
+ ${facts}` : `- ${o.title}`;
54
+ }).join(`
55
+ `);
56
+ }
57
+ function extractCompleted(observations) {
58
+ const completeTypes = new Set(["change", "feature", "refactor"]);
59
+ const completed = observations.filter((o) => completeTypes.has(o.type));
60
+ if (completed.length === 0)
61
+ return null;
62
+ return completed.slice(0, 5).map((o) => {
63
+ const files = o.files_modified ? parseJsonArray(o.files_modified) : [];
64
+ const fileCtx = files.length > 0 ? ` (${files.slice(0, 2).map((f) => f.split("/").pop()).join(", ")})` : "";
65
+ return `- ${o.title}${fileCtx}`;
66
+ }).join(`
67
+ `);
68
+ }
69
+ function extractNextSteps(observations) {
70
+ if (observations.length < 2)
71
+ return null;
72
+ const lastQuarterStart = Math.floor(observations.length * 0.75);
73
+ const lastQuarter = observations.slice(lastQuarterStart);
74
+ const unresolved = lastQuarter.filter((o) => o.type === "bugfix" && o.narrative && /error|fail|exception/i.test(o.narrative));
75
+ if (unresolved.length === 0)
76
+ return null;
77
+ return unresolved.map((o) => `- Investigate: ${o.title}`).slice(0, 3).join(`
78
+ `);
79
+ }
80
+ function extractTopFacts(obs, n) {
81
+ const facts = parseJsonArray(obs.facts);
82
+ if (facts.length === 0)
83
+ return null;
84
+ return facts.slice(0, n).map((f) => ` ${f}`).join(`
85
+ `);
86
+ }
87
+ function parseJsonArray(json) {
88
+ if (!json)
89
+ return [];
90
+ try {
91
+ const parsed = JSON.parse(json);
92
+ if (Array.isArray(parsed)) {
93
+ return parsed.filter((f) => typeof f === "string" && f.length > 0);
94
+ }
95
+ } catch {}
96
+ return [];
97
+ }
98
+
5
99
  // src/config.ts
6
100
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
101
  import { homedir, hostname, networkInterfaces } from "node:os";
@@ -699,8 +793,8 @@ class MemDatabase {
699
793
  return this.db.query("SELECT * FROM projects WHERE id = ?").get(id) ?? null;
700
794
  }
701
795
  insertObservation(obs) {
702
- const now = Math.floor(Date.now() / 1000);
703
- const createdAt = new Date().toISOString();
796
+ const now = obs.created_at_epoch ?? Math.floor(Date.now() / 1000);
797
+ const createdAt = obs.created_at ?? new Date(now * 1000).toISOString();
704
798
  const result = this.db.query(`INSERT INTO observations (
705
799
  session_id, project_id, type, title, narrative, facts, concepts,
706
800
  files_read, files_modified, quality, lifecycle, sensitivity,
@@ -717,11 +811,14 @@ class MemDatabase {
717
811
  getObservationById(id) {
718
812
  return this.db.query("SELECT * FROM observations WHERE id = ?").get(id) ?? null;
719
813
  }
720
- getObservationsByIds(ids) {
814
+ getObservationsByIds(ids, userId) {
721
815
  if (ids.length === 0)
722
816
  return [];
723
817
  const placeholders = ids.map(() => "?").join(",");
724
- return this.db.query(`SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`).all(...ids);
818
+ const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
819
+ return this.db.query(`SELECT * FROM observations
820
+ WHERE id IN (${placeholders})${visibilityClause}
821
+ ORDER BY created_at_epoch DESC`).all(...ids, ...userId ? [userId] : []);
725
822
  }
726
823
  getRecentObservations(projectId, sincEpoch, limit = 50) {
727
824
  return this.db.query(`SELECT * FROM observations
@@ -729,8 +826,9 @@ class MemDatabase {
729
826
  ORDER BY created_at_epoch DESC
730
827
  LIMIT ?`).all(projectId, sincEpoch, limit);
731
828
  }
732
- searchFts(query, projectId, lifecycles = ["active", "aging", "pinned"], limit = 20) {
829
+ searchFts(query, projectId, lifecycles = ["active", "aging", "pinned"], limit = 20, userId) {
733
830
  const lifecyclePlaceholders = lifecycles.map(() => "?").join(",");
831
+ const visibilityClause = userId ? " AND (o.sensitivity != 'personal' OR o.user_id = ?)" : "";
734
832
  if (projectId !== null) {
735
833
  return this.db.query(`SELECT o.id, observations_fts.rank
736
834
  FROM observations_fts
@@ -738,33 +836,39 @@ class MemDatabase {
738
836
  WHERE observations_fts MATCH ?
739
837
  AND o.project_id = ?
740
838
  AND o.lifecycle IN (${lifecyclePlaceholders})
839
+ ${visibilityClause}
741
840
  ORDER BY observations_fts.rank
742
- LIMIT ?`).all(query, projectId, ...lifecycles, limit);
841
+ LIMIT ?`).all(query, projectId, ...lifecycles, ...userId ? [userId] : [], limit);
743
842
  }
744
843
  return this.db.query(`SELECT o.id, observations_fts.rank
745
844
  FROM observations_fts
746
845
  JOIN observations o ON o.id = observations_fts.rowid
747
846
  WHERE observations_fts MATCH ?
748
847
  AND o.lifecycle IN (${lifecyclePlaceholders})
848
+ ${visibilityClause}
749
849
  ORDER BY observations_fts.rank
750
- LIMIT ?`).all(query, ...lifecycles, limit);
850
+ LIMIT ?`).all(query, ...lifecycles, ...userId ? [userId] : [], limit);
751
851
  }
752
- getTimeline(anchorId, projectId, depthBefore = 3, depthAfter = 3) {
753
- const anchor = this.getObservationById(anchorId);
852
+ getTimeline(anchorId, projectId, depthBefore = 3, depthAfter = 3, userId) {
853
+ const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
854
+ const anchor = this.db.query(`SELECT * FROM observations WHERE id = ?${visibilityClause}`).get(anchorId, ...userId ? [userId] : []) ?? null;
754
855
  if (!anchor)
755
856
  return [];
756
857
  const projectFilter = projectId !== null ? "AND project_id = ?" : "";
757
858
  const projectParams = projectId !== null ? [projectId] : [];
859
+ const visibilityParams = userId ? [userId] : [];
758
860
  const before = this.db.query(`SELECT * FROM observations
759
861
  WHERE created_at_epoch < ? ${projectFilter}
760
862
  AND lifecycle IN ('active', 'aging', 'pinned')
863
+ ${visibilityClause}
761
864
  ORDER BY created_at_epoch DESC
762
- LIMIT ?`).all(anchor.created_at_epoch, ...projectParams, depthBefore);
865
+ LIMIT ?`).all(anchor.created_at_epoch, ...projectParams, ...visibilityParams, depthBefore);
763
866
  const after = this.db.query(`SELECT * FROM observations
764
867
  WHERE created_at_epoch > ? ${projectFilter}
765
868
  AND lifecycle IN ('active', 'aging', 'pinned')
869
+ ${visibilityClause}
766
870
  ORDER BY created_at_epoch ASC
767
- LIMIT ?`).all(anchor.created_at_epoch, ...projectParams, depthAfter);
871
+ LIMIT ?`).all(anchor.created_at_epoch, ...projectParams, ...visibilityParams, depthAfter);
768
872
  return [...before.reverse(), anchor, ...after];
769
873
  }
770
874
  pinObservation(id, pinned) {
@@ -878,11 +982,12 @@ class MemDatabase {
878
982
  return;
879
983
  this.db.query("DELETE FROM vec_observations WHERE observation_id = ?").run(observationId);
880
984
  }
881
- searchVec(queryEmbedding, projectId, lifecycles = ["active", "aging", "pinned"], limit = 20) {
985
+ searchVec(queryEmbedding, projectId, lifecycles = ["active", "aging", "pinned"], limit = 20, userId) {
882
986
  if (!this.vecAvailable)
883
987
  return [];
884
988
  const lifecyclePlaceholders = lifecycles.map(() => "?").join(",");
885
989
  const embeddingBlob = new Uint8Array(queryEmbedding.buffer);
990
+ const visibilityClause = userId ? " AND (o.sensitivity != 'personal' OR o.user_id = ?)" : "";
886
991
  if (projectId !== null) {
887
992
  return this.db.query(`SELECT v.observation_id, v.distance
888
993
  FROM vec_observations v
@@ -891,7 +996,7 @@ class MemDatabase {
891
996
  AND k = ?
892
997
  AND o.project_id = ?
893
998
  AND o.lifecycle IN (${lifecyclePlaceholders})
894
- AND o.superseded_by IS NULL`).all(embeddingBlob, limit, projectId, ...lifecycles);
999
+ AND o.superseded_by IS NULL` + visibilityClause).all(embeddingBlob, limit, projectId, ...lifecycles, ...userId ? [userId] : []);
895
1000
  }
896
1001
  return this.db.query(`SELECT v.observation_id, v.distance
897
1002
  FROM vec_observations v
@@ -899,7 +1004,7 @@ class MemDatabase {
899
1004
  WHERE v.embedding MATCH ?
900
1005
  AND k = ?
901
1006
  AND o.lifecycle IN (${lifecyclePlaceholders})
902
- AND o.superseded_by IS NULL`).all(embeddingBlob, limit, ...lifecycles);
1007
+ AND o.superseded_by IS NULL` + visibilityClause).all(embeddingBlob, limit, ...lifecycles, ...userId ? [userId] : []);
903
1008
  }
904
1009
  getUnembeddedCount() {
905
1010
  if (!this.vecAvailable)
@@ -1018,98 +1123,58 @@ class MemDatabase {
1018
1123
  }
1019
1124
  }
1020
1125
 
1021
- // src/capture/retrospective.ts
1022
- function extractRetrospective(observations, sessionId, projectId, userId) {
1023
- if (observations.length === 0)
1024
- return null;
1025
- const request = extractRequest(observations);
1026
- const investigated = extractInvestigated(observations);
1027
- const learned = extractLearned(observations);
1028
- const completed = extractCompleted(observations);
1029
- const nextSteps = extractNextSteps(observations);
1030
- if (!request && !investigated && !learned && !completed && !nextSteps) {
1031
- return null;
1126
+ // src/hooks/common.ts
1127
+ var c = {
1128
+ dim: "\x1B[2m",
1129
+ yellow: "\x1B[33m",
1130
+ reset: "\x1B[0m"
1131
+ };
1132
+ async function readStdin() {
1133
+ const chunks = [];
1134
+ for await (const chunk of process.stdin) {
1135
+ chunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString());
1032
1136
  }
1033
- return {
1034
- session_id: sessionId,
1035
- project_id: projectId,
1036
- user_id: userId,
1037
- request,
1038
- investigated,
1039
- learned,
1040
- completed,
1041
- next_steps: nextSteps
1042
- };
1043
- }
1044
- function extractRequest(observations) {
1045
- const first = observations[0];
1046
- if (!first)
1047
- return null;
1048
- return first.title;
1137
+ return chunks.join("");
1049
1138
  }
1050
- function extractInvestigated(observations) {
1051
- const discoveries = observations.filter((o) => o.type === "discovery");
1052
- if (discoveries.length === 0)
1139
+ async function parseStdinJson() {
1140
+ const raw = await readStdin();
1141
+ if (!raw.trim())
1053
1142
  return null;
1054
- return discoveries.slice(0, 5).map((o) => {
1055
- const facts = extractTopFacts(o, 2);
1056
- return facts ? `- ${o.title}
1057
- ${facts}` : `- ${o.title}`;
1058
- }).join(`
1059
- `);
1060
- }
1061
- function extractLearned(observations) {
1062
- const learnTypes = new Set(["bugfix", "decision", "pattern"]);
1063
- const learned = observations.filter((o) => learnTypes.has(o.type));
1064
- if (learned.length === 0)
1143
+ try {
1144
+ return JSON.parse(raw);
1145
+ } catch {
1065
1146
  return null;
1066
- return learned.slice(0, 5).map((o) => {
1067
- const facts = extractTopFacts(o, 2);
1068
- return facts ? `- ${o.title}
1069
- ${facts}` : `- ${o.title}`;
1070
- }).join(`
1071
- `);
1147
+ }
1072
1148
  }
1073
- function extractCompleted(observations) {
1074
- const completeTypes = new Set(["change", "feature", "refactor"]);
1075
- const completed = observations.filter((o) => completeTypes.has(o.type));
1076
- if (completed.length === 0)
1149
+ function bootstrapHook(hookName) {
1150
+ if (!configExists()) {
1151
+ warnUser(hookName, "Engrm not configured. Run: npx engrm init");
1077
1152
  return null;
1078
- return completed.slice(0, 5).map((o) => {
1079
- const files = o.files_modified ? parseJsonArray(o.files_modified) : [];
1080
- const fileCtx = files.length > 0 ? ` (${files.slice(0, 2).map((f) => f.split("/").pop()).join(", ")})` : "";
1081
- return `- ${o.title}${fileCtx}`;
1082
- }).join(`
1083
- `);
1084
- }
1085
- function extractNextSteps(observations) {
1086
- if (observations.length < 2)
1153
+ }
1154
+ let config;
1155
+ try {
1156
+ config = loadConfig();
1157
+ } catch (err) {
1158
+ warnUser(hookName, `Config error: ${err instanceof Error ? err.message : String(err)}`);
1087
1159
  return null;
1088
- const lastQuarterStart = Math.floor(observations.length * 0.75);
1089
- const lastQuarter = observations.slice(lastQuarterStart);
1090
- const unresolved = lastQuarter.filter((o) => o.type === "bugfix" && o.narrative && /error|fail|exception/i.test(o.narrative));
1091
- if (unresolved.length === 0)
1160
+ }
1161
+ let db;
1162
+ try {
1163
+ db = new MemDatabase(getDbPath());
1164
+ } catch (err) {
1165
+ warnUser(hookName, `Database error: ${err instanceof Error ? err.message : String(err)}`);
1092
1166
  return null;
1093
- return unresolved.map((o) => `- Investigate: ${o.title}`).slice(0, 3).join(`
1094
- `);
1167
+ }
1168
+ return { config, db };
1095
1169
  }
1096
- function extractTopFacts(obs, n) {
1097
- const facts = parseJsonArray(obs.facts);
1098
- if (facts.length === 0)
1099
- return null;
1100
- return facts.slice(0, n).map((f) => ` ${f}`).join(`
1101
- `);
1170
+ function warnUser(hookName, message) {
1171
+ console.error(`${c.yellow}engrm ${hookName}:${c.reset} ${c.dim}${message}${c.reset}`);
1102
1172
  }
1103
- function parseJsonArray(json) {
1104
- if (!json)
1105
- return [];
1106
- try {
1107
- const parsed = JSON.parse(json);
1108
- if (Array.isArray(parsed)) {
1109
- return parsed.filter((f) => typeof f === "string" && f.length > 0);
1110
- }
1111
- } catch {}
1112
- return [];
1173
+ function runHook(hookName, fn) {
1174
+ fn().catch((err) => {
1175
+ warnUser(hookName, `Unexpected error: ${err instanceof Error ? err.message : String(err)}`);
1176
+ process.exit(0);
1177
+ });
1113
1178
  }
1114
1179
 
1115
1180
  // src/capture/risk-score.ts
@@ -1400,6 +1465,8 @@ function buildVectorDocument(obs, config, project) {
1400
1465
  files_modified: obs.files_modified ? JSON.parse(obs.files_modified) : [],
1401
1466
  session_id: obs.session_id,
1402
1467
  created_at_epoch: obs.created_at_epoch,
1468
+ created_at: obs.created_at,
1469
+ sensitivity: obs.sensitivity,
1403
1470
  local_id: obs.id
1404
1471
  }
1405
1472
  };
@@ -1689,7 +1756,24 @@ function detectStacks(filePaths) {
1689
1756
  }
1690
1757
 
1691
1758
  // src/telemetry/beacon.ts
1692
- function buildBeacon(db, config, sessionId) {
1759
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
1760
+ import { join as join3 } from "node:path";
1761
+ import { homedir as homedir2 } from "node:os";
1762
+ function readObserverState(sessionId) {
1763
+ try {
1764
+ const statePath = join3(homedir2(), ".engrm", "observer-sessions", `${sessionId}.json`);
1765
+ if (!existsSync2(statePath))
1766
+ return { eventCount: 0, saveCount: 0 };
1767
+ const state = JSON.parse(readFileSync2(statePath, "utf-8"));
1768
+ return {
1769
+ eventCount: typeof state.eventCount === "number" ? state.eventCount : 0,
1770
+ saveCount: typeof state.saveCount === "number" ? state.saveCount : 0
1771
+ };
1772
+ } catch {
1773
+ return { eventCount: 0, saveCount: 0 };
1774
+ }
1775
+ }
1776
+ function buildBeacon(db, config, sessionId, metrics) {
1693
1777
  const session = db.getSessionMetrics(sessionId);
1694
1778
  if (!session)
1695
1779
  return null;
@@ -1716,6 +1800,28 @@ function buildBeacon(db, config, sessionId) {
1716
1800
  const row = db.getSessionMetrics(sessionId);
1717
1801
  riskScore = typeof row?.risk_score === "number" ? row.risk_score : 0;
1718
1802
  } catch {}
1803
+ let configHash;
1804
+ let configChanged;
1805
+ let configFingerprintDetail;
1806
+ try {
1807
+ const fpPath = join3(homedir2(), ".engrm", "config-fingerprint.json");
1808
+ if (existsSync2(fpPath)) {
1809
+ const fp = JSON.parse(readFileSync2(fpPath, "utf-8"));
1810
+ configHash = fp.config_hash;
1811
+ configChanged = fp.config_changed;
1812
+ configFingerprintDetail = JSON.stringify({
1813
+ claude_md_hash: fp.claude_md_hash,
1814
+ memory_md_hash: fp.memory_md_hash,
1815
+ engrm_json_hash: fp.engrm_json_hash,
1816
+ memory_file_count: fp.memory_file_count,
1817
+ client_version: fp.client_version
1818
+ });
1819
+ }
1820
+ } catch {}
1821
+ const observerState = readObserverState(sessionId);
1822
+ const observerEvents = observerState.eventCount;
1823
+ const observerObservations = observerState.saveCount;
1824
+ const observerSkips = Math.max(0, observerEvents - observerObservations);
1719
1825
  return {
1720
1826
  device_id: config.device_id,
1721
1827
  agent: session.agent ?? "claude-code",
@@ -1725,13 +1831,22 @@ function buildBeacon(db, config, sessionId) {
1725
1831
  tool_calls_count: session.tool_calls_count ?? 0,
1726
1832
  files_touched_count: session.files_touched_count ?? 0,
1727
1833
  searches_performed: session.searches_performed ?? 0,
1728
- observer_events: 0,
1729
- observer_observations: 0,
1730
- observer_skips: 0,
1834
+ observer_events: observerEvents,
1835
+ observer_observations: observerObservations,
1836
+ observer_skips: observerSkips,
1731
1837
  sentinel_used: false,
1732
1838
  risk_score: riskScore,
1733
1839
  stacks_detected: stacks,
1734
- client_version: "0.4.0"
1840
+ client_version: "0.4.0",
1841
+ context_observations_injected: metrics?.contextObsInjected ?? 0,
1842
+ context_total_available: metrics?.contextTotalAvailable ?? 0,
1843
+ recall_attempts: metrics?.recallAttempts ?? 0,
1844
+ recall_hits: metrics?.recallHits ?? 0,
1845
+ search_count: metrics?.searchCount ?? 0,
1846
+ search_results_total: metrics?.searchResultsTotal ?? 0,
1847
+ config_hash: configHash,
1848
+ config_changed: configChanged,
1849
+ config_fingerprint_detail: configFingerprintDetail
1735
1850
  };
1736
1851
  }
1737
1852
  async function sendBeacon(config, beacon) {
@@ -1753,8 +1868,8 @@ async function sendBeacon(config, beacon) {
1753
1868
 
1754
1869
  // src/storage/projects.ts
1755
1870
  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";
1871
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
1872
+ import { basename as basename2, join as join4 } from "node:path";
1758
1873
  function normaliseGitRemoteUrl(remoteUrl) {
1759
1874
  let url = remoteUrl.trim();
1760
1875
  url = url.replace(/^(?:https?|ssh|git):\/\//, "");
@@ -1809,11 +1924,11 @@ function getGitRemoteUrl(directory) {
1809
1924
  }
1810
1925
  }
1811
1926
  function readProjectConfigFile(directory) {
1812
- const configPath = join3(directory, ".engrm.json");
1813
- if (!existsSync2(configPath))
1927
+ const configPath = join4(directory, ".engrm.json");
1928
+ if (!existsSync3(configPath))
1814
1929
  return null;
1815
1930
  try {
1816
- const raw = readFileSync2(configPath, "utf-8");
1931
+ const raw = readFileSync3(configPath, "utf-8");
1817
1932
  const parsed = JSON.parse(raw);
1818
1933
  if (typeof parsed["project_id"] !== "string" || !parsed["project_id"]) {
1819
1934
  return null;
@@ -1856,9 +1971,9 @@ function detectProject(directory) {
1856
1971
  }
1857
1972
 
1858
1973
  // 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";
1974
+ import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
1975
+ import { join as join5 } from "node:path";
1976
+ import { homedir as homedir3 } from "node:os";
1862
1977
 
1863
1978
  // src/tools/save.ts
1864
1979
  import { relative, isAbsolute } from "node:path";
@@ -2025,6 +2140,12 @@ function scoreQuality(input) {
2025
2140
  case "digest":
2026
2141
  score += 0.3;
2027
2142
  break;
2143
+ case "standard":
2144
+ score += 0.25;
2145
+ break;
2146
+ case "message":
2147
+ score += 0.1;
2148
+ break;
2028
2149
  }
2029
2150
  if (input.narrative && input.narrative.length > 50) {
2030
2151
  score += 0.15;
@@ -2182,9 +2303,9 @@ function mergeConceptsFromBoth(obs1, obs2) {
2182
2303
  try {
2183
2304
  const parsed = JSON.parse(obs.concepts);
2184
2305
  if (Array.isArray(parsed)) {
2185
- for (const c of parsed) {
2186
- if (typeof c === "string")
2187
- concepts.add(c);
2306
+ for (const c2 of parsed) {
2307
+ if (typeof c2 === "string")
2308
+ concepts.add(c2);
2188
2309
  }
2189
2310
  }
2190
2311
  } catch {}
@@ -2428,17 +2549,19 @@ function toRelativePath(filePath, projectRoot) {
2428
2549
  }
2429
2550
 
2430
2551
  // src/capture/transcript.ts
2431
- function resolveTranscriptPath(sessionId, cwd) {
2552
+ function resolveTranscriptPath(sessionId, cwd, transcriptPath) {
2553
+ if (transcriptPath)
2554
+ return transcriptPath;
2432
2555
  const encodedCwd = cwd.replace(/\//g, "-");
2433
- return join4(homedir2(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
2556
+ return join5(homedir3(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
2434
2557
  }
2435
- function readTranscript(sessionId, cwd) {
2436
- const path = resolveTranscriptPath(sessionId, cwd);
2437
- if (!existsSync3(path))
2558
+ function readTranscript(sessionId, cwd, transcriptPath) {
2559
+ const path = resolveTranscriptPath(sessionId, cwd, transcriptPath);
2560
+ if (!existsSync4(path))
2438
2561
  return [];
2439
2562
  let raw;
2440
2563
  try {
2441
- raw = readFileSync3(path, "utf-8");
2564
+ raw = readFileSync4(path, "utf-8");
2442
2565
  } catch {
2443
2566
  return [];
2444
2567
  }
@@ -2586,31 +2709,15 @@ function printRetrospective(summary) {
2586
2709
  `));
2587
2710
  }
2588
2711
  async function main() {
2589
- const chunks = [];
2590
- for await (const chunk of process.stdin) {
2591
- chunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString());
2592
- }
2593
- const raw = chunks.join("");
2594
- if (!raw.trim())
2595
- process.exit(0);
2596
- let event;
2597
- try {
2598
- event = JSON.parse(raw);
2599
- } catch {
2712
+ const event = await parseStdinJson();
2713
+ if (!event)
2600
2714
  process.exit(0);
2601
- }
2602
2715
  if (event.stop_hook_active)
2603
2716
  process.exit(0);
2604
- if (!configExists())
2605
- process.exit(0);
2606
- let config;
2607
- let db;
2608
- try {
2609
- config = loadConfig();
2610
- db = new MemDatabase(getDbPath());
2611
- } catch {
2717
+ const boot = bootstrapHook("stop");
2718
+ if (!boot)
2612
2719
  process.exit(0);
2613
- }
2720
+ const { config, db } = boot;
2614
2721
  try {
2615
2722
  if (event.session_id) {
2616
2723
  db.completeSession(event.session_id);
@@ -2662,7 +2769,7 @@ async function main() {
2662
2769
  }
2663
2770
  if (config.transcript_analysis?.enabled && event.session_id) {
2664
2771
  try {
2665
- const messages = readTranscript(event.session_id, event.cwd);
2772
+ const messages = readTranscript(event.session_id, event.cwd, event.transcript_path);
2666
2773
  if (messages.length > 10) {
2667
2774
  const transcript = truncateTranscript(messages);
2668
2775
  const results = await analyzeTranscript(config, transcript, event.session_id);
@@ -2679,7 +2786,8 @@ async function main() {
2679
2786
  await pushOnce(db, config);
2680
2787
  try {
2681
2788
  if (event.session_id) {
2682
- const beacon = buildBeacon(db, config, event.session_id);
2789
+ const metrics = readSessionMetrics(event.session_id);
2790
+ const beacon = buildBeacon(db, config, event.session_id, metrics);
2683
2791
  if (beacon) {
2684
2792
  await sendBeacon(config, beacon);
2685
2793
  }
@@ -2751,9 +2859,9 @@ ${bullets}`);
2751
2859
  try {
2752
2860
  const parsed = JSON.parse(obs.concepts);
2753
2861
  if (Array.isArray(parsed)) {
2754
- for (const c of parsed) {
2755
- if (typeof c === "string" && c.length > 0)
2756
- allConcepts.add(c);
2862
+ for (const c2 of parsed) {
2863
+ if (typeof c2 === "string" && c2.length > 0)
2864
+ allConcepts.add(c2);
2757
2865
  }
2758
2866
  }
2759
2867
  } catch {}
@@ -2803,6 +2911,53 @@ function detectUnsavedPlans(message) {
2803
2911
  }
2804
2912
  return hints;
2805
2913
  }
2806
- main().catch(() => {
2807
- process.exit(0);
2808
- });
2914
+ function readSessionMetrics(sessionId) {
2915
+ const { existsSync: existsSync5, readFileSync: readFileSync5, unlinkSync } = __require("node:fs");
2916
+ const { join: join6 } = __require("node:path");
2917
+ const { homedir: homedir4 } = __require("node:os");
2918
+ const result = {};
2919
+ try {
2920
+ const obsPath = join6(homedir4(), ".engrm", "observer-sessions", `${sessionId}.json`);
2921
+ if (existsSync5(obsPath)) {
2922
+ const state = JSON.parse(readFileSync5(obsPath, "utf-8"));
2923
+ if (typeof state.recallAttempts === "number")
2924
+ result.recallAttempts = state.recallAttempts;
2925
+ if (typeof state.recallHits === "number")
2926
+ result.recallHits = state.recallHits;
2927
+ }
2928
+ } catch {}
2929
+ try {
2930
+ const hookPath = join6(homedir4(), ".engrm", "hook-session-metrics.json");
2931
+ if (existsSync5(hookPath)) {
2932
+ const hookMetrics = JSON.parse(readFileSync5(hookPath, "utf-8"));
2933
+ if (typeof hookMetrics.contextObsInjected === "number")
2934
+ result.contextObsInjected = hookMetrics.contextObsInjected;
2935
+ if (typeof hookMetrics.contextTotalAvailable === "number")
2936
+ result.contextTotalAvailable = hookMetrics.contextTotalAvailable;
2937
+ try {
2938
+ unlinkSync(hookPath);
2939
+ } catch {}
2940
+ }
2941
+ } catch {}
2942
+ try {
2943
+ const mcpPath = join6(homedir4(), ".engrm", "mcp-session-metrics.json");
2944
+ if (existsSync5(mcpPath)) {
2945
+ const metrics = JSON.parse(readFileSync5(mcpPath, "utf-8"));
2946
+ if (typeof metrics.contextObsInjected === "number" && metrics.contextObsInjected > 0) {
2947
+ result.contextObsInjected = metrics.contextObsInjected;
2948
+ }
2949
+ if (typeof metrics.contextTotalAvailable === "number" && metrics.contextTotalAvailable > 0) {
2950
+ result.contextTotalAvailable = metrics.contextTotalAvailable;
2951
+ }
2952
+ if (typeof metrics.searchCount === "number")
2953
+ result.searchCount = metrics.searchCount;
2954
+ if (typeof metrics.searchResultsTotal === "number")
2955
+ result.searchResultsTotal = metrics.searchResultsTotal;
2956
+ try {
2957
+ unlinkSync(mcpPath);
2958
+ } catch {}
2959
+ }
2960
+ } catch {}
2961
+ return result;
2962
+ }
2963
+ runHook("stop", main);