@gethmy/mcp 2.3.1 → 2.3.2

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.
Files changed (34) hide show
  1. package/dist/lib/active-learning.js +939 -787
  2. package/dist/lib/api-client.js +2527 -644
  3. package/dist/lib/auto-session.js +177 -196
  4. package/dist/lib/cli.js +34954 -128
  5. package/dist/lib/config.js +235 -201
  6. package/dist/lib/consolidation.js +374 -289
  7. package/dist/lib/context-assembly.js +1265 -838
  8. package/dist/lib/graph-expansion.js +139 -155
  9. package/dist/lib/http.js +1917 -130
  10. package/dist/lib/index.js +29525 -5
  11. package/dist/lib/lifecycle-maintenance.js +663 -79
  12. package/dist/lib/memory-cleanup.js +1315 -409
  13. package/dist/lib/onboard.js +2588 -32
  14. package/dist/lib/prompt-builder.js +438 -445
  15. package/dist/lib/remote.js +31733 -143
  16. package/dist/lib/server.js +29388 -3229
  17. package/dist/lib/skills.js +315 -132
  18. package/dist/lib/tui/agents.js +128 -107
  19. package/dist/lib/tui/docs.js +1590 -687
  20. package/dist/lib/tui/setup.js +5698 -804
  21. package/dist/lib/tui/theme.js +183 -86
  22. package/dist/lib/tui/writer.js +1149 -176
  23. package/package.json +2 -2
  24. package/src/memory-cleanup.ts +2 -4
  25. package/dist/lib/__tests__/active-learning.test.js +0 -386
  26. package/dist/lib/__tests__/agent-performance-profiles.test.js +0 -325
  27. package/dist/lib/__tests__/auto-session.test.js +0 -661
  28. package/dist/lib/__tests__/context-assembly.test.js +0 -362
  29. package/dist/lib/__tests__/graph-expansion.test.js +0 -150
  30. package/dist/lib/__tests__/integration-memory-crud.test.js +0 -797
  31. package/dist/lib/__tests__/integration-memory-system.test.js +0 -281
  32. package/dist/lib/__tests__/lifecycle-maintenance.test.js +0 -207
  33. package/dist/lib/__tests__/pattern-detection.test.js +0 -295
  34. package/dist/lib/__tests__/prompt-builder.test.js +0 -418
@@ -1,303 +1,388 @@
1
- /**
2
- * Smart Memory Consolidation
3
- *
4
- * Clusters similar draft/episode memories and merges them into
5
- * consolidated reference entities to reduce noise and improve retrieval.
6
- */
7
- import { findSimilarEntities } from "./graph-expansion.js";
8
- /**
9
- * Consolidate similar draft/episode memories into reference entities.
10
- *
11
- * 1. Lists all draft and episode tier entities in scope
12
- * 2. Groups by entity type
13
- * 3. For each type group, finds clusters via embedding similarity
14
- * 4. Merges clusters into new reference entities with part_of relations
15
- */
16
- export async function consolidateMemories(client, workspaceId, projectId, options) {
17
- const dryRun = options?.dryRun !== false; // default true
18
- const minClusterSize = options?.minClusterSize ?? 3; // raised from 2 to reduce noise
19
- const result = {
20
- consolidated: 0,
21
- clustersFound: 0,
22
- entitiesProcessed: 0,
23
- details: [],
24
- };
25
- // Step 1: Fetch all draft and episode entities
26
- const listResult = await client.listMemoryEntities({
27
- workspace_id: workspaceId,
1
+ import { createRequire } from "node:module";
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __toESM = (mod, isNodeMode, target) => {
8
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
9
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
+ for (let key of __getOwnPropNames(mod))
11
+ if (!__hasOwnProp.call(to, key))
12
+ __defProp(to, key, {
13
+ get: () => mod[key],
14
+ enumerable: true
15
+ });
16
+ return to;
17
+ };
18
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true,
24
+ configurable: true,
25
+ set: (newValue) => all[name] = () => newValue
26
+ });
27
+ };
28
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
29
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
30
+
31
+ // src/graph-expansion.ts
32
+ async function autoExpandGraph(client, entityId, title, content, _tags, workspaceId, projectId, maxRelations = 5) {
33
+ try {
34
+ const contentSnippet = content.slice(0, 200).trim();
35
+ const query = [title, contentSnippet].filter(Boolean).join(" ");
36
+ let candidates = [];
37
+ const { entities } = await client.searchMemoryEntities(workspaceId, query, {
38
+ project_id: projectId,
39
+ limit: 20
40
+ });
41
+ candidates = entities.filter((e) => e.id !== entityId).slice(0, maxRelations);
42
+ if (candidates.length === 0) {
43
+ await new Promise((resolve) => setTimeout(resolve, 2000));
44
+ const retry = await client.searchMemoryEntities(workspaceId, query, {
28
45
  project_id: projectId,
29
- limit: 100,
46
+ limit: 20
47
+ });
48
+ candidates = retry.entities.filter((e) => e.id !== entityId).slice(0, maxRelations);
49
+ }
50
+ let relationsCreated = 0;
51
+ for (const candidate of candidates) {
52
+ try {
53
+ await client.createMemoryRelation({
54
+ source_id: entityId,
55
+ target_id: candidate.id,
56
+ relation_type: "relates_to",
57
+ confidence: 0.6
58
+ });
59
+ relationsCreated++;
60
+ } catch (err) {
61
+ const status = err?.status;
62
+ if (status !== 409) {}
63
+ }
64
+ }
65
+ return { relationsCreated };
66
+ } catch {
67
+ return { relationsCreated: 0 };
68
+ }
69
+ }
70
+ async function findSimilarEntities(client, title, content, workspaceId, options) {
71
+ const contentSnippet = content.slice(0, 200).trim();
72
+ const query = [title, contentSnippet].filter(Boolean).join(" ");
73
+ try {
74
+ const { entities } = await client.searchMemoryEntities(workspaceId, query, {
75
+ project_id: options?.projectId,
76
+ limit: options?.limit ?? 20,
77
+ type: options?.type
30
78
  });
31
- const allEntities = (listResult.entities || []).filter((e) => e.memory_tier === "draft" || e.memory_tier === "episode");
32
- result.entitiesProcessed = allEntities.length;
33
- if (allEntities.length < minClusterSize)
34
- return result;
35
- // Step 2: Group by type
36
- const typeGroups = new Map();
37
- for (const entity of allEntities) {
38
- const group = typeGroups.get(entity.type) || [];
39
- group.push(entity);
40
- typeGroups.set(entity.type, group);
79
+ const minScore = options?.minRrfScore ?? 0;
80
+ const excludeSet = new Set(options?.excludeIds || []);
81
+ return entities.filter((e) => {
82
+ if (excludeSet.has(e.id))
83
+ return false;
84
+ if (minScore > 0 && (e.rrf_score ?? 0) < minScore)
85
+ return false;
86
+ return true;
87
+ });
88
+ } catch {
89
+ return [];
90
+ }
91
+ }
92
+ var CAUSAL_LOOKUP = [
93
+ {
94
+ sourceType: "error",
95
+ targetType: "solution",
96
+ relation: "resolved_by",
97
+ direction: "forward"
98
+ },
99
+ {
100
+ sourceType: "solution",
101
+ targetType: "error",
102
+ relation: "resolved_by",
103
+ direction: "reverse"
104
+ },
105
+ {
106
+ sourceType: "lesson",
107
+ targetType: "error",
108
+ relation: "learned_from",
109
+ direction: "forward"
110
+ }
111
+ ];
112
+ async function linkCrossTypeNeighbors(client, entityId, entityType, title, content, workspaceId, projectId) {
113
+ const rules = CAUSAL_LOOKUP.filter((r) => r.sourceType === entityType);
114
+ if (rules.length === 0)
115
+ return { relationsCreated: 0 };
116
+ let relationsCreated = 0;
117
+ for (const rule of rules) {
118
+ try {
119
+ const matches = await findSimilarEntities(client, title, content, workspaceId, {
120
+ projectId,
121
+ limit: 10,
122
+ minRrfScore: 0.04,
123
+ excludeIds: [entityId],
124
+ type: rule.targetType
125
+ });
126
+ for (const match of matches.slice(0, 3)) {
127
+ const sourceId = rule.direction === "forward" ? entityId : match.id;
128
+ const targetId = rule.direction === "forward" ? match.id : entityId;
129
+ try {
130
+ await client.createMemoryRelation({
131
+ source_id: sourceId,
132
+ target_id: targetId,
133
+ relation_type: rule.relation,
134
+ confidence: 0.65
135
+ });
136
+ relationsCreated++;
137
+ } catch {}
138
+ }
139
+ } catch {}
140
+ }
141
+ return { relationsCreated };
142
+ }
143
+
144
+ // src/consolidation.ts
145
+ async function consolidateMemories(client, workspaceId, projectId, options) {
146
+ const dryRun = options?.dryRun !== false;
147
+ const minClusterSize = options?.minClusterSize ?? 3;
148
+ const result = {
149
+ consolidated: 0,
150
+ clustersFound: 0,
151
+ entitiesProcessed: 0,
152
+ details: []
153
+ };
154
+ const listResult = await client.listMemoryEntities({
155
+ workspace_id: workspaceId,
156
+ project_id: projectId,
157
+ limit: 100
158
+ });
159
+ const allEntities = (listResult.entities || []).filter((e) => e.memory_tier === "draft" || e.memory_tier === "episode");
160
+ result.entitiesProcessed = allEntities.length;
161
+ if (allEntities.length < minClusterSize)
162
+ return result;
163
+ const typeGroups = new Map;
164
+ for (const entity of allEntities) {
165
+ const group = typeGroups.get(entity.type) || [];
166
+ group.push(entity);
167
+ typeGroups.set(entity.type, group);
168
+ }
169
+ for (const [type, entities] of typeGroups) {
170
+ if (entities.length < minClusterSize)
171
+ continue;
172
+ const clustered = new Set;
173
+ const clusters = [];
174
+ for (const entity of entities) {
175
+ if (clustered.has(entity.id))
176
+ continue;
177
+ const similar = await findSimilarEntities(client, entity.title, entity.content, workspaceId, {
178
+ projectId,
179
+ limit: 20,
180
+ minRrfScore: 0.01,
181
+ excludeIds: [...clustered]
182
+ });
183
+ const entityIdSet = new Set(entities.map((e) => e.id));
184
+ const clusterMembers = similar.filter((s) => entityIdSet.has(s.id) && !clustered.has(s.id) && s.id !== entity.id && s.type === type);
185
+ if (clusterMembers.length >= minClusterSize - 1) {
186
+ const cluster = [
187
+ entity,
188
+ ...clusterMembers.slice(0, 5).map((s) => {
189
+ return entities.find((e) => e.id === s.id) || entity;
190
+ })
191
+ ];
192
+ const uniqueCluster = [];
193
+ const seen = new Set;
194
+ for (const member of cluster) {
195
+ if (!seen.has(member.id)) {
196
+ seen.add(member.id);
197
+ uniqueCluster.push(member);
198
+ }
199
+ }
200
+ if (uniqueCluster.length >= minClusterSize) {
201
+ clusters.push(uniqueCluster);
202
+ for (const member of uniqueCluster) {
203
+ clustered.add(member.id);
204
+ }
205
+ }
206
+ }
41
207
  }
42
- // Step 3: Find clusters within each type group
43
- for (const [type, entities] of typeGroups) {
44
- if (entities.length < minClusterSize)
45
- continue;
46
- const clustered = new Set();
47
- const clusters = [];
48
- for (const entity of entities) {
49
- if (clustered.has(entity.id))
50
- continue;
51
- // Search for similar entities using embedding-based search
52
- const similar = await findSimilarEntities(client, entity.title, entity.content, workspaceId, {
53
- projectId,
54
- limit: 20,
55
- minRrfScore: 0.01,
56
- excludeIds: [...clustered],
57
- });
58
- // Filter to only entities in our current type group that aren't yet clustered
59
- const entityIdSet = new Set(entities.map((e) => e.id));
60
- const clusterMembers = similar.filter((s) => entityIdSet.has(s.id) &&
61
- !clustered.has(s.id) &&
62
- s.id !== entity.id &&
63
- s.type === type);
64
- if (clusterMembers.length >= minClusterSize - 1) {
65
- const cluster = [
66
- entity,
67
- ...clusterMembers.slice(0, 5).map((s) => {
68
- // Map back to full entity from our list
69
- return entities.find((e) => e.id === s.id) || entity;
70
- }),
71
- ];
72
- // Deduplicate by id
73
- const uniqueCluster = [];
74
- const seen = new Set();
75
- for (const member of cluster) {
76
- if (!seen.has(member.id)) {
77
- seen.add(member.id);
78
- uniqueCluster.push(member);
79
- }
80
- }
81
- if (uniqueCluster.length >= minClusterSize) {
82
- clusters.push(uniqueCluster);
83
- for (const member of uniqueCluster) {
84
- clustered.add(member.id);
85
- }
86
- }
208
+ for (const cluster of clusters) {
209
+ result.clustersFound++;
210
+ const mergedTitle = deriveClusterTitle(cluster, type);
211
+ const memberTitles = cluster.map((e) => e.title);
212
+ const mergedContent = synthesizeClusterContent(cluster, type);
213
+ const maxConfidence = Math.max(...cluster.map((e) => e.confidence));
214
+ const allTags = [...new Set(cluster.flatMap((e) => e.tags || []))];
215
+ const detail = {
216
+ clusterSize: cluster.length,
217
+ mergedTitle,
218
+ memberTitles
219
+ };
220
+ if (!dryRun) {
221
+ try {
222
+ const createResult = await client.createMemoryEntity({
223
+ workspace_id: workspaceId,
224
+ project_id: projectId,
225
+ type,
226
+ scope: "project",
227
+ memory_tier: "reference",
228
+ title: mergedTitle,
229
+ content: mergedContent,
230
+ confidence: maxConfidence,
231
+ tags: [...allTags.slice(0, 15), "consolidated"],
232
+ metadata: {
233
+ source: "consolidation",
234
+ member_ids: cluster.map((e) => e.id),
235
+ consolidated_at: new Date().toISOString()
87
236
  }
88
- }
89
- // Step 4: Create consolidated entities for each cluster
90
- for (const cluster of clusters) {
91
- result.clustersFound++;
92
- // Derive title from most common words across cluster titles
93
- const mergedTitle = deriveClusterTitle(cluster, type);
94
- const memberTitles = cluster.map((e) => e.title);
95
- // Synthesize content: extract unique knowledge from each member,
96
- // not just a bullet list of titles. Each member's content is trimmed
97
- // to its first meaningful paragraph (skipping headers and metadata).
98
- const mergedContent = synthesizeClusterContent(cluster, type);
99
- // Max confidence from cluster members
100
- const maxConfidence = Math.max(...cluster.map((e) => e.confidence));
101
- // Union of all tags (deduped)
102
- const allTags = [...new Set(cluster.flatMap((e) => e.tags || []))];
103
- const detail = {
104
- clusterSize: cluster.length,
105
- mergedTitle,
106
- memberTitles,
107
- };
108
- if (!dryRun) {
109
- try {
110
- // Create consolidated reference entity
111
- const createResult = await client.createMemoryEntity({
112
- workspace_id: workspaceId,
113
- project_id: projectId,
114
- type,
115
- scope: "project",
116
- memory_tier: "reference",
117
- title: mergedTitle,
118
- content: mergedContent,
119
- confidence: maxConfidence,
120
- tags: [...allTags.slice(0, 15), "consolidated"],
121
- metadata: {
122
- source: "consolidation",
123
- member_ids: cluster.map((e) => e.id),
124
- consolidated_at: new Date().toISOString(),
125
- },
126
- });
127
- const newEntity = createResult.entity;
128
- if (newEntity?.id) {
129
- detail.entityId = newEntity.id;
130
- // Create part_of relations from members → consolidated entity
131
- for (const member of cluster) {
132
- try {
133
- await client.createMemoryRelation({
134
- source_id: member.id,
135
- target_id: newEntity.id,
136
- relation_type: "part_of",
137
- confidence: 0.8,
138
- });
139
- }
140
- catch {
141
- // Skip duplicate relations
142
- }
143
- }
144
- // Downgrade member confidence by 0.3 (min 0.1)
145
- for (const member of cluster) {
146
- try {
147
- const newConf = Math.max(member.confidence - 0.3, 0.1);
148
- await client.updateMemoryEntity(member.id, {
149
- confidence: newConf,
150
- metadata: {
151
- consolidated_into: newEntity.id,
152
- original_confidence: member.confidence,
153
- },
154
- });
155
- }
156
- catch {
157
- // Non-fatal
158
- }
159
- }
160
- result.consolidated++;
161
- }
162
- }
163
- catch {
164
- // Non-fatal: consolidation failure for one cluster shouldn't block others
165
- }
237
+ });
238
+ const newEntity = createResult.entity;
239
+ if (newEntity?.id) {
240
+ detail.entityId = newEntity.id;
241
+ for (const member of cluster) {
242
+ try {
243
+ await client.createMemoryRelation({
244
+ source_id: member.id,
245
+ target_id: newEntity.id,
246
+ relation_type: "part_of",
247
+ confidence: 0.8
248
+ });
249
+ } catch {}
166
250
  }
167
- else {
168
- result.consolidated++;
251
+ for (const member of cluster) {
252
+ try {
253
+ const newConf = Math.max(member.confidence - 0.3, 0.1);
254
+ await client.updateMemoryEntity(member.id, {
255
+ confidence: newConf,
256
+ metadata: {
257
+ consolidated_into: newEntity.id,
258
+ original_confidence: member.confidence
259
+ }
260
+ });
261
+ } catch {}
169
262
  }
170
- result.details.push(detail);
171
- }
263
+ result.consolidated++;
264
+ }
265
+ } catch {}
266
+ } else {
267
+ result.consolidated++;
268
+ }
269
+ result.details.push(detail);
172
270
  }
173
- return result;
271
+ }
272
+ return result;
174
273
  }
175
- /**
176
- * Synthesize cluster content by extracting unique, actionable knowledge
177
- * from each member entity. Skips boilerplate (headers, metadata, agent names)
178
- * and deduplicates similar lines across members.
179
- */
180
274
  function synthesizeClusterContent(cluster, type) {
181
- // Lines to skip: headers, agent metadata, timestamps, progress percentages
182
- const SKIP_PATTERNS = [
183
- /^##\s/,
184
- /^Agent:/,
185
- /^Duration:/,
186
- /^Labels:/,
187
- /^Progress:/,
188
- /^Session status:/,
189
- /^Completed at/,
190
- /^Final state:/,
191
- /^Related:/,
192
- /^When working on:/,
193
- /^\d+\.\s+.+\(\d+%,\s*\+\d+%\)/, // procedure step with progress percentages
194
- /^Last updated:/,
195
- /^Recurring pattern:/,
196
- /^Consolidated from/,
197
- ];
198
- const seenLines = new Set();
199
- const knowledgeLines = [];
200
- for (const entity of cluster) {
201
- const lines = entity.content.split("\n").map((l) => l.trim());
202
- for (const line of lines) {
203
- if (!line || line.length < 20)
204
- continue;
205
- if (SKIP_PATTERNS.some((p) => p.test(line)))
206
- continue;
207
- // Normalize for dedup: lowercase, strip markdown formatting
208
- const normalized = line
209
- .toLowerCase()
210
- .replace(/[*_`#[\]]/g, "")
211
- .trim();
212
- if (seenLines.has(normalized))
213
- continue;
214
- seenLines.add(normalized);
215
- knowledgeLines.push(line);
216
- }
217
- }
218
- if (knowledgeLines.length === 0) {
219
- // Fallback: if no knowledge was extractable, use a compact summary
220
- return `${cluster.length} related ${type} entities consolidated. Original titles:\n${cluster.map((e) => `- ${e.title}`).join("\n")}`;
275
+ const SKIP_PATTERNS = [
276
+ /^##\s/,
277
+ /^Agent:/,
278
+ /^Duration:/,
279
+ /^Labels:/,
280
+ /^Progress:/,
281
+ /^Session status:/,
282
+ /^Completed at/,
283
+ /^Final state:/,
284
+ /^Related:/,
285
+ /^When working on:/,
286
+ /^\d+\.\s+.+\(\d+%,\s*\+\d+%\)/,
287
+ /^Last updated:/,
288
+ /^Recurring pattern:/,
289
+ /^Consolidated from/
290
+ ];
291
+ const seenLines = new Set;
292
+ const knowledgeLines = [];
293
+ for (const entity of cluster) {
294
+ const lines = entity.content.split(`
295
+ `).map((l) => l.trim());
296
+ for (const line of lines) {
297
+ if (!line || line.length < 20)
298
+ continue;
299
+ if (SKIP_PATTERNS.some((p) => p.test(line)))
300
+ continue;
301
+ const normalized = line.toLowerCase().replace(/[*_`#[\]]/g, "").trim();
302
+ if (seenLines.has(normalized))
303
+ continue;
304
+ seenLines.add(normalized);
305
+ knowledgeLines.push(line);
221
306
  }
222
- // Cap at ~400 tokens worth of content (1600 chars)
223
- const MAX_CHARS = 1600;
224
- const result = [
225
- `Consolidated knowledge from ${cluster.length} ${type} entities:\n`,
226
- ];
227
- let charCount = result[0].length;
228
- for (const line of knowledgeLines) {
229
- if (charCount + line.length + 3 > MAX_CHARS)
230
- break;
231
- result.push(`- ${line}`);
232
- charCount += line.length + 3;
233
- }
234
- return result.join("\n");
307
+ }
308
+ if (knowledgeLines.length === 0) {
309
+ return `${cluster.length} related ${type} entities consolidated. Original titles:
310
+ ${cluster.map((e) => `- ${e.title}`).join(`
311
+ `)}`;
312
+ }
313
+ const MAX_CHARS = 1600;
314
+ const result = [
315
+ `Consolidated knowledge from ${cluster.length} ${type} entities:
316
+ `
317
+ ];
318
+ let charCount = result[0].length;
319
+ for (const line of knowledgeLines) {
320
+ if (charCount + line.length + 3 > MAX_CHARS)
321
+ break;
322
+ result.push(`- ${line}`);
323
+ charCount += line.length + 3;
324
+ }
325
+ return result.join(`
326
+ `);
235
327
  }
236
- /**
237
- * Derive a cluster title from the most common meaningful words across member titles.
238
- */
239
328
  function deriveClusterTitle(cluster, type) {
240
- const stopWords = new Set([
241
- "the",
242
- "a",
243
- "an",
244
- "is",
245
- "are",
246
- "was",
247
- "were",
248
- "be",
249
- "been",
250
- "being",
251
- "have",
252
- "has",
253
- "had",
254
- "do",
255
- "does",
256
- "did",
257
- "will",
258
- "shall",
259
- "would",
260
- "should",
261
- "may",
262
- "might",
263
- "can",
264
- "could",
265
- "of",
266
- "in",
267
- "to",
268
- "for",
269
- "with",
270
- "on",
271
- "at",
272
- "from",
273
- "by",
274
- "and",
275
- "or",
276
- "but",
277
- "not",
278
- "session",
279
- "blocker",
280
- "pattern",
281
- "solution",
282
- "error",
283
- "task",
284
- "mid-session",
285
- ]);
286
- const wordCounts = new Map();
287
- for (const entity of cluster) {
288
- const words = entity.title
289
- .toLowerCase()
290
- .split(/\W+/)
291
- .filter((w) => w.length > 2 && !stopWords.has(w));
292
- for (const word of words) {
293
- wordCounts.set(word, (wordCounts.get(word) || 0) + 1);
294
- }
329
+ const stopWords = new Set([
330
+ "the",
331
+ "a",
332
+ "an",
333
+ "is",
334
+ "are",
335
+ "was",
336
+ "were",
337
+ "be",
338
+ "been",
339
+ "being",
340
+ "have",
341
+ "has",
342
+ "had",
343
+ "do",
344
+ "does",
345
+ "did",
346
+ "will",
347
+ "shall",
348
+ "would",
349
+ "should",
350
+ "may",
351
+ "might",
352
+ "can",
353
+ "could",
354
+ "of",
355
+ "in",
356
+ "to",
357
+ "for",
358
+ "with",
359
+ "on",
360
+ "at",
361
+ "from",
362
+ "by",
363
+ "and",
364
+ "or",
365
+ "but",
366
+ "not",
367
+ "session",
368
+ "blocker",
369
+ "pattern",
370
+ "solution",
371
+ "error",
372
+ "task",
373
+ "mid-session"
374
+ ]);
375
+ const wordCounts = new Map;
376
+ for (const entity of cluster) {
377
+ const words = entity.title.toLowerCase().split(/\W+/).filter((w) => w.length > 2 && !stopWords.has(w));
378
+ for (const word of words) {
379
+ wordCounts.set(word, (wordCounts.get(word) || 0) + 1);
295
380
  }
296
- // Sort by frequency, take top 4 for more descriptive titles
297
- const topWords = [...wordCounts.entries()]
298
- .sort((a, b) => b[1] - a[1])
299
- .slice(0, 4)
300
- .map(([word]) => word[0].toUpperCase() + word.slice(1));
301
- const suffix = topWords.length > 0 ? topWords.join(" / ") : "Various";
302
- return `${type[0].toUpperCase() + type.slice(1)}: ${suffix}`;
381
+ }
382
+ const topWords = [...wordCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 4).map(([word]) => word[0].toUpperCase() + word.slice(1));
383
+ const suffix = topWords.length > 0 ? topWords.join(" / ") : "Various";
384
+ return `${type[0].toUpperCase() + type.slice(1)}: ${suffix}`;
303
385
  }
386
+ export {
387
+ consolidateMemories
388
+ };