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.
@@ -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.18";
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 rows = pickContextIndexObservations(context, shownItems).map((obs) => {
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
- `${c2.dim}Handoff index:${c2.reset} use IDs when you want the deeper thread`,
3365
- ...rows
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 = context.observations.slice(0, 5).map((obs) => obs.id);
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 ")}`,
@@ -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.18",
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 summary = extractRetrospective(observations, 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);
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.18"
19820
+ version: "0.4.19"
19821
19821
  });
19822
19822
  server.tool("save_observation", "Save an observation to memory", {
19823
19823
  type: exports_external.enum([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.4.18",
3
+ "version": "0.4.19",
4
4
  "description": "Shared memory across devices, sessions, and coding agents",
5
5
  "mcpName": "io.github.dr12hes/engrm",
6
6
  "type": "module",