engrm 0.4.5 → 0.4.7

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/server.js CHANGED
@@ -14827,6 +14827,80 @@ function findDuplicate(newTitle, candidates) {
14827
14827
  return bestMatch;
14828
14828
  }
14829
14829
 
14830
+ // src/capture/facts.ts
14831
+ var FACT_ELIGIBLE_TYPES = new Set([
14832
+ "bugfix",
14833
+ "decision",
14834
+ "discovery",
14835
+ "pattern",
14836
+ "feature",
14837
+ "refactor",
14838
+ "change"
14839
+ ]);
14840
+ function buildStructuredFacts(input) {
14841
+ const seedFacts = dedupeFacts(input.facts ?? []);
14842
+ if (!FACT_ELIGIBLE_TYPES.has(input.type)) {
14843
+ return seedFacts;
14844
+ }
14845
+ const derived = [...seedFacts];
14846
+ if (seedFacts.length === 0 && looksMeaningful(input.title)) {
14847
+ derived.push(input.title.trim());
14848
+ }
14849
+ for (const sentence of extractNarrativeFacts(input.narrative)) {
14850
+ derived.push(sentence);
14851
+ }
14852
+ const fileFact = buildFilesFact(input.filesModified);
14853
+ if (fileFact) {
14854
+ derived.push(fileFact);
14855
+ }
14856
+ return dedupeFacts(derived).slice(0, 4);
14857
+ }
14858
+ function extractNarrativeFacts(narrative) {
14859
+ if (!narrative)
14860
+ return [];
14861
+ const cleaned = narrative.replace(/\s+/g, " ").trim();
14862
+ if (cleaned.length < 24)
14863
+ return [];
14864
+ const parts = cleaned.split(/(?<=[.!?;])\s+/).map((part) => part.trim().replace(/[.!?;]+$/, "")).filter(Boolean).filter(looksMeaningful);
14865
+ return parts.slice(0, 2);
14866
+ }
14867
+ function buildFilesFact(filesModified) {
14868
+ if (!filesModified || filesModified.length === 0)
14869
+ return null;
14870
+ const cleaned = filesModified.map((file2) => file2.trim()).filter(Boolean).slice(0, 3);
14871
+ if (cleaned.length === 0)
14872
+ return null;
14873
+ if (cleaned.length === 1) {
14874
+ return `Touched ${cleaned[0]}`;
14875
+ }
14876
+ return `Touched ${cleaned.join(", ")}`;
14877
+ }
14878
+ function dedupeFacts(facts) {
14879
+ const seen = new Set;
14880
+ const result = [];
14881
+ for (const fact of facts) {
14882
+ const cleaned = fact.trim().replace(/\s+/g, " ");
14883
+ if (!looksMeaningful(cleaned))
14884
+ continue;
14885
+ const key = cleaned.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\s+/g, " ").trim();
14886
+ if (!key || seen.has(key))
14887
+ continue;
14888
+ seen.add(key);
14889
+ result.push(cleaned);
14890
+ }
14891
+ return result;
14892
+ }
14893
+ function looksMeaningful(value) {
14894
+ const cleaned = value.trim();
14895
+ if (cleaned.length < 12)
14896
+ return false;
14897
+ if (/^[A-Za-z0-9_.\-\/]+\.[A-Za-z0-9]+$/.test(cleaned))
14898
+ return false;
14899
+ if (/^(updated|modified|edited|changed|touched)\s+[A-Za-z0-9_.\-\/]+$/i.test(cleaned))
14900
+ return false;
14901
+ return true;
14902
+ }
14903
+
14830
14904
  // src/storage/projects.ts
14831
14905
  import { execSync } from "node:child_process";
14832
14906
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
@@ -15218,10 +15292,17 @@ async function saveObservation(db, config2, input) {
15218
15292
  const customPatterns = config2.scrubbing.enabled ? config2.scrubbing.custom_patterns : [];
15219
15293
  const title = config2.scrubbing.enabled ? scrubSecrets(input.title, customPatterns) : input.title;
15220
15294
  const narrative = input.narrative ? config2.scrubbing.enabled ? scrubSecrets(input.narrative, customPatterns) : input.narrative : null;
15221
- const factsJson = input.facts ? config2.scrubbing.enabled ? scrubSecrets(JSON.stringify(input.facts), customPatterns) : JSON.stringify(input.facts) : null;
15222
15295
  const conceptsJson = input.concepts ? JSON.stringify(input.concepts) : null;
15223
15296
  const filesRead = input.files_read ? input.files_read.map((f) => toRelativePath(f, cwd)) : null;
15224
15297
  const filesModified = input.files_modified ? input.files_modified.map((f) => toRelativePath(f, cwd)) : null;
15298
+ const structuredFacts = buildStructuredFacts({
15299
+ type: input.type,
15300
+ title: input.title,
15301
+ narrative: input.narrative,
15302
+ facts: input.facts,
15303
+ filesModified
15304
+ });
15305
+ const factsJson = structuredFacts.length > 0 ? config2.scrubbing.enabled ? scrubSecrets(JSON.stringify(structuredFacts), customPatterns) : JSON.stringify(structuredFacts) : null;
15225
15306
  const filesReadJson = filesRead ? JSON.stringify(filesRead) : null;
15226
15307
  const filesModifiedJson = filesModified ? JSON.stringify(filesModified) : null;
15227
15308
  let sensitivity = input.sensitivity ?? config2.scrubbing.default_sensitivity;
@@ -15326,6 +15407,69 @@ function toRelativePath(filePath, projectRoot) {
15326
15407
  return rel;
15327
15408
  }
15328
15409
 
15410
+ // src/intelligence/observation-priority.ts
15411
+ var RECENCY_WINDOW_SECONDS = 30 * 86400;
15412
+ function computeBlendedScore(quality, createdAtEpoch, nowEpoch) {
15413
+ const age = nowEpoch - createdAtEpoch;
15414
+ const recencyNorm = Math.max(0, Math.min(1, 1 - age / RECENCY_WINDOW_SECONDS));
15415
+ return quality * 0.6 + recencyNorm * 0.4;
15416
+ }
15417
+ function observationTypeBoost(type) {
15418
+ switch (type) {
15419
+ case "decision":
15420
+ return 0.2;
15421
+ case "pattern":
15422
+ return 0.18;
15423
+ case "bugfix":
15424
+ return 0.14;
15425
+ case "feature":
15426
+ return 0.12;
15427
+ case "discovery":
15428
+ return 0.1;
15429
+ case "refactor":
15430
+ return 0.05;
15431
+ case "digest":
15432
+ return 0.03;
15433
+ case "change":
15434
+ return 0;
15435
+ default:
15436
+ return 0;
15437
+ }
15438
+ }
15439
+ function computeObservationPriority(obs, nowEpoch) {
15440
+ return computeBlendedScore(obs.quality, obs.created_at_epoch, nowEpoch) + observationTypeBoost(obs.type);
15441
+ }
15442
+ function textIncludesQuery(text, query) {
15443
+ return Boolean(text && query && text.toLowerCase().includes(query));
15444
+ }
15445
+ function tokenOverlapBoost(text, queryTokens) {
15446
+ if (!text || queryTokens.length === 0)
15447
+ return 0;
15448
+ const lower = text.toLowerCase();
15449
+ const matched = queryTokens.filter((token) => lower.includes(token)).length;
15450
+ if (matched === 0)
15451
+ return 0;
15452
+ return Math.min(0.12, matched * 0.03);
15453
+ }
15454
+ function computeSearchRank(obs, baseScore, query, nowEpoch) {
15455
+ const normalizedQuery = query.trim().toLowerCase();
15456
+ const queryTokens = normalizedQuery.split(/\s+/).map((token) => token.trim()).filter((token) => token.length >= 3);
15457
+ let matchBoost = 0;
15458
+ if (textIncludesQuery(obs.title, normalizedQuery))
15459
+ matchBoost += 0.2;
15460
+ if (textIncludesQuery(obs.facts, normalizedQuery))
15461
+ matchBoost += 0.15;
15462
+ if (textIncludesQuery(obs.narrative, normalizedQuery))
15463
+ matchBoost += 0.08;
15464
+ matchBoost += tokenOverlapBoost(obs.title, queryTokens);
15465
+ matchBoost += tokenOverlapBoost(obs.facts, queryTokens);
15466
+ matchBoost += tokenOverlapBoost(obs.narrative, queryTokens) * 0.6;
15467
+ const lifecycleWeight = obs.lifecycle === "aging" ? 0.7 : 1;
15468
+ const retrievalScore = baseScore * 20 * lifecycleWeight;
15469
+ const priorityBoost = computeObservationPriority(obs, nowEpoch) * 0.12;
15470
+ return retrievalScore + priorityBoost + Math.min(0.35, matchBoost);
15471
+ }
15472
+
15329
15473
  // src/tools/search.ts
15330
15474
  async function searchObservations(db, input) {
15331
15475
  const query = input.query.trim();
@@ -15370,9 +15514,9 @@ async function searchObservations(db, input) {
15370
15514
  }
15371
15515
  }
15372
15516
  }
15517
+ const nowEpoch = Math.floor(Date.now() / 1000);
15373
15518
  const entries = active.map((obs) => {
15374
15519
  const baseScore = scoreMap.get(obs.id) ?? 0;
15375
- const lifecycleWeight = obs.lifecycle === "aging" ? 0.7 : 1;
15376
15520
  return {
15377
15521
  id: obs.id,
15378
15522
  type: obs.type,
@@ -15384,7 +15528,7 @@ async function searchObservations(db, input) {
15384
15528
  quality: obs.quality,
15385
15529
  lifecycle: obs.lifecycle,
15386
15530
  created_at: obs.created_at,
15387
- rank: baseScore * lifecycleWeight,
15531
+ rank: computeSearchRank(obs, baseScore, query, nowEpoch),
15388
15532
  ...!projectScoped ? { project_name: projectNameCache.get(obs.project_id) } : {}
15389
15533
  };
15390
15534
  });
@@ -15575,16 +15719,136 @@ function getOutboxStats(db) {
15575
15719
  return stats;
15576
15720
  }
15577
15721
 
15722
+ // src/intelligence/value-signals.ts
15723
+ var LESSON_TYPES = new Set(["bugfix", "decision", "pattern"]);
15724
+ function computeSessionValueSignals(observations, securityFindings = []) {
15725
+ const decisionsCount = observations.filter((o) => o.type === "decision").length;
15726
+ const lessonsCount = observations.filter((o) => LESSON_TYPES.has(o.type)).length;
15727
+ const discoveriesCount = observations.filter((o) => o.type === "discovery").length;
15728
+ const featuresCount = observations.filter((o) => o.type === "feature").length;
15729
+ const refactorsCount = observations.filter((o) => o.type === "refactor").length;
15730
+ const repeatedPatternsCount = observations.filter((o) => o.type === "pattern").length;
15731
+ const hasRequestSignal = observations.some((o) => ["feature", "decision", "change", "bugfix", "discovery"].includes(o.type));
15732
+ const hasCompletionSignal = observations.some((o) => ["feature", "change", "refactor", "bugfix"].includes(o.type));
15733
+ return {
15734
+ decisions_count: decisionsCount,
15735
+ lessons_count: lessonsCount,
15736
+ discoveries_count: discoveriesCount,
15737
+ features_count: featuresCount,
15738
+ refactors_count: refactorsCount,
15739
+ repeated_patterns_count: repeatedPatternsCount,
15740
+ security_findings_count: securityFindings.length,
15741
+ critical_security_findings_count: securityFindings.filter((f) => f.severity === "critical").length,
15742
+ delivery_review_ready: hasRequestSignal && hasCompletionSignal,
15743
+ vibe_guardian_active: securityFindings.length > 0
15744
+ };
15745
+ }
15746
+
15747
+ // src/intelligence/session-insights.ts
15748
+ function computeSessionInsights(summaries, observations) {
15749
+ const orderedSummaries = [...summaries].sort((a, b) => (b.created_at_epoch ?? 0) - (a.created_at_epoch ?? 0));
15750
+ const summaryCount = orderedSummaries.length;
15751
+ const summariesWithLearned = orderedSummaries.filter((s) => hasContent(s.learned)).length;
15752
+ const summariesWithCompleted = orderedSummaries.filter((s) => hasContent(s.completed)).length;
15753
+ const summariesWithNextSteps = orderedSummaries.filter((s) => hasContent(s.next_steps)).length;
15754
+ const totalSummarySectionsPresent = orderedSummaries.reduce((total, summary) => total + countPresentSections(summary), 0);
15755
+ const recentRequests = dedupeLines(orderedSummaries.map((summary) => summary.request?.trim() ?? "").filter(Boolean)).slice(0, 3);
15756
+ const recentLessons = dedupeLines([
15757
+ ...orderedSummaries.flatMap((summary) => extractSectionItems(summary.learned)),
15758
+ ...extractObservationTitles(observations, ["decision", "pattern", "bugfix"])
15759
+ ]).slice(0, 4);
15760
+ const recentCompleted = dedupeLines([
15761
+ ...orderedSummaries.flatMap((summary) => extractSectionItems(summary.completed)),
15762
+ ...extractObservationTitles(observations, ["feature", "refactor", "change"])
15763
+ ]).slice(0, 4);
15764
+ const nextSteps = dedupeLines([
15765
+ ...orderedSummaries.flatMap((summary) => extractSectionItems(summary.next_steps)),
15766
+ ...extractObservationTitles(observations, ["decision"]).map((title) => `Follow through: ${title}`)
15767
+ ]).slice(0, 4);
15768
+ return {
15769
+ summary_count: summaryCount,
15770
+ summaries_with_learned: summariesWithLearned,
15771
+ summaries_with_completed: summariesWithCompleted,
15772
+ summaries_with_next_steps: summariesWithNextSteps,
15773
+ total_summary_sections_present: totalSummarySectionsPresent,
15774
+ recent_requests: recentRequests,
15775
+ recent_lessons: recentLessons,
15776
+ recent_completed: recentCompleted,
15777
+ next_steps: nextSteps
15778
+ };
15779
+ }
15780
+ function countPresentSections(summary) {
15781
+ return [
15782
+ summary.request,
15783
+ summary.investigated,
15784
+ summary.learned,
15785
+ summary.completed,
15786
+ summary.next_steps
15787
+ ].filter(hasContent).length;
15788
+ }
15789
+ function extractSectionItems(section) {
15790
+ if (!hasContent(section))
15791
+ return [];
15792
+ return section.split(`
15793
+ `).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean);
15794
+ }
15795
+ function extractObservationTitles(observations, types) {
15796
+ const typeSet = new Set(types);
15797
+ return observations.filter((obs) => typeSet.has(obs.type)).sort((a, b) => b.created_at_epoch - a.created_at_epoch).map((obs) => obs.title.trim()).filter(Boolean);
15798
+ }
15799
+ function dedupeLines(lines) {
15800
+ const seen = new Set;
15801
+ const result = [];
15802
+ for (const line of lines) {
15803
+ const cleaned = line.replace(/\s+/g, " ").trim();
15804
+ if (!cleaned)
15805
+ continue;
15806
+ const key = cleaned.toLowerCase();
15807
+ if (seen.has(key))
15808
+ continue;
15809
+ seen.add(key);
15810
+ result.push(cleaned);
15811
+ }
15812
+ return result;
15813
+ }
15814
+ function hasContent(value) {
15815
+ return Boolean(value && value.trim());
15816
+ }
15817
+
15578
15818
  // src/tools/stats.ts
15579
15819
  function getMemoryStats(db) {
15580
15820
  const activeObservations = db.getActiveObservationCount();
15581
15821
  const messages = db.db.query(`SELECT COUNT(*) as count FROM observations
15582
15822
  WHERE type = 'message' AND lifecycle IN ('active', 'aging', 'pinned')`).get()?.count ?? 0;
15583
15823
  const sessionSummaries = db.db.query("SELECT COUNT(*) as count FROM session_summaries").get()?.count ?? 0;
15824
+ const observations = db.db.query(`SELECT * FROM observations
15825
+ WHERE lifecycle IN ('active', 'aging', 'pinned') AND superseded_by IS NULL`).all();
15826
+ const securityFindings = db.db.query("SELECT * FROM security_findings ORDER BY created_at_epoch DESC LIMIT 500").all();
15827
+ const summaries = db.db.query("SELECT * FROM session_summaries ORDER BY created_at_epoch DESC LIMIT 50").all();
15828
+ const signals = computeSessionValueSignals(observations, securityFindings);
15829
+ const insights = computeSessionInsights(summaries, observations);
15584
15830
  return {
15585
15831
  active_observations: activeObservations,
15586
15832
  messages,
15587
15833
  session_summaries: sessionSummaries,
15834
+ decisions: signals.decisions_count,
15835
+ lessons: signals.lessons_count,
15836
+ discoveries: signals.discoveries_count,
15837
+ features: signals.features_count,
15838
+ refactors: signals.refactors_count,
15839
+ repeated_patterns: signals.repeated_patterns_count,
15840
+ security_findings: signals.security_findings_count,
15841
+ critical_security_findings: signals.critical_security_findings_count,
15842
+ delivery_review_ready: signals.delivery_review_ready,
15843
+ vibe_guardian_active: signals.vibe_guardian_active,
15844
+ summaries_with_learned: insights.summaries_with_learned,
15845
+ summaries_with_completed: insights.summaries_with_completed,
15846
+ summaries_with_next_steps: insights.summaries_with_next_steps,
15847
+ total_summary_sections_present: insights.total_summary_sections_present,
15848
+ recent_requests: insights.recent_requests,
15849
+ recent_lessons: insights.recent_lessons,
15850
+ recent_completed: insights.recent_completed,
15851
+ next_steps: insights.next_steps,
15588
15852
  installed_packs: db.getInstalledPacks(),
15589
15853
  outbox: getOutboxStats(db)
15590
15854
  };
@@ -15764,12 +16028,6 @@ function findStaleDecisionsGlobal(db, options) {
15764
16028
  }
15765
16029
 
15766
16030
  // src/context/inject.ts
15767
- var RECENCY_WINDOW_SECONDS = 30 * 86400;
15768
- function computeBlendedScore(quality, createdAtEpoch, nowEpoch) {
15769
- const age = nowEpoch - createdAtEpoch;
15770
- const recencyNorm = Math.max(0, Math.min(1, 1 - age / RECENCY_WINDOW_SECONDS));
15771
- return quality * 0.6 + recencyNorm * 0.4;
15772
- }
15773
16031
  function estimateTokens(text) {
15774
16032
  if (!text)
15775
16033
  return 0;
@@ -15862,10 +16120,8 @@ function buildSessionContext(db, cwd, options = {}) {
15862
16120
  }
15863
16121
  const nowEpoch = Math.floor(Date.now() / 1000);
15864
16122
  const sorted = [...deduped].sort((a, b) => {
15865
- const boostA = a.type === "digest" ? 0.15 : 0;
15866
- const boostB = b.type === "digest" ? 0.15 : 0;
15867
- const scoreA = computeBlendedScore(a.quality, a.created_at_epoch, nowEpoch) + boostA;
15868
- const scoreB = computeBlendedScore(b.quality, b.created_at_epoch, nowEpoch) + boostB;
16123
+ const scoreA = computeObservationPriority(a, nowEpoch);
16124
+ const scoreB = computeObservationPriority(b, nowEpoch);
15869
16125
  return scoreB - scoreA;
15870
16126
  });
15871
16127
  const projectName = project?.name ?? detected.name;
@@ -16504,7 +16760,7 @@ function buildVectorDocument(obs, config2, project) {
16504
16760
  }
16505
16761
  };
16506
16762
  }
16507
- function buildSummaryVectorDocument(summary, config2, project) {
16763
+ function buildSummaryVectorDocument(summary, config2, project, observations = []) {
16508
16764
  const parts = [];
16509
16765
  if (summary.request)
16510
16766
  parts.push(`Request: ${summary.request}`);
@@ -16516,6 +16772,7 @@ function buildSummaryVectorDocument(summary, config2, project) {
16516
16772
  parts.push(`Completed: ${summary.completed}`);
16517
16773
  if (summary.next_steps)
16518
16774
  parts.push(`Next Steps: ${summary.next_steps}`);
16775
+ const valueSignals = computeSessionValueSignals(observations, []);
16519
16776
  return {
16520
16777
  site_id: config2.site_id,
16521
16778
  namespace: config2.namespace,
@@ -16534,6 +16791,19 @@ function buildSummaryVectorDocument(summary, config2, project) {
16534
16791
  learned: summary.learned,
16535
16792
  completed: summary.completed,
16536
16793
  next_steps: summary.next_steps,
16794
+ summary_sections_present: countPresentSections2(summary),
16795
+ investigated_items: extractSectionItems2(summary.investigated),
16796
+ learned_items: extractSectionItems2(summary.learned),
16797
+ completed_items: extractSectionItems2(summary.completed),
16798
+ next_step_items: extractSectionItems2(summary.next_steps),
16799
+ decisions_count: valueSignals.decisions_count,
16800
+ lessons_count: valueSignals.lessons_count,
16801
+ discoveries_count: valueSignals.discoveries_count,
16802
+ features_count: valueSignals.features_count,
16803
+ refactors_count: valueSignals.refactors_count,
16804
+ repeated_patterns_count: valueSignals.repeated_patterns_count,
16805
+ delivery_review_ready: valueSignals.delivery_review_ready,
16806
+ vibe_guardian_active: valueSignals.vibe_guardian_active,
16537
16807
  created_at_epoch: summary.created_at_epoch,
16538
16808
  local_id: summary.id
16539
16809
  }
@@ -16563,10 +16833,11 @@ async function pushOutbox(db, client, config2, batchSize = 50) {
16563
16833
  continue;
16564
16834
  }
16565
16835
  markSyncing(db, entry.id);
16836
+ const summaryObservations = db.getObservationsBySession(summary.session_id);
16566
16837
  const doc3 = buildSummaryVectorDocument(summary, config2, {
16567
16838
  canonical_id: project2.canonical_id,
16568
16839
  name: project2.name
16569
- });
16840
+ }, summaryObservations);
16570
16841
  batch.push({ entryId: entry.id, doc: doc3 });
16571
16842
  continue;
16572
16843
  }
@@ -16625,6 +16896,21 @@ async function pushOutbox(db, client, config2, batchSize = 50) {
16625
16896
  }
16626
16897
  return { pushed, failed, skipped };
16627
16898
  }
16899
+ function countPresentSections2(summary) {
16900
+ return [
16901
+ summary.request,
16902
+ summary.investigated,
16903
+ summary.learned,
16904
+ summary.completed,
16905
+ summary.next_steps
16906
+ ].filter((value) => Boolean(value && value.trim())).length;
16907
+ }
16908
+ function extractSectionItems2(section) {
16909
+ if (!section)
16910
+ return [];
16911
+ return section.split(`
16912
+ `).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).slice(0, 4);
16913
+ }
16628
16914
 
16629
16915
  // src/sync/pull.ts
16630
16916
  var PULL_CURSOR_KEY = "pull_cursor";
@@ -16947,7 +17233,7 @@ process.on("SIGTERM", () => {
16947
17233
  });
16948
17234
  var server = new McpServer({
16949
17235
  name: "engrm",
16950
- version: "0.1.0"
17236
+ version: "0.4.6"
16951
17237
  });
16952
17238
  server.tool("save_observation", "Save an observation to memory", {
16953
17239
  type: exports_external.enum([
@@ -17031,13 +17317,22 @@ server.tool("search", "Search memory for observations", {
17031
17317
  ]
17032
17318
  };
17033
17319
  }
17034
- const header = "| ID | Type | Q | Title | Created |";
17035
- const separator = "|---|---|---|---|---|";
17320
+ const includeProjectColumn = result.observations.some((obs) => obs.project_name);
17321
+ const header = includeProjectColumn ? "| ID | Type | Q | Title | Project | Created |" : "| ID | Type | Q | Title | Created |";
17322
+ const separator = includeProjectColumn ? "|---|---|---|---|---|---|" : "|---|---|---|---|---|";
17036
17323
  const rows = result.observations.map((obs) => {
17037
17324
  const qualityDots = qualityIndicator(obs.quality);
17038
17325
  const date5 = obs.created_at.split("T")[0];
17326
+ if (includeProjectColumn) {
17327
+ return `| ${obs.id} | ${obs.type} | ${qualityDots} | ${obs.title} | ${obs.project_name ?? "-"} | ${date5} |`;
17328
+ }
17039
17329
  return `| ${obs.id} | ${obs.type} | ${qualityDots} | ${obs.title} | ${date5} |`;
17040
17330
  });
17331
+ const previews = result.observations.slice(0, Math.min(3, result.observations.length)).map((obs) => {
17332
+ const preview = formatFactPreview(obs.facts, obs.narrative);
17333
+ const projectSuffix = obs.project_name ? ` [${obs.project_name}]` : "";
17334
+ return preview ? `- #${obs.id} [${obs.type}] ${obs.title}${projectSuffix}: ${preview}` : `- #${obs.id} [${obs.type}] ${obs.title}${projectSuffix}`;
17335
+ });
17041
17336
  const projectLine = result.project ? `Project: ${result.project}
17042
17337
  ` : "";
17043
17338
  return {
@@ -17049,6 +17344,10 @@ server.tool("search", "Search memory for observations", {
17049
17344
  ${header}
17050
17345
  ${separator}
17051
17346
  ${rows.join(`
17347
+ `)}
17348
+
17349
+ Top context:
17350
+ ${previews.join(`
17052
17351
  `)}`
17053
17352
  }
17054
17353
  ]
@@ -17269,6 +17568,14 @@ ${rows.join(`
17269
17568
  server.tool("memory_stats", "Show high-level Engrm capture and sync statistics", {}, async () => {
17270
17569
  const stats = getMemoryStats(db);
17271
17570
  const packs = stats.installed_packs.length > 0 ? stats.installed_packs.join(", ") : "(none)";
17571
+ const recentRequests = stats.recent_requests.length > 0 ? stats.recent_requests.map((item) => `- ${item}`).join(`
17572
+ `) : "- (none)";
17573
+ const recentLessons = stats.recent_lessons.length > 0 ? stats.recent_lessons.map((item) => `- ${item}`).join(`
17574
+ `) : "- (none)";
17575
+ const recentCompleted = stats.recent_completed.length > 0 ? stats.recent_completed.map((item) => `- ${item}`).join(`
17576
+ `) : "- (none)";
17577
+ const nextSteps = stats.next_steps.length > 0 ? stats.next_steps.map((item) => `- ${item}`).join(`
17578
+ `) : "- (none)";
17272
17579
  return {
17273
17580
  content: [
17274
17581
  {
@@ -17276,8 +17583,21 @@ server.tool("memory_stats", "Show high-level Engrm capture and sync statistics",
17276
17583
  text: `Active observations: ${stats.active_observations}
17277
17584
  ` + `Messages: ${stats.messages}
17278
17585
  ` + `Session summaries: ${stats.session_summaries}
17586
+ ` + `Summary coverage: learned ${stats.summaries_with_learned}, completed ${stats.summaries_with_completed}, next steps ${stats.summaries_with_next_steps}
17279
17587
  ` + `Installed packs: ${packs}
17280
- ` + `Outbox: pending ${stats.outbox.pending ?? 0}, failed ${stats.outbox.failed ?? 0}, synced ${stats.outbox.synced ?? 0}`
17588
+ ` + `Outbox: pending ${stats.outbox.pending ?? 0}, failed ${stats.outbox.failed ?? 0}, synced ${stats.outbox.synced ?? 0}
17589
+
17590
+ ` + `Recent requests:
17591
+ ${recentRequests}
17592
+
17593
+ ` + `Recent lessons:
17594
+ ${recentLessons}
17595
+
17596
+ ` + `Recent completed:
17597
+ ${recentCompleted}
17598
+
17599
+ ` + `Next steps:
17600
+ ${nextSteps}`
17281
17601
  }
17282
17602
  ]
17283
17603
  };
@@ -17381,6 +17701,28 @@ function qualityIndicator(quality) {
17381
17701
  const filled = Math.round(quality * 5);
17382
17702
  return "●".repeat(filled) + "○".repeat(5 - filled);
17383
17703
  }
17704
+ function formatFactPreview(factsRaw, narrative) {
17705
+ if (factsRaw) {
17706
+ try {
17707
+ const parsed = JSON.parse(factsRaw);
17708
+ if (Array.isArray(parsed)) {
17709
+ const facts = parsed.filter((item) => typeof item === "string" && item.trim().length > 0).slice(0, 2);
17710
+ if (facts.length > 0) {
17711
+ return facts.join("; ");
17712
+ }
17713
+ }
17714
+ } catch {
17715
+ const trimmedFacts = factsRaw.trim();
17716
+ if (trimmedFacts.length > 0) {
17717
+ return trimmedFacts.length > 160 ? `${trimmedFacts.slice(0, 157)}...` : trimmedFacts;
17718
+ }
17719
+ }
17720
+ }
17721
+ if (!narrative)
17722
+ return null;
17723
+ const trimmed = narrative.trim().replace(/\s+/g, " ");
17724
+ return trimmed.length > 160 ? `${trimmed.slice(0, 157)}...` : trimmed;
17725
+ }
17384
17726
  async function main() {
17385
17727
  runDueJobs(db);
17386
17728
  if (db.vecAvailable) {
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.4.5",
4
- "description": "Cross-device, team-shared memory layer for AI coding agents",
3
+ "version": "0.4.7",
4
+ "description": "Shared memory across devices, sessions, and coding agents",
5
+ "mcpName": "io.github.dr12hes/engrm",
5
6
  "type": "module",
6
7
  "main": "dist/server.js",
7
8
  "bin": {