@gethmy/mcp 2.0.0 → 2.1.1
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/README.md +6 -1
- package/dist/cli.js +711 -59
- package/dist/index.js +5 -3
- package/dist/lib/__tests__/active-learning.test.js +386 -0
- package/dist/lib/__tests__/agent-performance-profiles.test.js +325 -0
- package/dist/lib/__tests__/auto-session.test.js +661 -0
- package/dist/lib/__tests__/context-assembly.test.js +362 -0
- package/dist/lib/__tests__/graph-expansion.test.js +150 -0
- package/dist/lib/__tests__/integration-memory-crud.test.js +797 -0
- package/dist/lib/__tests__/integration-memory-system.test.js +281 -0
- package/dist/lib/__tests__/lifecycle-maintenance.test.js +207 -0
- package/dist/lib/__tests__/pattern-detection.test.js +295 -0
- package/dist/lib/__tests__/prompt-builder.test.js +418 -0
- package/dist/lib/active-learning.js +878 -0
- package/dist/lib/api-client.js +550 -0
- package/dist/lib/auto-session.js +173 -0
- package/dist/lib/cli.js +127 -0
- package/dist/lib/config.js +205 -0
- package/dist/lib/consolidation.js +243 -0
- package/dist/lib/context-assembly.js +606 -0
- package/dist/lib/graph-expansion.js +163 -0
- package/dist/lib/http.js +174 -0
- package/dist/lib/index.js +7 -0
- package/dist/lib/lifecycle-maintenance.js +88 -0
- package/dist/lib/prompt-builder.js +483 -0
- package/dist/lib/remote.js +166 -0
- package/dist/lib/server.js +3132 -0
- package/dist/lib/tui/agents.js +116 -0
- package/dist/lib/tui/docs.js +744 -0
- package/dist/lib/tui/setup.js +1068 -0
- package/dist/lib/tui/theme.js +95 -0
- package/dist/lib/tui/writer.js +200 -0
- package/package.json +15 -6
- package/src/__tests__/active-learning.test.ts +483 -0
- package/src/__tests__/agent-performance-profiles.test.ts +468 -0
- package/src/__tests__/auto-session.test.ts +912 -0
- package/src/__tests__/context-assembly.test.ts +506 -0
- package/src/__tests__/graph-expansion.test.ts +285 -0
- package/src/__tests__/integration-memory-crud.test.ts +948 -0
- package/src/__tests__/integration-memory-system.test.ts +321 -0
- package/src/__tests__/lifecycle-maintenance.test.ts +238 -0
- package/src/__tests__/pattern-detection.test.ts +438 -0
- package/src/__tests__/prompt-builder.test.ts +505 -0
- package/src/active-learning.ts +1227 -0
- package/src/api-client.ts +969 -0
- package/src/auto-session.ts +218 -0
- package/src/cli.ts +166 -0
- package/src/config.ts +285 -0
- package/src/consolidation.ts +314 -0
- package/src/context-assembly.ts +842 -0
- package/src/graph-expansion.ts +234 -0
- package/src/http.ts +265 -0
- package/src/index.ts +8 -0
- package/src/lifecycle-maintenance.ts +120 -0
- package/src/prompt-builder.ts +681 -0
- package/src/remote.ts +227 -0
- package/src/server.ts +3858 -0
- package/src/tui/agents.ts +154 -0
- package/src/tui/docs.ts +863 -0
- package/src/tui/setup.ts +1281 -0
- package/src/tui/theme.ts +114 -0
- package/src/tui/writer.ts +260 -0
|
@@ -0,0 +1,314 @@
|
|
|
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
|
+
|
|
8
|
+
import type { HarmonyApiClient } from "./api-client.js";
|
|
9
|
+
import { findSimilarEntities } from "./graph-expansion.js";
|
|
10
|
+
|
|
11
|
+
interface MemoryEntity {
|
|
12
|
+
id: string;
|
|
13
|
+
type: string;
|
|
14
|
+
title: string;
|
|
15
|
+
content: string;
|
|
16
|
+
confidence: number;
|
|
17
|
+
memory_tier: string;
|
|
18
|
+
tags: string[];
|
|
19
|
+
metadata?: Record<string, unknown>;
|
|
20
|
+
updated_at?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ConsolidationResult {
|
|
24
|
+
consolidated: number;
|
|
25
|
+
clustersFound: number;
|
|
26
|
+
entitiesProcessed: number;
|
|
27
|
+
details: Array<{
|
|
28
|
+
clusterSize: number;
|
|
29
|
+
mergedTitle: string;
|
|
30
|
+
memberTitles: string[];
|
|
31
|
+
entityId?: string; // set when not dry run
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ConsolidationOptions {
|
|
36
|
+
dryRun?: boolean;
|
|
37
|
+
minClusterSize?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Consolidate similar draft/episode memories into reference entities.
|
|
42
|
+
*
|
|
43
|
+
* 1. Lists all draft and episode tier entities in scope
|
|
44
|
+
* 2. Groups by entity type
|
|
45
|
+
* 3. For each type group, finds clusters via embedding similarity
|
|
46
|
+
* 4. Merges clusters into new reference entities with part_of relations
|
|
47
|
+
*/
|
|
48
|
+
export async function consolidateMemories(
|
|
49
|
+
client: HarmonyApiClient,
|
|
50
|
+
workspaceId: string,
|
|
51
|
+
projectId?: string,
|
|
52
|
+
options?: ConsolidationOptions,
|
|
53
|
+
): Promise<ConsolidationResult> {
|
|
54
|
+
const dryRun = options?.dryRun !== false; // default true
|
|
55
|
+
const minClusterSize = options?.minClusterSize ?? 2;
|
|
56
|
+
|
|
57
|
+
const result: ConsolidationResult = {
|
|
58
|
+
consolidated: 0,
|
|
59
|
+
clustersFound: 0,
|
|
60
|
+
entitiesProcessed: 0,
|
|
61
|
+
details: [],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Step 1: Fetch all draft and episode entities
|
|
65
|
+
const listResult = await client.listMemoryEntities({
|
|
66
|
+
workspace_id: workspaceId,
|
|
67
|
+
project_id: projectId,
|
|
68
|
+
limit: 100,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const allEntities = ((listResult.entities || []) as MemoryEntity[]).filter(
|
|
72
|
+
(e) => e.memory_tier === "draft" || e.memory_tier === "episode",
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
result.entitiesProcessed = allEntities.length;
|
|
76
|
+
if (allEntities.length < minClusterSize) return result;
|
|
77
|
+
|
|
78
|
+
// Step 2: Group by type
|
|
79
|
+
const typeGroups = new Map<string, MemoryEntity[]>();
|
|
80
|
+
for (const entity of allEntities) {
|
|
81
|
+
const group = typeGroups.get(entity.type) || [];
|
|
82
|
+
group.push(entity);
|
|
83
|
+
typeGroups.set(entity.type, group);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Step 3: Find clusters within each type group
|
|
87
|
+
for (const [type, entities] of typeGroups) {
|
|
88
|
+
if (entities.length < minClusterSize) continue;
|
|
89
|
+
|
|
90
|
+
const clustered = new Set<string>();
|
|
91
|
+
const clusters: MemoryEntity[][] = [];
|
|
92
|
+
|
|
93
|
+
for (const entity of entities) {
|
|
94
|
+
if (clustered.has(entity.id)) continue;
|
|
95
|
+
|
|
96
|
+
// Search for similar entities using embedding-based search
|
|
97
|
+
const similar = await findSimilarEntities(
|
|
98
|
+
client,
|
|
99
|
+
entity.title,
|
|
100
|
+
entity.content,
|
|
101
|
+
workspaceId,
|
|
102
|
+
{
|
|
103
|
+
projectId,
|
|
104
|
+
limit: 20,
|
|
105
|
+
minRrfScore: 0.01,
|
|
106
|
+
excludeIds: [...clustered],
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Filter to only entities in our current type group that aren't yet clustered
|
|
111
|
+
const entityIdSet = new Set(entities.map((e) => e.id));
|
|
112
|
+
const clusterMembers = similar.filter(
|
|
113
|
+
(s) =>
|
|
114
|
+
entityIdSet.has(s.id) &&
|
|
115
|
+
!clustered.has(s.id) &&
|
|
116
|
+
s.id !== entity.id &&
|
|
117
|
+
s.type === type,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (clusterMembers.length >= minClusterSize - 1) {
|
|
121
|
+
const cluster = [
|
|
122
|
+
entity,
|
|
123
|
+
...clusterMembers.slice(0, 5).map((s) => {
|
|
124
|
+
// Map back to full entity from our list
|
|
125
|
+
return entities.find((e) => e.id === s.id) || entity;
|
|
126
|
+
}),
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
// Deduplicate by id
|
|
130
|
+
const uniqueCluster: MemoryEntity[] = [];
|
|
131
|
+
const seen = new Set<string>();
|
|
132
|
+
for (const member of cluster) {
|
|
133
|
+
if (!seen.has(member.id)) {
|
|
134
|
+
seen.add(member.id);
|
|
135
|
+
uniqueCluster.push(member);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (uniqueCluster.length >= minClusterSize) {
|
|
140
|
+
clusters.push(uniqueCluster);
|
|
141
|
+
for (const member of uniqueCluster) {
|
|
142
|
+
clustered.add(member.id);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Step 4: Create consolidated entities for each cluster
|
|
149
|
+
for (const cluster of clusters) {
|
|
150
|
+
result.clustersFound++;
|
|
151
|
+
|
|
152
|
+
// Derive title from most common words across cluster titles
|
|
153
|
+
const mergedTitle = deriveClusterTitle(cluster, type);
|
|
154
|
+
const memberTitles = cluster.map((e) => e.title);
|
|
155
|
+
|
|
156
|
+
// Merge content as bullet points
|
|
157
|
+
const mergedContent = [
|
|
158
|
+
`Consolidated from ${cluster.length} ${type} memories:\n`,
|
|
159
|
+
...cluster.map((e) => `- **${e.title}**: ${e.content.slice(0, 200)}`),
|
|
160
|
+
].join("\n");
|
|
161
|
+
|
|
162
|
+
// Max confidence from cluster members
|
|
163
|
+
const maxConfidence = Math.max(...cluster.map((e) => e.confidence));
|
|
164
|
+
|
|
165
|
+
// Union of all tags (deduped)
|
|
166
|
+
const allTags = [...new Set(cluster.flatMap((e) => e.tags || []))];
|
|
167
|
+
|
|
168
|
+
const detail: ConsolidationResult["details"][0] = {
|
|
169
|
+
clusterSize: cluster.length,
|
|
170
|
+
mergedTitle,
|
|
171
|
+
memberTitles,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (!dryRun) {
|
|
175
|
+
try {
|
|
176
|
+
// Create consolidated reference entity
|
|
177
|
+
const createResult = await client.createMemoryEntity({
|
|
178
|
+
workspace_id: workspaceId,
|
|
179
|
+
project_id: projectId,
|
|
180
|
+
type,
|
|
181
|
+
scope: "project",
|
|
182
|
+
memory_tier: "reference",
|
|
183
|
+
title: mergedTitle,
|
|
184
|
+
content: mergedContent,
|
|
185
|
+
confidence: maxConfidence,
|
|
186
|
+
tags: [...allTags.slice(0, 15), "consolidated"],
|
|
187
|
+
metadata: {
|
|
188
|
+
source: "consolidation",
|
|
189
|
+
member_ids: cluster.map((e) => e.id),
|
|
190
|
+
consolidated_at: new Date().toISOString(),
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const newEntity = createResult.entity as { id: string };
|
|
195
|
+
if (newEntity?.id) {
|
|
196
|
+
detail.entityId = newEntity.id;
|
|
197
|
+
|
|
198
|
+
// Create part_of relations from members → consolidated entity
|
|
199
|
+
for (const member of cluster) {
|
|
200
|
+
try {
|
|
201
|
+
await client.createMemoryRelation({
|
|
202
|
+
source_id: member.id,
|
|
203
|
+
target_id: newEntity.id,
|
|
204
|
+
relation_type: "part_of",
|
|
205
|
+
confidence: 0.8,
|
|
206
|
+
});
|
|
207
|
+
} catch {
|
|
208
|
+
// Skip duplicate relations
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Downgrade member confidence by 0.3 (min 0.1)
|
|
213
|
+
for (const member of cluster) {
|
|
214
|
+
try {
|
|
215
|
+
const newConf = Math.max(member.confidence - 0.3, 0.1);
|
|
216
|
+
await client.updateMemoryEntity(member.id, {
|
|
217
|
+
confidence: newConf,
|
|
218
|
+
metadata: {
|
|
219
|
+
consolidated_into: newEntity.id,
|
|
220
|
+
original_confidence: member.confidence,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
} catch {
|
|
224
|
+
// Non-fatal
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
result.consolidated++;
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
// Non-fatal: consolidation failure for one cluster shouldn't block others
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
result.consolidated++;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
result.details.push(detail);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Derive a cluster title from the most common meaningful words across member titles.
|
|
246
|
+
*/
|
|
247
|
+
function deriveClusterTitle(cluster: MemoryEntity[], type: string): string {
|
|
248
|
+
const stopWords = new Set([
|
|
249
|
+
"the",
|
|
250
|
+
"a",
|
|
251
|
+
"an",
|
|
252
|
+
"is",
|
|
253
|
+
"are",
|
|
254
|
+
"was",
|
|
255
|
+
"were",
|
|
256
|
+
"be",
|
|
257
|
+
"been",
|
|
258
|
+
"being",
|
|
259
|
+
"have",
|
|
260
|
+
"has",
|
|
261
|
+
"had",
|
|
262
|
+
"do",
|
|
263
|
+
"does",
|
|
264
|
+
"did",
|
|
265
|
+
"will",
|
|
266
|
+
"shall",
|
|
267
|
+
"would",
|
|
268
|
+
"should",
|
|
269
|
+
"may",
|
|
270
|
+
"might",
|
|
271
|
+
"can",
|
|
272
|
+
"could",
|
|
273
|
+
"of",
|
|
274
|
+
"in",
|
|
275
|
+
"to",
|
|
276
|
+
"for",
|
|
277
|
+
"with",
|
|
278
|
+
"on",
|
|
279
|
+
"at",
|
|
280
|
+
"from",
|
|
281
|
+
"by",
|
|
282
|
+
"and",
|
|
283
|
+
"or",
|
|
284
|
+
"but",
|
|
285
|
+
"not",
|
|
286
|
+
"session",
|
|
287
|
+
"blocker",
|
|
288
|
+
"pattern",
|
|
289
|
+
"solution",
|
|
290
|
+
"error",
|
|
291
|
+
"task",
|
|
292
|
+
"mid-session",
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
const wordCounts = new Map<string, number>();
|
|
296
|
+
for (const entity of cluster) {
|
|
297
|
+
const words = entity.title
|
|
298
|
+
.toLowerCase()
|
|
299
|
+
.split(/\W+/)
|
|
300
|
+
.filter((w) => w.length > 2 && !stopWords.has(w));
|
|
301
|
+
for (const word of words) {
|
|
302
|
+
wordCounts.set(word, (wordCounts.get(word) || 0) + 1);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Sort by frequency, take top 3
|
|
307
|
+
const topWords = [...wordCounts.entries()]
|
|
308
|
+
.sort((a, b) => b[1] - a[1])
|
|
309
|
+
.slice(0, 3)
|
|
310
|
+
.map(([word]) => word);
|
|
311
|
+
|
|
312
|
+
const suffix = topWords.length > 0 ? topWords.join(", ") : "various";
|
|
313
|
+
return `Consolidated ${type}: ${suffix}`;
|
|
314
|
+
}
|