engrm 0.4.1 → 0.4.4

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,212 @@
2
2
  import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
+ // src/capture/extractor.ts
6
+ var SKIP_TOOLS = new Set([
7
+ "Glob",
8
+ "Grep",
9
+ "Read",
10
+ "WebSearch",
11
+ "WebFetch",
12
+ "Agent"
13
+ ]);
14
+ var SKIP_BASH_PATTERNS = [
15
+ /^\s*(ls|pwd|cd|echo|cat|head|tail|wc|which|whoami|date|uname)\b/,
16
+ /^\s*git\s+(status|log|branch|diff|show|remote)\b/,
17
+ /^\s*(node|bun|npm|npx|yarn|pnpm)\s+--?version\b/,
18
+ /^\s*export\s+/,
19
+ /^\s*#/
20
+ ];
21
+ var TRIVIAL_RESPONSE_PATTERNS = [
22
+ /^$/,
23
+ /^\s*$/,
24
+ /^Already up to date\.$/
25
+ ];
26
+ function extractObservation(event) {
27
+ const { tool_name, tool_input, tool_response } = event;
28
+ if (SKIP_TOOLS.has(tool_name)) {
29
+ return null;
30
+ }
31
+ switch (tool_name) {
32
+ case "Edit":
33
+ return extractFromEdit(tool_input, tool_response);
34
+ case "Write":
35
+ return extractFromWrite(tool_input, tool_response);
36
+ case "Bash":
37
+ return extractFromBash(tool_input, tool_response);
38
+ default:
39
+ if (tool_name.startsWith("mcp__")) {
40
+ return extractFromMcpTool(tool_name, tool_input, tool_response);
41
+ }
42
+ return null;
43
+ }
44
+ }
45
+ function extractFromEdit(input, response) {
46
+ const filePath = input["file_path"];
47
+ if (!filePath)
48
+ return null;
49
+ const oldStr = input["old_string"];
50
+ const newStr = input["new_string"];
51
+ if (!oldStr && !newStr)
52
+ return null;
53
+ if (oldStr && newStr) {
54
+ const oldTrimmed = oldStr.trim();
55
+ const newTrimmed = newStr.trim();
56
+ if (oldTrimmed === newTrimmed)
57
+ return null;
58
+ if (Math.abs(oldTrimmed.length - newTrimmed.length) < 3 && oldTrimmed.length < 20) {
59
+ return null;
60
+ }
61
+ }
62
+ const fileName = filePath.split("/").pop() ?? filePath;
63
+ const changeSize = (newStr?.length ?? 0) - (oldStr?.length ?? 0);
64
+ const verb = changeSize > 50 ? "Extended" : changeSize < -50 ? "Reduced" : "Modified";
65
+ return {
66
+ type: "change",
67
+ title: `${verb} ${fileName}`,
68
+ narrative: buildEditNarrative(oldStr, newStr, filePath),
69
+ files_modified: [filePath]
70
+ };
71
+ }
72
+ function extractFromWrite(input, response) {
73
+ const filePath = input["file_path"];
74
+ if (!filePath)
75
+ return null;
76
+ const content = input["content"];
77
+ const fileName = filePath.split("/").pop() ?? filePath;
78
+ if (content === undefined || content.length < 50)
79
+ return null;
80
+ return {
81
+ type: "change",
82
+ title: `Created ${fileName}`,
83
+ narrative: `New file created: ${filePath}`,
84
+ files_modified: [filePath]
85
+ };
86
+ }
87
+ function extractFromBash(input, response) {
88
+ const command = input["command"];
89
+ if (!command)
90
+ return null;
91
+ for (const pattern of SKIP_BASH_PATTERNS) {
92
+ if (pattern.test(command))
93
+ return null;
94
+ }
95
+ for (const pattern of TRIVIAL_RESPONSE_PATTERNS) {
96
+ if (pattern.test(response.trim()))
97
+ return null;
98
+ }
99
+ const hasError = detectError(response);
100
+ const isTestRun = detectTestRun(command);
101
+ if (isTestRun) {
102
+ return extractTestResult(command, response);
103
+ }
104
+ if (hasError) {
105
+ return {
106
+ type: "bugfix",
107
+ title: summariseCommand(command) + " (error)",
108
+ narrative: `Command: ${truncate(command, 200)}
109
+ Error: ${truncate(response, 500)}`
110
+ };
111
+ }
112
+ if (/\b(npm|bun|yarn|pnpm)\s+(install|add|remove|uninstall)\b/.test(command)) {
113
+ return {
114
+ type: "change",
115
+ title: `Dependency change: ${summariseCommand(command)}`,
116
+ narrative: `Command: ${truncate(command, 200)}
117
+ Output: ${truncate(response, 300)}`
118
+ };
119
+ }
120
+ if (/\b(npm|bun|yarn)\s+(run\s+)?(build|compile|bundle)\b/.test(command)) {
121
+ if (hasError) {
122
+ return {
123
+ type: "bugfix",
124
+ title: `Build failure: ${summariseCommand(command)}`,
125
+ narrative: `Build command failed.
126
+ Command: ${truncate(command, 200)}
127
+ Output: ${truncate(response, 500)}`
128
+ };
129
+ }
130
+ return null;
131
+ }
132
+ if (response.length > 200) {
133
+ return {
134
+ type: "change",
135
+ title: summariseCommand(command),
136
+ narrative: `Command: ${truncate(command, 200)}
137
+ Output: ${truncate(response, 300)}`
138
+ };
139
+ }
140
+ return null;
141
+ }
142
+ function extractFromMcpTool(toolName, input, response) {
143
+ if (toolName.startsWith("mcp__engrm__"))
144
+ return null;
145
+ if (response.length < 100)
146
+ return null;
147
+ const parts = toolName.split("__");
148
+ const serverName = parts[1] ?? "unknown";
149
+ const toolAction = parts[2] ?? "unknown";
150
+ return {
151
+ type: "change",
152
+ title: `${serverName}: ${toolAction}`,
153
+ narrative: `MCP tool ${toolName} called.
154
+ Response: ${truncate(response, 300)}`
155
+ };
156
+ }
157
+ function detectError(response) {
158
+ const lower = response.toLowerCase();
159
+ return lower.includes("error:") || lower.includes("error[") || lower.includes("failed") || lower.includes("exception") || lower.includes("traceback") || lower.includes("panic:") || lower.includes("fatal:") || /exit code [1-9]/.test(lower);
160
+ }
161
+ function detectTestRun(command) {
162
+ return /\b(test|spec|jest|vitest|mocha|pytest|cargo\s+test|go\s+test|bun\s+test)\b/i.test(command);
163
+ }
164
+ function extractTestResult(command, response) {
165
+ const hasFailure = /[1-9]\d*\s+(fail|failed|failures?)\b/i.test(response) || /\bFAILED\b/.test(response) || /\berror\b/i.test(response);
166
+ const hasPass = /\d+\s+(pass|passed|ok)\b/i.test(response) || /\bPASS\b/.test(response);
167
+ if (hasFailure) {
168
+ return {
169
+ type: "bugfix",
170
+ title: `Test failure: ${summariseCommand(command)}`,
171
+ narrative: `Test run failed.
172
+ Command: ${truncate(command, 200)}
173
+ Output: ${truncate(response, 500)}`
174
+ };
175
+ }
176
+ if (hasPass && !hasFailure) {
177
+ return null;
178
+ }
179
+ return null;
180
+ }
181
+ function buildEditNarrative(oldStr, newStr, filePath) {
182
+ const parts = [`File: ${filePath}`];
183
+ if (oldStr && newStr) {
184
+ const oldLines = oldStr.split(`
185
+ `).length;
186
+ const newLines = newStr.split(`
187
+ `).length;
188
+ if (oldLines !== newLines) {
189
+ parts.push(`Lines: ${oldLines} → ${newLines}`);
190
+ }
191
+ parts.push(`Replaced: ${truncate(oldStr, 100)}`);
192
+ parts.push(`With: ${truncate(newStr, 100)}`);
193
+ } else if (newStr) {
194
+ parts.push(`Added: ${truncate(newStr, 150)}`);
195
+ }
196
+ return parts.join(`
197
+ `);
198
+ }
199
+ function summariseCommand(command) {
200
+ const trimmed = command.trim();
201
+ const firstLine = trimmed.split(`
202
+ `)[0] ?? trimmed;
203
+ return truncate(firstLine, 80);
204
+ }
205
+ function truncate(text, maxLen) {
206
+ if (text.length <= maxLen)
207
+ return text;
208
+ return text.slice(0, maxLen - 3) + "...";
209
+ }
210
+
5
211
  // src/config.ts
6
212
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
213
  import { homedir, hostname, networkInterfaces } from "node:os";
@@ -699,8 +905,8 @@ class MemDatabase {
699
905
  return this.db.query("SELECT * FROM projects WHERE id = ?").get(id) ?? null;
700
906
  }
701
907
  insertObservation(obs) {
702
- const now = Math.floor(Date.now() / 1000);
703
- const createdAt = new Date().toISOString();
908
+ const now = obs.created_at_epoch ?? Math.floor(Date.now() / 1000);
909
+ const createdAt = obs.created_at ?? new Date(now * 1000).toISOString();
704
910
  const result = this.db.query(`INSERT INTO observations (
705
911
  session_id, project_id, type, title, narrative, facts, concepts,
706
912
  files_read, files_modified, quality, lifecycle, sensitivity,
@@ -717,11 +923,14 @@ class MemDatabase {
717
923
  getObservationById(id) {
718
924
  return this.db.query("SELECT * FROM observations WHERE id = ?").get(id) ?? null;
719
925
  }
720
- getObservationsByIds(ids) {
926
+ getObservationsByIds(ids, userId) {
721
927
  if (ids.length === 0)
722
928
  return [];
723
929
  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);
930
+ const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
931
+ return this.db.query(`SELECT * FROM observations
932
+ WHERE id IN (${placeholders})${visibilityClause}
933
+ ORDER BY created_at_epoch DESC`).all(...ids, ...userId ? [userId] : []);
725
934
  }
726
935
  getRecentObservations(projectId, sincEpoch, limit = 50) {
727
936
  return this.db.query(`SELECT * FROM observations
@@ -729,8 +938,9 @@ class MemDatabase {
729
938
  ORDER BY created_at_epoch DESC
730
939
  LIMIT ?`).all(projectId, sincEpoch, limit);
731
940
  }
732
- searchFts(query, projectId, lifecycles = ["active", "aging", "pinned"], limit = 20) {
941
+ searchFts(query, projectId, lifecycles = ["active", "aging", "pinned"], limit = 20, userId) {
733
942
  const lifecyclePlaceholders = lifecycles.map(() => "?").join(",");
943
+ const visibilityClause = userId ? " AND (o.sensitivity != 'personal' OR o.user_id = ?)" : "";
734
944
  if (projectId !== null) {
735
945
  return this.db.query(`SELECT o.id, observations_fts.rank
736
946
  FROM observations_fts
@@ -738,33 +948,39 @@ class MemDatabase {
738
948
  WHERE observations_fts MATCH ?
739
949
  AND o.project_id = ?
740
950
  AND o.lifecycle IN (${lifecyclePlaceholders})
951
+ ${visibilityClause}
741
952
  ORDER BY observations_fts.rank
742
- LIMIT ?`).all(query, projectId, ...lifecycles, limit);
953
+ LIMIT ?`).all(query, projectId, ...lifecycles, ...userId ? [userId] : [], limit);
743
954
  }
744
955
  return this.db.query(`SELECT o.id, observations_fts.rank
745
956
  FROM observations_fts
746
957
  JOIN observations o ON o.id = observations_fts.rowid
747
958
  WHERE observations_fts MATCH ?
748
959
  AND o.lifecycle IN (${lifecyclePlaceholders})
960
+ ${visibilityClause}
749
961
  ORDER BY observations_fts.rank
750
- LIMIT ?`).all(query, ...lifecycles, limit);
962
+ LIMIT ?`).all(query, ...lifecycles, ...userId ? [userId] : [], limit);
751
963
  }
752
- getTimeline(anchorId, projectId, depthBefore = 3, depthAfter = 3) {
753
- const anchor = this.getObservationById(anchorId);
964
+ getTimeline(anchorId, projectId, depthBefore = 3, depthAfter = 3, userId) {
965
+ const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
966
+ const anchor = this.db.query(`SELECT * FROM observations WHERE id = ?${visibilityClause}`).get(anchorId, ...userId ? [userId] : []) ?? null;
754
967
  if (!anchor)
755
968
  return [];
756
969
  const projectFilter = projectId !== null ? "AND project_id = ?" : "";
757
970
  const projectParams = projectId !== null ? [projectId] : [];
971
+ const visibilityParams = userId ? [userId] : [];
758
972
  const before = this.db.query(`SELECT * FROM observations
759
973
  WHERE created_at_epoch < ? ${projectFilter}
760
974
  AND lifecycle IN ('active', 'aging', 'pinned')
975
+ ${visibilityClause}
761
976
  ORDER BY created_at_epoch DESC
762
- LIMIT ?`).all(anchor.created_at_epoch, ...projectParams, depthBefore);
977
+ LIMIT ?`).all(anchor.created_at_epoch, ...projectParams, ...visibilityParams, depthBefore);
763
978
  const after = this.db.query(`SELECT * FROM observations
764
979
  WHERE created_at_epoch > ? ${projectFilter}
765
980
  AND lifecycle IN ('active', 'aging', 'pinned')
981
+ ${visibilityClause}
766
982
  ORDER BY created_at_epoch ASC
767
- LIMIT ?`).all(anchor.created_at_epoch, ...projectParams, depthAfter);
983
+ LIMIT ?`).all(anchor.created_at_epoch, ...projectParams, ...visibilityParams, depthAfter);
768
984
  return [...before.reverse(), anchor, ...after];
769
985
  }
770
986
  pinObservation(id, pinned) {
@@ -878,11 +1094,12 @@ class MemDatabase {
878
1094
  return;
879
1095
  this.db.query("DELETE FROM vec_observations WHERE observation_id = ?").run(observationId);
880
1096
  }
881
- searchVec(queryEmbedding, projectId, lifecycles = ["active", "aging", "pinned"], limit = 20) {
1097
+ searchVec(queryEmbedding, projectId, lifecycles = ["active", "aging", "pinned"], limit = 20, userId) {
882
1098
  if (!this.vecAvailable)
883
1099
  return [];
884
1100
  const lifecyclePlaceholders = lifecycles.map(() => "?").join(",");
885
1101
  const embeddingBlob = new Uint8Array(queryEmbedding.buffer);
1102
+ const visibilityClause = userId ? " AND (o.sensitivity != 'personal' OR o.user_id = ?)" : "";
886
1103
  if (projectId !== null) {
887
1104
  return this.db.query(`SELECT v.observation_id, v.distance
888
1105
  FROM vec_observations v
@@ -891,7 +1108,7 @@ class MemDatabase {
891
1108
  AND k = ?
892
1109
  AND o.project_id = ?
893
1110
  AND o.lifecycle IN (${lifecyclePlaceholders})
894
- AND o.superseded_by IS NULL`).all(embeddingBlob, limit, projectId, ...lifecycles);
1111
+ AND o.superseded_by IS NULL` + visibilityClause).all(embeddingBlob, limit, projectId, ...lifecycles, ...userId ? [userId] : []);
895
1112
  }
896
1113
  return this.db.query(`SELECT v.observation_id, v.distance
897
1114
  FROM vec_observations v
@@ -899,7 +1116,7 @@ class MemDatabase {
899
1116
  WHERE v.embedding MATCH ?
900
1117
  AND k = ?
901
1118
  AND o.lifecycle IN (${lifecyclePlaceholders})
902
- AND o.superseded_by IS NULL`).all(embeddingBlob, limit, ...lifecycles);
1119
+ AND o.superseded_by IS NULL` + visibilityClause).all(embeddingBlob, limit, ...lifecycles, ...userId ? [userId] : []);
903
1120
  }
904
1121
  getUnembeddedCount() {
905
1122
  if (!this.vecAvailable)
@@ -1018,210 +1235,58 @@ class MemDatabase {
1018
1235
  }
1019
1236
  }
1020
1237
 
1021
- // src/capture/extractor.ts
1022
- var SKIP_TOOLS = new Set([
1023
- "Glob",
1024
- "Grep",
1025
- "Read",
1026
- "WebSearch",
1027
- "WebFetch",
1028
- "Agent"
1029
- ]);
1030
- var SKIP_BASH_PATTERNS = [
1031
- /^\s*(ls|pwd|cd|echo|cat|head|tail|wc|which|whoami|date|uname)\b/,
1032
- /^\s*git\s+(status|log|branch|diff|show|remote)\b/,
1033
- /^\s*(node|bun|npm|npx|yarn|pnpm)\s+--?version\b/,
1034
- /^\s*export\s+/,
1035
- /^\s*#/
1036
- ];
1037
- var TRIVIAL_RESPONSE_PATTERNS = [
1038
- /^$/,
1039
- /^\s*$/,
1040
- /^Already up to date\.$/
1041
- ];
1042
- function extractObservation(event) {
1043
- const { tool_name, tool_input, tool_response } = event;
1044
- if (SKIP_TOOLS.has(tool_name)) {
1045
- return null;
1046
- }
1047
- switch (tool_name) {
1048
- case "Edit":
1049
- return extractFromEdit(tool_input, tool_response);
1050
- case "Write":
1051
- return extractFromWrite(tool_input, tool_response);
1052
- case "Bash":
1053
- return extractFromBash(tool_input, tool_response);
1054
- default:
1055
- if (tool_name.startsWith("mcp__")) {
1056
- return extractFromMcpTool(tool_name, tool_input, tool_response);
1057
- }
1058
- return null;
1238
+ // src/hooks/common.ts
1239
+ var c = {
1240
+ dim: "\x1B[2m",
1241
+ yellow: "\x1B[33m",
1242
+ reset: "\x1B[0m"
1243
+ };
1244
+ async function readStdin() {
1245
+ const chunks = [];
1246
+ for await (const chunk of process.stdin) {
1247
+ chunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString());
1059
1248
  }
1249
+ return chunks.join("");
1060
1250
  }
1061
- function extractFromEdit(input, response) {
1062
- const filePath = input["file_path"];
1063
- if (!filePath)
1251
+ async function parseStdinJson() {
1252
+ const raw = await readStdin();
1253
+ if (!raw.trim())
1064
1254
  return null;
1065
- const oldStr = input["old_string"];
1066
- const newStr = input["new_string"];
1067
- if (!oldStr && !newStr)
1255
+ try {
1256
+ return JSON.parse(raw);
1257
+ } catch {
1068
1258
  return null;
1069
- if (oldStr && newStr) {
1070
- const oldTrimmed = oldStr.trim();
1071
- const newTrimmed = newStr.trim();
1072
- if (oldTrimmed === newTrimmed)
1073
- return null;
1074
- if (Math.abs(oldTrimmed.length - newTrimmed.length) < 3 && oldTrimmed.length < 20) {
1075
- return null;
1076
- }
1077
1259
  }
1078
- const fileName = filePath.split("/").pop() ?? filePath;
1079
- const changeSize = (newStr?.length ?? 0) - (oldStr?.length ?? 0);
1080
- const verb = changeSize > 50 ? "Extended" : changeSize < -50 ? "Reduced" : "Modified";
1081
- return {
1082
- type: "change",
1083
- title: `${verb} ${fileName}`,
1084
- narrative: buildEditNarrative(oldStr, newStr, filePath),
1085
- files_modified: [filePath]
1086
- };
1087
1260
  }
1088
- function extractFromWrite(input, response) {
1089
- const filePath = input["file_path"];
1090
- if (!filePath)
1091
- return null;
1092
- const content = input["content"];
1093
- const fileName = filePath.split("/").pop() ?? filePath;
1094
- if (content === undefined || content.length < 50)
1095
- return null;
1096
- return {
1097
- type: "change",
1098
- title: `Created ${fileName}`,
1099
- narrative: `New file created: ${filePath}`,
1100
- files_modified: [filePath]
1101
- };
1102
- }
1103
- function extractFromBash(input, response) {
1104
- const command = input["command"];
1105
- if (!command)
1106
- return null;
1107
- for (const pattern of SKIP_BASH_PATTERNS) {
1108
- if (pattern.test(command))
1109
- return null;
1110
- }
1111
- for (const pattern of TRIVIAL_RESPONSE_PATTERNS) {
1112
- if (pattern.test(response.trim()))
1113
- return null;
1114
- }
1115
- const hasError = detectError(response);
1116
- const isTestRun = detectTestRun(command);
1117
- if (isTestRun) {
1118
- return extractTestResult(command, response);
1119
- }
1120
- if (hasError) {
1121
- return {
1122
- type: "bugfix",
1123
- title: summariseCommand(command) + " (error)",
1124
- narrative: `Command: ${truncate(command, 200)}
1125
- Error: ${truncate(response, 500)}`
1126
- };
1127
- }
1128
- if (/\b(npm|bun|yarn|pnpm)\s+(install|add|remove|uninstall)\b/.test(command)) {
1129
- return {
1130
- type: "change",
1131
- title: `Dependency change: ${summariseCommand(command)}`,
1132
- narrative: `Command: ${truncate(command, 200)}
1133
- Output: ${truncate(response, 300)}`
1134
- };
1135
- }
1136
- if (/\b(npm|bun|yarn)\s+(run\s+)?(build|compile|bundle)\b/.test(command)) {
1137
- if (hasError) {
1138
- return {
1139
- type: "bugfix",
1140
- title: `Build failure: ${summariseCommand(command)}`,
1141
- narrative: `Build command failed.
1142
- Command: ${truncate(command, 200)}
1143
- Output: ${truncate(response, 500)}`
1144
- };
1145
- }
1261
+ function bootstrapHook(hookName) {
1262
+ if (!configExists()) {
1263
+ warnUser(hookName, "Engrm not configured. Run: npx engrm init");
1146
1264
  return null;
1147
1265
  }
1148
- if (response.length > 200) {
1149
- return {
1150
- type: "change",
1151
- title: summariseCommand(command),
1152
- narrative: `Command: ${truncate(command, 200)}
1153
- Output: ${truncate(response, 300)}`
1154
- };
1155
- }
1156
- return null;
1157
- }
1158
- function extractFromMcpTool(toolName, input, response) {
1159
- if (toolName.startsWith("mcp__engrm__"))
1160
- return null;
1161
- if (response.length < 100)
1266
+ let config;
1267
+ try {
1268
+ config = loadConfig();
1269
+ } catch (err) {
1270
+ warnUser(hookName, `Config error: ${err instanceof Error ? err.message : String(err)}`);
1162
1271
  return null;
1163
- const parts = toolName.split("__");
1164
- const serverName = parts[1] ?? "unknown";
1165
- const toolAction = parts[2] ?? "unknown";
1166
- return {
1167
- type: "change",
1168
- title: `${serverName}: ${toolAction}`,
1169
- narrative: `MCP tool ${toolName} called.
1170
- Response: ${truncate(response, 300)}`
1171
- };
1172
- }
1173
- function detectError(response) {
1174
- const lower = response.toLowerCase();
1175
- return lower.includes("error:") || lower.includes("error[") || lower.includes("failed") || lower.includes("exception") || lower.includes("traceback") || lower.includes("panic:") || lower.includes("fatal:") || /exit code [1-9]/.test(lower);
1176
- }
1177
- function detectTestRun(command) {
1178
- return /\b(test|spec|jest|vitest|mocha|pytest|cargo\s+test|go\s+test|bun\s+test)\b/i.test(command);
1179
- }
1180
- function extractTestResult(command, response) {
1181
- const hasFailure = /[1-9]\d*\s+(fail|failed|failures?)\b/i.test(response) || /\bFAILED\b/.test(response) || /\berror\b/i.test(response);
1182
- const hasPass = /\d+\s+(pass|passed|ok)\b/i.test(response) || /\bPASS\b/.test(response);
1183
- if (hasFailure) {
1184
- return {
1185
- type: "bugfix",
1186
- title: `Test failure: ${summariseCommand(command)}`,
1187
- narrative: `Test run failed.
1188
- Command: ${truncate(command, 200)}
1189
- Output: ${truncate(response, 500)}`
1190
- };
1191
1272
  }
1192
- if (hasPass && !hasFailure) {
1273
+ let db;
1274
+ try {
1275
+ db = new MemDatabase(getDbPath());
1276
+ } catch (err) {
1277
+ warnUser(hookName, `Database error: ${err instanceof Error ? err.message : String(err)}`);
1193
1278
  return null;
1194
1279
  }
1195
- return null;
1280
+ return { config, db };
1196
1281
  }
1197
- function buildEditNarrative(oldStr, newStr, filePath) {
1198
- const parts = [`File: ${filePath}`];
1199
- if (oldStr && newStr) {
1200
- const oldLines = oldStr.split(`
1201
- `).length;
1202
- const newLines = newStr.split(`
1203
- `).length;
1204
- if (oldLines !== newLines) {
1205
- parts.push(`Lines: ${oldLines} → ${newLines}`);
1206
- }
1207
- parts.push(`Replaced: ${truncate(oldStr, 100)}`);
1208
- parts.push(`With: ${truncate(newStr, 100)}`);
1209
- } else if (newStr) {
1210
- parts.push(`Added: ${truncate(newStr, 150)}`);
1211
- }
1212
- return parts.join(`
1213
- `);
1214
- }
1215
- function summariseCommand(command) {
1216
- const trimmed = command.trim();
1217
- const firstLine = trimmed.split(`
1218
- `)[0] ?? trimmed;
1219
- return truncate(firstLine, 80);
1282
+ function warnUser(hookName, message) {
1283
+ console.error(`${c.yellow}engrm ${hookName}:${c.reset} ${c.dim}${message}${c.reset}`);
1220
1284
  }
1221
- function truncate(text, maxLen) {
1222
- if (text.length <= maxLen)
1223
- return text;
1224
- return text.slice(0, maxLen - 3) + "...";
1285
+ function runHook(hookName, fn) {
1286
+ fn().catch((err) => {
1287
+ warnUser(hookName, `Unexpected error: ${err instanceof Error ? err.message : String(err)}`);
1288
+ process.exit(0);
1289
+ });
1225
1290
  }
1226
1291
 
1227
1292
  // src/tools/save.ts
@@ -1389,6 +1454,12 @@ function scoreQuality(input) {
1389
1454
  case "digest":
1390
1455
  score += 0.3;
1391
1456
  break;
1457
+ case "standard":
1458
+ score += 0.25;
1459
+ break;
1460
+ case "message":
1461
+ score += 0.1;
1462
+ break;
1392
1463
  }
1393
1464
  if (input.narrative && input.narrative.length > 50) {
1394
1465
  score += 0.15;
@@ -1709,9 +1780,9 @@ function mergeConceptsFromBoth(obs1, obs2) {
1709
1780
  try {
1710
1781
  const parsed = JSON.parse(obs.concepts);
1711
1782
  if (Array.isArray(parsed)) {
1712
- for (const c of parsed) {
1713
- if (typeof c === "string")
1714
- concepts.add(c);
1783
+ for (const c2 of parsed) {
1784
+ if (typeof c2 === "string")
1785
+ concepts.add(c2);
1715
1786
  }
1716
1787
  }
1717
1788
  } catch {}
@@ -2601,29 +2672,13 @@ function checkSessionFatigue(db, sessionId) {
2601
2672
 
2602
2673
  // hooks/post-tool-use.ts
2603
2674
  async function main() {
2604
- const chunks = [];
2605
- for await (const chunk of process.stdin) {
2606
- chunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString());
2607
- }
2608
- const raw = chunks.join("");
2609
- if (!raw.trim())
2675
+ const event = await parseStdinJson();
2676
+ if (!event)
2610
2677
  process.exit(0);
2611
- let event;
2612
- try {
2613
- event = JSON.parse(raw);
2614
- } catch {
2678
+ const boot = bootstrapHook("post-tool-use");
2679
+ if (!boot)
2615
2680
  process.exit(0);
2616
- }
2617
- if (!configExists())
2618
- process.exit(0);
2619
- let config;
2620
- let db;
2621
- try {
2622
- config = loadConfig();
2623
- db = new MemDatabase(getDbPath());
2624
- } catch {
2625
- process.exit(0);
2626
- }
2681
+ const { config, db } = boot;
2627
2682
  try {
2628
2683
  if (event.session_id) {
2629
2684
  const detected = detectProject(event.cwd);
@@ -2786,6 +2841,4 @@ function incrementRecallMetrics(sessionId, hit) {
2786
2841
  writeFileSync3(path, JSON.stringify(state), "utf-8");
2787
2842
  } catch {}
2788
2843
  }
2789
- main().catch(() => {
2790
- process.exit(0);
2791
- });
2844
+ runHook("post-tool-use", main);