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.
- package/README.md +49 -5
- package/dist/cli.js +288 -28
- package/dist/hooks/codex-stop.js +62 -0
- package/dist/hooks/elicitation-result.js +1690 -1637
- package/dist/hooks/post-tool-use.js +326 -231
- package/dist/hooks/pre-compact.js +410 -78
- package/dist/hooks/sentinel.js +150 -103
- package/dist/hooks/session-start.js +2311 -1983
- package/dist/hooks/stop.js +302 -147
- package/dist/server.js +634 -118
- package/package.json +6 -5
- package/bin/build.mjs +0 -97
- package/bin/engrm.mjs +0 -13
package/dist/hooks/stop.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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/
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
const
|
|
1029
|
-
const
|
|
1030
|
-
|
|
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
|
|
1051
|
-
const
|
|
1052
|
-
if (
|
|
1139
|
+
async function parseStdinJson() {
|
|
1140
|
+
const raw = await readStdin();
|
|
1141
|
+
if (!raw.trim())
|
|
1053
1142
|
return null;
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
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
|
-
|
|
1067
|
-
const facts = extractTopFacts(o, 2);
|
|
1068
|
-
return facts ? `- ${o.title}
|
|
1069
|
-
${facts}` : `- ${o.title}`;
|
|
1070
|
-
}).join(`
|
|
1071
|
-
`);
|
|
1147
|
+
}
|
|
1072
1148
|
}
|
|
1073
|
-
function
|
|
1074
|
-
|
|
1075
|
-
|
|
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
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
})
|
|
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
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
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
|
-
|
|
1094
|
-
|
|
1167
|
+
}
|
|
1168
|
+
return { config, db };
|
|
1095
1169
|
}
|
|
1096
|
-
function
|
|
1097
|
-
|
|
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
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
-
|
|
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:
|
|
1729
|
-
observer_observations:
|
|
1730
|
-
observer_skips:
|
|
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
|
|
1757
|
-
import { basename as basename2, join as
|
|
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 =
|
|
1813
|
-
if (!
|
|
1927
|
+
const configPath = join4(directory, ".engrm.json");
|
|
1928
|
+
if (!existsSync3(configPath))
|
|
1814
1929
|
return null;
|
|
1815
1930
|
try {
|
|
1816
|
-
const raw =
|
|
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
|
|
1860
|
-
import { join as
|
|
1861
|
-
import { homedir as
|
|
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
|
|
2186
|
-
if (typeof
|
|
2187
|
-
concepts.add(
|
|
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
|
|
2556
|
+
return join5(homedir3(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
|
|
2434
2557
|
}
|
|
2435
|
-
function readTranscript(sessionId, cwd) {
|
|
2436
|
-
const path = resolveTranscriptPath(sessionId, cwd);
|
|
2437
|
-
if (!
|
|
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 =
|
|
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
|
|
2590
|
-
|
|
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
|
-
|
|
2605
|
-
|
|
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
|
|
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
|
|
2755
|
-
if (typeof
|
|
2756
|
-
allConcepts.add(
|
|
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
|
-
|
|
2807
|
-
|
|
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);
|