engrm 0.4.18 → 0.4.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hooks/session-start.js +16 -12
- package/dist/hooks/stop.js +166 -83
- package/dist/server.js +1 -1
- package/package.json +1 -1
|
@@ -1154,7 +1154,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
|
|
|
1154
1154
|
import { join as join3 } from "node:path";
|
|
1155
1155
|
import { homedir } from "node:os";
|
|
1156
1156
|
var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
|
|
1157
|
-
var CLIENT_VERSION = "0.4.
|
|
1157
|
+
var CLIENT_VERSION = "0.4.19";
|
|
1158
1158
|
function hashFile(filePath) {
|
|
1159
1159
|
try {
|
|
1160
1160
|
if (!existsSync3(filePath))
|
|
@@ -3210,13 +3210,13 @@ function formatSplashScreen(data) {
|
|
|
3210
3210
|
}
|
|
3211
3211
|
}
|
|
3212
3212
|
const contextIndex = formatContextIndex(data.context, handoffShownItems);
|
|
3213
|
-
if (contextIndex.length > 0) {
|
|
3213
|
+
if (contextIndex.lines.length > 0) {
|
|
3214
3214
|
lines.push("");
|
|
3215
|
-
for (const line of contextIndex) {
|
|
3215
|
+
for (const line of contextIndex.lines) {
|
|
3216
3216
|
lines.push(` ${line}`);
|
|
3217
3217
|
}
|
|
3218
3218
|
}
|
|
3219
|
-
const inspectHints = formatInspectHints(data.context);
|
|
3219
|
+
const inspectHints = formatInspectHints(data.context, contextIndex.observationIds);
|
|
3220
3220
|
if (inspectHints.length > 0) {
|
|
3221
3221
|
lines.push("");
|
|
3222
3222
|
for (const line of inspectHints) {
|
|
@@ -3353,19 +3353,23 @@ function formatLegend() {
|
|
|
3353
3353
|
];
|
|
3354
3354
|
}
|
|
3355
3355
|
function formatContextIndex(context, shownItems) {
|
|
3356
|
-
const
|
|
3356
|
+
const selected = pickContextIndexObservations(context, shownItems);
|
|
3357
|
+
const rows = selected.map((obs) => {
|
|
3357
3358
|
const icon = observationIcon(obs.type);
|
|
3358
3359
|
const fileHint = extractPrimaryFileHint(obs);
|
|
3359
3360
|
return `${icon} #${obs.id} ${truncateInline(obs.title, 110)}${fileHint ? ` ${c2.dim}(${fileHint})${c2.reset}` : ""}`;
|
|
3360
3361
|
});
|
|
3361
3362
|
if (rows.length === 0)
|
|
3362
|
-
return [];
|
|
3363
|
-
return
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3363
|
+
return { lines: [], observationIds: [] };
|
|
3364
|
+
return {
|
|
3365
|
+
lines: [
|
|
3366
|
+
`${c2.dim}Handoff index:${c2.reset} use IDs when you want the deeper thread`,
|
|
3367
|
+
...rows
|
|
3368
|
+
],
|
|
3369
|
+
observationIds: selected.map((obs) => obs.id)
|
|
3370
|
+
};
|
|
3367
3371
|
}
|
|
3368
|
-
function formatInspectHints(context) {
|
|
3372
|
+
function formatInspectHints(context, visibleObservationIds = []) {
|
|
3369
3373
|
const hints = [];
|
|
3370
3374
|
if ((context.recentSessions?.length ?? 0) > 0) {
|
|
3371
3375
|
hints.push("recent_sessions");
|
|
@@ -3380,7 +3384,7 @@ function formatInspectHints(context) {
|
|
|
3380
3384
|
const unique = Array.from(new Set(hints)).slice(0, 4);
|
|
3381
3385
|
if (unique.length === 0)
|
|
3382
3386
|
return [];
|
|
3383
|
-
const ids =
|
|
3387
|
+
const ids = visibleObservationIds.slice(0, 5);
|
|
3384
3388
|
const fetchHint = ids.length > 0 ? `get_observations([${ids.join(", ")}])` : null;
|
|
3385
3389
|
return [
|
|
3386
3390
|
`${c2.dim}Next look:${c2.reset} ${unique.join(" \xB7 ")}`,
|
package/dist/hooks/stop.js
CHANGED
|
@@ -237,6 +237,84 @@ function normalizeObservationKey(value) {
|
|
|
237
237
|
return value.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\b(modified|updated|edited|touched|changed)\b/g, "").replace(/\s+/g, " ").trim();
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
// src/intelligence/summary-sections.ts
|
|
241
|
+
function extractSummaryItems(section, limit) {
|
|
242
|
+
if (!section || !section.trim())
|
|
243
|
+
return [];
|
|
244
|
+
const rawLines = section.split(`
|
|
245
|
+
`).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
246
|
+
const items = [];
|
|
247
|
+
const seen = new Set;
|
|
248
|
+
let heading = null;
|
|
249
|
+
for (const rawLine of rawLines) {
|
|
250
|
+
const line = stripSectionPrefix(rawLine);
|
|
251
|
+
if (!line)
|
|
252
|
+
continue;
|
|
253
|
+
const headingOnly = parseHeading(line);
|
|
254
|
+
if (headingOnly) {
|
|
255
|
+
heading = headingOnly;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const isBullet = /^[-*•]\s+/.test(line);
|
|
259
|
+
const stripped = line.replace(/^[-*•]\s+/, "").trim();
|
|
260
|
+
if (!stripped)
|
|
261
|
+
continue;
|
|
262
|
+
const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
|
|
263
|
+
const normalized = normalizeItem(item);
|
|
264
|
+
if (!normalized || seen.has(normalized))
|
|
265
|
+
continue;
|
|
266
|
+
seen.add(normalized);
|
|
267
|
+
items.push(item);
|
|
268
|
+
if (limit && items.length >= limit)
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
return items;
|
|
272
|
+
}
|
|
273
|
+
function formatSummaryItems(section, maxLen) {
|
|
274
|
+
const items = extractSummaryItems(section);
|
|
275
|
+
if (items.length === 0)
|
|
276
|
+
return null;
|
|
277
|
+
const cleaned = items.map((item) => `- ${item}`).join(`
|
|
278
|
+
`);
|
|
279
|
+
if (cleaned.length <= maxLen)
|
|
280
|
+
return cleaned;
|
|
281
|
+
const truncated = cleaned.slice(0, maxLen).trimEnd();
|
|
282
|
+
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
283
|
+
`), truncated.lastIndexOf(" "));
|
|
284
|
+
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
285
|
+
return `${safe.trimEnd()}…`;
|
|
286
|
+
}
|
|
287
|
+
function normalizeSummarySection(section) {
|
|
288
|
+
const items = extractSummaryItems(section);
|
|
289
|
+
if (items.length === 0) {
|
|
290
|
+
const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
|
|
291
|
+
return cleaned || null;
|
|
292
|
+
}
|
|
293
|
+
return items.map((item) => `- ${item}`).join(`
|
|
294
|
+
`);
|
|
295
|
+
}
|
|
296
|
+
function normalizeSummaryRequest(value) {
|
|
297
|
+
const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
298
|
+
return cleaned || null;
|
|
299
|
+
}
|
|
300
|
+
function stripSectionPrefix(value) {
|
|
301
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
|
|
302
|
+
}
|
|
303
|
+
function parseHeading(value) {
|
|
304
|
+
const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
|
|
305
|
+
if (boldMatch?.[1]) {
|
|
306
|
+
return boldMatch[1].trim().replace(/\s+/g, " ");
|
|
307
|
+
}
|
|
308
|
+
const plainMatch = value.match(/^(.+?):$/);
|
|
309
|
+
if (plainMatch?.[1]) {
|
|
310
|
+
return plainMatch[1].trim().replace(/\s+/g, " ");
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
function normalizeItem(value) {
|
|
315
|
+
return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
|
|
316
|
+
}
|
|
317
|
+
|
|
240
318
|
// src/config.ts
|
|
241
319
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
242
320
|
import { homedir, hostname, networkInterfaces } from "node:os";
|
|
@@ -953,86 +1031,6 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
|
|
|
953
1031
|
|
|
954
1032
|
// src/storage/sqlite.ts
|
|
955
1033
|
import { createHash as createHash2 } from "node:crypto";
|
|
956
|
-
|
|
957
|
-
// src/intelligence/summary-sections.ts
|
|
958
|
-
function extractSummaryItems(section, limit) {
|
|
959
|
-
if (!section || !section.trim())
|
|
960
|
-
return [];
|
|
961
|
-
const rawLines = section.split(`
|
|
962
|
-
`).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
963
|
-
const items = [];
|
|
964
|
-
const seen = new Set;
|
|
965
|
-
let heading = null;
|
|
966
|
-
for (const rawLine of rawLines) {
|
|
967
|
-
const line = stripSectionPrefix(rawLine);
|
|
968
|
-
if (!line)
|
|
969
|
-
continue;
|
|
970
|
-
const headingOnly = parseHeading(line);
|
|
971
|
-
if (headingOnly) {
|
|
972
|
-
heading = headingOnly;
|
|
973
|
-
continue;
|
|
974
|
-
}
|
|
975
|
-
const isBullet = /^[-*•]\s+/.test(line);
|
|
976
|
-
const stripped = line.replace(/^[-*•]\s+/, "").trim();
|
|
977
|
-
if (!stripped)
|
|
978
|
-
continue;
|
|
979
|
-
const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
|
|
980
|
-
const normalized = normalizeItem(item);
|
|
981
|
-
if (!normalized || seen.has(normalized))
|
|
982
|
-
continue;
|
|
983
|
-
seen.add(normalized);
|
|
984
|
-
items.push(item);
|
|
985
|
-
if (limit && items.length >= limit)
|
|
986
|
-
break;
|
|
987
|
-
}
|
|
988
|
-
return items;
|
|
989
|
-
}
|
|
990
|
-
function formatSummaryItems(section, maxLen) {
|
|
991
|
-
const items = extractSummaryItems(section);
|
|
992
|
-
if (items.length === 0)
|
|
993
|
-
return null;
|
|
994
|
-
const cleaned = items.map((item) => `- ${item}`).join(`
|
|
995
|
-
`);
|
|
996
|
-
if (cleaned.length <= maxLen)
|
|
997
|
-
return cleaned;
|
|
998
|
-
const truncated = cleaned.slice(0, maxLen).trimEnd();
|
|
999
|
-
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
1000
|
-
`), truncated.lastIndexOf(" "));
|
|
1001
|
-
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
1002
|
-
return `${safe.trimEnd()}…`;
|
|
1003
|
-
}
|
|
1004
|
-
function normalizeSummarySection(section) {
|
|
1005
|
-
const items = extractSummaryItems(section);
|
|
1006
|
-
if (items.length === 0) {
|
|
1007
|
-
const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
|
|
1008
|
-
return cleaned || null;
|
|
1009
|
-
}
|
|
1010
|
-
return items.map((item) => `- ${item}`).join(`
|
|
1011
|
-
`);
|
|
1012
|
-
}
|
|
1013
|
-
function normalizeSummaryRequest(value) {
|
|
1014
|
-
const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
1015
|
-
return cleaned || null;
|
|
1016
|
-
}
|
|
1017
|
-
function stripSectionPrefix(value) {
|
|
1018
|
-
return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
|
|
1019
|
-
}
|
|
1020
|
-
function parseHeading(value) {
|
|
1021
|
-
const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
|
|
1022
|
-
if (boldMatch?.[1]) {
|
|
1023
|
-
return boldMatch[1].trim().replace(/\s+/g, " ");
|
|
1024
|
-
}
|
|
1025
|
-
const plainMatch = value.match(/^(.+?):$/);
|
|
1026
|
-
if (plainMatch?.[1]) {
|
|
1027
|
-
return plainMatch[1].trim().replace(/\s+/g, " ");
|
|
1028
|
-
}
|
|
1029
|
-
return null;
|
|
1030
|
-
}
|
|
1031
|
-
function normalizeItem(value) {
|
|
1032
|
-
return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
// src/storage/sqlite.ts
|
|
1036
1034
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
1037
1035
|
function openDatabase(dbPath) {
|
|
1038
1036
|
if (IS_BUN) {
|
|
@@ -2560,7 +2558,7 @@ function buildBeacon(db, config, sessionId, metrics) {
|
|
|
2560
2558
|
sentinel_used: valueSignals.security_findings_count > 0,
|
|
2561
2559
|
risk_score: riskScore,
|
|
2562
2560
|
stacks_detected: stacks,
|
|
2563
|
-
client_version: "0.4.
|
|
2561
|
+
client_version: "0.4.19",
|
|
2564
2562
|
context_observations_injected: metrics?.contextObsInjected ?? 0,
|
|
2565
2563
|
context_total_available: metrics?.contextTotalAvailable ?? 0,
|
|
2566
2564
|
recall_attempts: metrics?.recallAttempts ?? 0,
|
|
@@ -3592,7 +3590,9 @@ async function main() {
|
|
|
3592
3590
|
if (!existing) {
|
|
3593
3591
|
const observations = db.getObservationsBySession(event.session_id);
|
|
3594
3592
|
const session = db.getSessionMetrics(event.session_id);
|
|
3595
|
-
const
|
|
3593
|
+
const retrospective = extractRetrospective(observations, event.session_id, session?.project_id ?? null, config.user_id);
|
|
3594
|
+
const assistantSections = extractAssistantSummarySections(event.last_assistant_message);
|
|
3595
|
+
const summary = mergeSessionSummary(retrospective, assistantSections, event.session_id, session?.project_id ?? null, config.user_id) ?? mergeSessionSummary(buildFallbackSessionSummary(db, event.session_id, session?.project_id ?? null, config.user_id, event.last_assistant_message), assistantSections, event.session_id, session?.project_id ?? null, config.user_id) ?? buildFallbackSessionSummary(db, event.session_id, session?.project_id ?? null, config.user_id, event.last_assistant_message);
|
|
3596
3596
|
if (summary) {
|
|
3597
3597
|
const row = db.insertSessionSummary(summary);
|
|
3598
3598
|
db.addToOutbox("summary", row.id);
|
|
@@ -3701,6 +3701,85 @@ function buildCheckpointCompleted(checkpoint) {
|
|
|
3701
3701
|
return lines.join(`
|
|
3702
3702
|
`);
|
|
3703
3703
|
}
|
|
3704
|
+
function mergeSessionSummary(base, extra, sessionId, projectId, userId) {
|
|
3705
|
+
if (!base && !extra)
|
|
3706
|
+
return null;
|
|
3707
|
+
return {
|
|
3708
|
+
session_id: sessionId,
|
|
3709
|
+
project_id: projectId,
|
|
3710
|
+
user_id: userId,
|
|
3711
|
+
request: chooseRicherSummaryValue(base?.request ?? null, extra?.request ?? null, true),
|
|
3712
|
+
investigated: chooseRicherSummaryValue(base?.investigated ?? null, extra?.investigated ?? null, false),
|
|
3713
|
+
learned: chooseRicherSummaryValue(base?.learned ?? null, extra?.learned ?? null, false),
|
|
3714
|
+
completed: chooseRicherSummaryValue(base?.completed ?? null, extra?.completed ?? null, false),
|
|
3715
|
+
next_steps: chooseRicherSummaryValue(base?.next_steps ?? null, extra?.next_steps ?? null, false)
|
|
3716
|
+
};
|
|
3717
|
+
}
|
|
3718
|
+
function chooseRicherSummaryValue(base, extra, isRequest) {
|
|
3719
|
+
const normalizedBase = isRequest ? normalizeSummaryRequest(base) : normalizeSummarySection(base);
|
|
3720
|
+
const normalizedExtra = isRequest ? normalizeSummaryRequest(extra) : normalizeSummarySection(extra);
|
|
3721
|
+
if (!normalizedBase)
|
|
3722
|
+
return normalizedExtra;
|
|
3723
|
+
if (!normalizedExtra)
|
|
3724
|
+
return normalizedBase;
|
|
3725
|
+
if (normalizedExtra.length > normalizedBase.length + 24)
|
|
3726
|
+
return normalizedExtra;
|
|
3727
|
+
if (isRequest && isGenericCheckpointLine(normalizedBase) && !isGenericCheckpointLine(normalizedExtra)) {
|
|
3728
|
+
return normalizedExtra;
|
|
3729
|
+
}
|
|
3730
|
+
return normalizedBase;
|
|
3731
|
+
}
|
|
3732
|
+
function extractAssistantSummarySections(message) {
|
|
3733
|
+
const compact = message?.replace(/\r/g, "").trim();
|
|
3734
|
+
if (!compact || compact.length < 80)
|
|
3735
|
+
return null;
|
|
3736
|
+
const sections = new Map;
|
|
3737
|
+
let current = null;
|
|
3738
|
+
for (const rawLine of compact.split(`
|
|
3739
|
+
`)) {
|
|
3740
|
+
const line = rawLine.trim();
|
|
3741
|
+
if (!line)
|
|
3742
|
+
continue;
|
|
3743
|
+
const heading = parseAssistantSectionHeading(line);
|
|
3744
|
+
if (heading) {
|
|
3745
|
+
current = heading;
|
|
3746
|
+
if (!sections.has(current))
|
|
3747
|
+
sections.set(current, []);
|
|
3748
|
+
continue;
|
|
3749
|
+
}
|
|
3750
|
+
if (!current)
|
|
3751
|
+
continue;
|
|
3752
|
+
if (current === "request" && isGenericCheckpointLine(line))
|
|
3753
|
+
continue;
|
|
3754
|
+
sections.get(current)?.push(line);
|
|
3755
|
+
}
|
|
3756
|
+
const request = normalizeSummaryRequest(sections.get("request")?.join(" ") ?? null);
|
|
3757
|
+
const investigated = normalizeSummarySection(sections.get("investigated")?.join(`
|
|
3758
|
+
`) ?? null);
|
|
3759
|
+
const learned = normalizeSummarySection(sections.get("learned")?.join(`
|
|
3760
|
+
`) ?? null);
|
|
3761
|
+
const completed = normalizeSummarySection(sections.get("completed")?.join(`
|
|
3762
|
+
`) ?? null);
|
|
3763
|
+
const next_steps = normalizeSummarySection(sections.get("next_steps")?.join(`
|
|
3764
|
+
`) ?? null);
|
|
3765
|
+
if (!request && !investigated && !learned && !completed && !next_steps)
|
|
3766
|
+
return null;
|
|
3767
|
+
return { request, investigated, learned, completed, next_steps };
|
|
3768
|
+
}
|
|
3769
|
+
function parseAssistantSectionHeading(value) {
|
|
3770
|
+
const normalized = value.toLowerCase().replace(/\*+/g, "").trim();
|
|
3771
|
+
if (/^request:/.test(normalized))
|
|
3772
|
+
return "request";
|
|
3773
|
+
if (/^investigated:/.test(normalized))
|
|
3774
|
+
return "investigated";
|
|
3775
|
+
if (/^learned:/.test(normalized))
|
|
3776
|
+
return "learned";
|
|
3777
|
+
if (/^completed:/.test(normalized))
|
|
3778
|
+
return "completed";
|
|
3779
|
+
if (/^next steps?:/.test(normalized))
|
|
3780
|
+
return "next_steps";
|
|
3781
|
+
return null;
|
|
3782
|
+
}
|
|
3704
3783
|
function createSessionDigest(db, sessionId, cwd) {
|
|
3705
3784
|
const observations = db.getObservationsBySession(sessionId);
|
|
3706
3785
|
if (observations.length < 2)
|
|
@@ -3856,9 +3935,13 @@ function extractAssistantCheckpoint(message) {
|
|
|
3856
3935
|
};
|
|
3857
3936
|
}
|
|
3858
3937
|
function pickAssistantCheckpointTitle(substantiveLines, bulletLines) {
|
|
3859
|
-
const candidates = [...bulletLines, ...substantiveLines].map((line) => line.replace(/^Completed:\s*/i, "").trim()).filter((line) => line.length > 20).filter((line) => !/^Next Steps?:/i.test(line)).filter((line) => !/^Investigated:/i.test(line)).filter((line) => !/^Learned:/i.test(line));
|
|
3938
|
+
const candidates = [...bulletLines, ...substantiveLines].map((line) => line.replace(/^Completed:\s*/i, "").trim()).filter((line) => line.length > 20).filter((line) => !isGenericCheckpointLine(line)).filter((line) => !/^Next Steps?:/i.test(line)).filter((line) => !/^Investigated:/i.test(line)).filter((line) => !/^Learned:/i.test(line));
|
|
3860
3939
|
return candidates[0] ?? null;
|
|
3861
3940
|
}
|
|
3941
|
+
function isGenericCheckpointLine(value) {
|
|
3942
|
+
const normalized = value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
3943
|
+
return normalized === "here's where things stand:" || normalized === "here's where things stand" || normalized === "where things stand:" || normalized === "where things stand" || normalized === "current status:" || normalized === "current status" || normalized === "status update:" || normalized === "status update";
|
|
3944
|
+
}
|
|
3862
3945
|
function detectUnsavedPlans(message) {
|
|
3863
3946
|
const hints = [];
|
|
3864
3947
|
const lower = message.toLowerCase();
|
package/dist/server.js
CHANGED
|
@@ -19817,7 +19817,7 @@ process.on("SIGTERM", () => {
|
|
|
19817
19817
|
});
|
|
19818
19818
|
var server = new McpServer({
|
|
19819
19819
|
name: "engrm",
|
|
19820
|
-
version: "0.4.
|
|
19820
|
+
version: "0.4.19"
|
|
19821
19821
|
});
|
|
19822
19822
|
server.tool("save_observation", "Save an observation to memory", {
|
|
19823
19823
|
type: exports_external.enum([
|