engrm 0.4.19 → 0.4.22

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
@@ -14120,6 +14120,16 @@ var MIGRATIONS = [
14120
14120
  ON tool_events(created_at_epoch DESC, id DESC);
14121
14121
  `
14122
14122
  },
14123
+ {
14124
+ version: 12,
14125
+ description: "Add synced handoff metadata to session summaries",
14126
+ sql: `
14127
+ ALTER TABLE session_summaries ADD COLUMN capture_state TEXT;
14128
+ ALTER TABLE session_summaries ADD COLUMN recent_tool_names TEXT;
14129
+ ALTER TABLE session_summaries ADD COLUMN hot_files TEXT;
14130
+ ALTER TABLE session_summaries ADD COLUMN recent_outcomes TEXT;
14131
+ `
14132
+ },
14123
14133
  {
14124
14134
  version: 11,
14125
14135
  description: "Add observation provenance from tool and prompt chronology",
@@ -14186,6 +14196,9 @@ function inferLegacySchemaVersion(db) {
14186
14196
  version2 = Math.max(version2, 10);
14187
14197
  if (columnExists(db, "observations", "source_tool"))
14188
14198
  version2 = Math.max(version2, 11);
14199
+ if (columnExists(db, "session_summaries", "capture_state") && columnExists(db, "session_summaries", "recent_tool_names") && columnExists(db, "session_summaries", "hot_files") && columnExists(db, "session_summaries", "recent_outcomes")) {
14200
+ version2 = Math.max(version2, 12);
14201
+ }
14189
14202
  return version2;
14190
14203
  }
14191
14204
  function runMigrations(db) {
@@ -14264,6 +14277,23 @@ function ensureObservationTypes(db) {
14264
14277
  }
14265
14278
  }
14266
14279
  }
14280
+ function ensureSessionSummaryColumns(db) {
14281
+ const required2 = [
14282
+ "capture_state",
14283
+ "recent_tool_names",
14284
+ "hot_files",
14285
+ "recent_outcomes"
14286
+ ];
14287
+ for (const column of required2) {
14288
+ if (columnExists(db, "session_summaries", column))
14289
+ continue;
14290
+ db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
14291
+ }
14292
+ const current = getSchemaVersion(db);
14293
+ if (current < 12) {
14294
+ db.exec("PRAGMA user_version = 12");
14295
+ }
14296
+ }
14267
14297
  function getSchemaVersion(db) {
14268
14298
  const result = db.query("PRAGMA user_version").get();
14269
14299
  return result.user_version;
@@ -14422,6 +14452,7 @@ class MemDatabase {
14422
14452
  this.vecAvailable = this.loadVecExtension();
14423
14453
  runMigrations(this.db);
14424
14454
  ensureObservationTypes(this.db);
14455
+ ensureSessionSummaryColumns(this.db);
14425
14456
  }
14426
14457
  loadVecExtension() {
14427
14458
  try {
@@ -14647,6 +14678,10 @@ class MemDatabase {
14647
14678
  p.name AS project_name,
14648
14679
  ss.request AS request,
14649
14680
  ss.completed AS completed,
14681
+ ss.capture_state AS capture_state,
14682
+ ss.recent_tool_names AS recent_tool_names,
14683
+ ss.hot_files AS hot_files,
14684
+ ss.recent_outcomes AS recent_outcomes,
14650
14685
  (SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
14651
14686
  (SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
14652
14687
  FROM sessions s
@@ -14661,6 +14696,10 @@ class MemDatabase {
14661
14696
  p.name AS project_name,
14662
14697
  ss.request AS request,
14663
14698
  ss.completed AS completed,
14699
+ ss.capture_state AS capture_state,
14700
+ ss.recent_tool_names AS recent_tool_names,
14701
+ ss.hot_files AS hot_files,
14702
+ ss.recent_outcomes AS recent_outcomes,
14664
14703
  (SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
14665
14704
  (SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
14666
14705
  FROM sessions s
@@ -14833,8 +14872,11 @@ class MemDatabase {
14833
14872
  completed: normalizeSummarySection(summary.completed),
14834
14873
  next_steps: normalizeSummarySection(summary.next_steps)
14835
14874
  };
14836
- const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
14837
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
14875
+ const result = this.db.query(`INSERT INTO session_summaries (
14876
+ session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
14877
+ capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
14878
+ )
14879
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, summary.capture_state ?? null, summary.recent_tool_names ?? null, summary.hot_files ?? null, summary.recent_outcomes ?? null, now);
14838
14880
  const id = Number(result.lastInsertRowid);
14839
14881
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
14840
14882
  }
@@ -14849,7 +14891,11 @@ class MemDatabase {
14849
14891
  investigated: normalizeSummarySection(summary.investigated ?? existing.investigated),
14850
14892
  learned: normalizeSummarySection(summary.learned ?? existing.learned),
14851
14893
  completed: normalizeSummarySection(summary.completed ?? existing.completed),
14852
- next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps)
14894
+ next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
14895
+ capture_state: summary.capture_state ?? existing.capture_state,
14896
+ recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
14897
+ hot_files: summary.hot_files ?? existing.hot_files,
14898
+ recent_outcomes: summary.recent_outcomes ?? existing.recent_outcomes
14853
14899
  };
14854
14900
  this.db.query(`UPDATE session_summaries
14855
14901
  SET project_id = ?,
@@ -14859,8 +14905,12 @@ class MemDatabase {
14859
14905
  learned = ?,
14860
14906
  completed = ?,
14861
14907
  next_steps = ?,
14908
+ capture_state = ?,
14909
+ recent_tool_names = ?,
14910
+ hot_files = ?,
14911
+ recent_outcomes = ?,
14862
14912
  created_at_epoch = ?
14863
- WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now, summary.session_id);
14913
+ WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, normalized.capture_state, normalized.recent_tool_names, normalized.hot_files, normalized.recent_outcomes, now, summary.session_id);
14864
14914
  return this.getSessionSummary(summary.session_id);
14865
14915
  }
14866
14916
  getSessionSummary(sessionId) {
@@ -16460,6 +16510,16 @@ function findStaleDecisionsGlobal(db, options) {
16460
16510
  function tokenizeProjectHint(text) {
16461
16511
  return Array.from(new Set((text.toLowerCase().match(/[a-z0-9_+-]{4,}/g) ?? []).filter(Boolean)));
16462
16512
  }
16513
+ function parseSummaryJsonList(value) {
16514
+ if (!value)
16515
+ return [];
16516
+ try {
16517
+ const parsed = JSON.parse(value);
16518
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
16519
+ } catch {
16520
+ return [];
16521
+ }
16522
+ }
16463
16523
  function isObservationRelatedToProject(obs, detected) {
16464
16524
  const hints = new Set([
16465
16525
  ...tokenizeProjectHint(detected.name),
@@ -16591,7 +16651,7 @@ function buildSessionContext(db, cwd, options = {}) {
16591
16651
  const recentToolEvents2 = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
16592
16652
  const recentSessions2 = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
16593
16653
  const projectTypeCounts2 = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
16594
- const recentOutcomes2 = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId);
16654
+ const recentOutcomes2 = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId, recentSessions2);
16595
16655
  return {
16596
16656
  project_name: projectName,
16597
16657
  canonical_id: canonicalId,
@@ -16629,7 +16689,7 @@ function buildSessionContext(db, cwd, options = {}) {
16629
16689
  const recentToolEvents = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
16630
16690
  const recentSessions = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
16631
16691
  const projectTypeCounts = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
16632
- const recentOutcomes = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId);
16692
+ const recentOutcomes = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId, recentSessions);
16633
16693
  let securityFindings = [];
16634
16694
  if (!isNewProject) {
16635
16695
  try {
@@ -16972,7 +17032,7 @@ function getProjectTypeCounts(db, projectId, userId) {
16972
17032
  }
16973
17033
  return counts;
16974
17034
  }
16975
- function getRecentOutcomes(db, projectId, userId) {
17035
+ function getRecentOutcomes(db, projectId, userId, recentSessions) {
16976
17036
  const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
16977
17037
  const visibilityParams = userId ? [userId] : [];
16978
17038
  const summaries = db.db.query(`SELECT * FROM session_summaries
@@ -16982,6 +17042,15 @@ function getRecentOutcomes(db, projectId, userId) {
16982
17042
  const picked = [];
16983
17043
  const seen = new Set;
16984
17044
  for (const summary of summaries) {
17045
+ for (const item of parseSummaryJsonList(summary.recent_outcomes)) {
17046
+ const normalized = item.toLowerCase().replace(/\s+/g, " ").trim();
17047
+ if (!normalized || seen.has(normalized))
17048
+ continue;
17049
+ seen.add(normalized);
17050
+ picked.push(item);
17051
+ if (picked.length >= 5)
17052
+ return picked;
17053
+ }
16985
17054
  for (const line of [
16986
17055
  ...extractMeaningfulLines(summary.completed, 2),
16987
17056
  ...extractMeaningfulLines(summary.learned, 1)
@@ -16995,6 +17064,17 @@ function getRecentOutcomes(db, projectId, userId) {
16995
17064
  return picked;
16996
17065
  }
16997
17066
  }
17067
+ for (const session of recentSessions ?? []) {
17068
+ for (const item of parseSummaryJsonList(session.recent_outcomes)) {
17069
+ const normalized = item.toLowerCase().replace(/\s+/g, " ").trim();
17070
+ if (!normalized || seen.has(normalized))
17071
+ continue;
17072
+ seen.add(normalized);
17073
+ picked.push(item);
17074
+ if (picked.length >= 5)
17075
+ return picked;
17076
+ }
17077
+ }
16998
17078
  const rows = db.db.query(`SELECT * FROM observations
16999
17079
  WHERE project_id = ?
17000
17080
  AND lifecycle IN ('active', 'aging', 'pinned')
@@ -18653,6 +18733,50 @@ class VectorApiError extends Error {
18653
18733
  }
18654
18734
  }
18655
18735
 
18736
+ // src/capture/session-handoff.ts
18737
+ function buildSessionHandoffMetadata(prompts, toolEvents, observations) {
18738
+ const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
18739
+ const recentRequestPrompts = prompts.slice(-3).map((prompt) => prompt.prompt.trim()).filter(Boolean);
18740
+ const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
18741
+ const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
18742
+ const hotFiles = [...new Set(observations.flatMap((obs) => [
18743
+ ...parseJsonArray3(obs.files_modified),
18744
+ ...parseJsonArray3(obs.files_read)
18745
+ ]).filter(Boolean))].slice(0, 6);
18746
+ const recentOutcomes = observations.filter((obs) => ["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type)).map((obs) => obs.title.trim()).filter((title) => title.length > 0).slice(0, 6);
18747
+ const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
18748
+ const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
18749
+ if (!obs.source_tool)
18750
+ return acc;
18751
+ acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
18752
+ return acc;
18753
+ }, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
18754
+ const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
18755
+ return {
18756
+ prompt_count: prompts.length,
18757
+ tool_event_count: toolEvents.length,
18758
+ recent_request_prompts: recentRequestPrompts,
18759
+ latest_request: latestRequest,
18760
+ recent_tool_names: recentToolNames,
18761
+ recent_tool_commands: recentToolCommands,
18762
+ capture_state: captureState,
18763
+ hot_files: hotFiles,
18764
+ recent_outcomes: recentOutcomes,
18765
+ observation_source_tools: observationSourceTools,
18766
+ latest_observation_prompt_number: latestObservationPromptNumber
18767
+ };
18768
+ }
18769
+ function parseJsonArray3(value) {
18770
+ if (!value)
18771
+ return [];
18772
+ try {
18773
+ const parsed = JSON.parse(value);
18774
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
18775
+ } catch {
18776
+ return [];
18777
+ }
18778
+ }
18779
+
18656
18780
  // src/sync/push.ts
18657
18781
  function buildVectorDocument(obs, config2, project) {
18658
18782
  const parts = [obs.title];
@@ -18793,7 +18917,7 @@ async function pushOutbox(db, client, config2, batchSize = 50) {
18793
18917
  const doc3 = buildSummaryVectorDocument(summary, config2, {
18794
18918
  canonical_id: project2.canonical_id,
18795
18919
  name: project2.name
18796
- }, summaryObservations, buildSummaryCaptureContext(sessionPrompts, sessionToolEvents, summaryObservations));
18920
+ }, summaryObservations, buildSessionHandoffMetadata(sessionPrompts, sessionToolEvents, summaryObservations));
18797
18921
  batch.push({ entryId: entry.id, doc: doc3 });
18798
18922
  continue;
18799
18923
  }
@@ -18864,48 +18988,6 @@ function countPresentSections2(summary) {
18864
18988
  function extractSectionItems2(section) {
18865
18989
  return extractSummaryItems(section, 4);
18866
18990
  }
18867
- function buildSummaryCaptureContext(prompts, toolEvents, observations) {
18868
- const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
18869
- const recentRequestPrompts = prompts.slice(-3).map((prompt) => prompt.prompt.trim()).filter(Boolean);
18870
- const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
18871
- const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
18872
- const hotFiles = [...new Set(observations.flatMap((obs) => [
18873
- ...parseJsonArray3(obs.files_modified),
18874
- ...parseJsonArray3(obs.files_read)
18875
- ]).filter(Boolean))].slice(0, 6);
18876
- const recentOutcomes = observations.filter((obs) => ["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type)).map((obs) => obs.title.trim()).filter((title) => title.length > 0).slice(0, 6);
18877
- const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
18878
- const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
18879
- if (!obs.source_tool)
18880
- return acc;
18881
- acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
18882
- return acc;
18883
- }, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
18884
- const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
18885
- return {
18886
- prompt_count: prompts.length,
18887
- tool_event_count: toolEvents.length,
18888
- recent_request_prompts: recentRequestPrompts,
18889
- latest_request: latestRequest,
18890
- recent_tool_names: recentToolNames,
18891
- recent_tool_commands: recentToolCommands,
18892
- capture_state: captureState,
18893
- hot_files: hotFiles,
18894
- recent_outcomes: recentOutcomes,
18895
- observation_source_tools: observationSourceTools,
18896
- latest_observation_prompt_number: latestObservationPromptNumber
18897
- };
18898
- }
18899
- function parseJsonArray3(value) {
18900
- if (!value)
18901
- return [];
18902
- try {
18903
- const parsed = JSON.parse(value);
18904
- return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
18905
- } catch {
18906
- return [];
18907
- }
18908
- }
18909
18991
 
18910
18992
  // src/sync/pull.ts
18911
18993
  var PULL_CURSOR_KEY = "pull_cursor";
@@ -19011,10 +19093,20 @@ function mergeRemoteSummary(db, config2, change, projectId) {
19011
19093
  investigated: typeof change.metadata?.investigated === "string" ? change.metadata.investigated : null,
19012
19094
  learned: typeof change.metadata?.learned === "string" ? change.metadata.learned : null,
19013
19095
  completed: typeof change.metadata?.completed === "string" ? change.metadata.completed : null,
19014
- next_steps: typeof change.metadata?.next_steps === "string" ? change.metadata.next_steps : null
19096
+ next_steps: typeof change.metadata?.next_steps === "string" ? change.metadata.next_steps : null,
19097
+ capture_state: typeof change.metadata?.capture_state === "string" ? change.metadata.capture_state : null,
19098
+ recent_tool_names: encodeStringArray(change.metadata?.recent_tool_names),
19099
+ hot_files: encodeStringArray(change.metadata?.hot_files),
19100
+ recent_outcomes: encodeStringArray(change.metadata?.recent_outcomes)
19015
19101
  });
19016
19102
  return Boolean(summary);
19017
19103
  }
19104
+ function encodeStringArray(value) {
19105
+ if (!Array.isArray(value))
19106
+ return null;
19107
+ const normalized = value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
19108
+ return normalized.length > 0 ? JSON.stringify(normalized) : null;
19109
+ }
19018
19110
  function normalizeRemoteObservationType(rawType, sourceId) {
19019
19111
  const type = typeof rawType === "string" ? rawType.trim().toLowerCase() : "";
19020
19112
  if (type === "bugfix" || type === "discovery" || type === "decision" || type === "pattern" || type === "change" || type === "feature" || type === "refactor" || type === "digest" || type === "standard" || type === "message") {
@@ -19817,7 +19909,7 @@ process.on("SIGTERM", () => {
19817
19909
  });
19818
19910
  var server = new McpServer({
19819
19911
  name: "engrm",
19820
- version: "0.4.19"
19912
+ version: "0.4.22"
19821
19913
  });
19822
19914
  server.tool("save_observation", "Save an observation to memory", {
19823
19915
  type: exports_external.enum([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.4.19",
3
+ "version": "0.4.22",
4
4
  "description": "Shared memory across devices, sessions, and coding agents",
5
5
  "mcpName": "io.github.dr12hes/engrm",
6
6
  "type": "module",