@gethmy/mcp 2.3.1 → 2.3.3

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/api-client.js +2099 -648
  2. package/dist/lib/config.js +217 -201
  3. package/package.json +9 -5
  4. package/src/memory-cleanup.ts +2 -4
  5. package/dist/lib/__tests__/active-learning.test.js +0 -386
  6. package/dist/lib/__tests__/agent-performance-profiles.test.js +0 -325
  7. package/dist/lib/__tests__/auto-session.test.js +0 -661
  8. package/dist/lib/__tests__/context-assembly.test.js +0 -362
  9. package/dist/lib/__tests__/graph-expansion.test.js +0 -150
  10. package/dist/lib/__tests__/integration-memory-crud.test.js +0 -797
  11. package/dist/lib/__tests__/integration-memory-system.test.js +0 -281
  12. package/dist/lib/__tests__/lifecycle-maintenance.test.js +0 -207
  13. package/dist/lib/__tests__/pattern-detection.test.js +0 -295
  14. package/dist/lib/__tests__/prompt-builder.test.js +0 -418
  15. package/dist/lib/active-learning.js +0 -822
  16. package/dist/lib/auto-session.js +0 -214
  17. package/dist/lib/cli.js +0 -138
  18. package/dist/lib/consolidation.js +0 -303
  19. package/dist/lib/context-assembly.js +0 -884
  20. package/dist/lib/graph-expansion.js +0 -163
  21. package/dist/lib/http.js +0 -175
  22. package/dist/lib/index.js +0 -7
  23. package/dist/lib/lifecycle-maintenance.js +0 -88
  24. package/dist/lib/memory-cleanup.js +0 -455
  25. package/dist/lib/onboard.js +0 -36
  26. package/dist/lib/prompt-builder.js +0 -488
  27. package/dist/lib/remote.js +0 -166
  28. package/dist/lib/server.js +0 -3365
  29. package/dist/lib/skills.js +0 -593
  30. package/dist/lib/tui/agents.js +0 -116
  31. package/dist/lib/tui/docs.js +0 -744
  32. package/dist/lib/tui/setup.js +0 -934
  33. package/dist/lib/tui/theme.js +0 -95
  34. package/dist/lib/tui/writer.js +0 -200
@@ -1,455 +0,0 @@
1
- /**
2
- * Unified Memory Cleanup
3
- *
4
- * Orchestrates a 5-stage cleanup pipeline: prune stale drafts, consolidate
5
- * similar memories, detect orphans, detect duplicates, and backfill embeddings.
6
- *
7
- * All stages are non-fatal — individual failures are collected but never block
8
- * the remaining stages. Defaults to dry-run mode (preview only).
9
- */
10
- import { evaluateLifecycle } from "@harmony/memory";
11
- import { consolidateMemories, } from "./consolidation.js";
12
- import { findSimilarEntities } from "./graph-expansion.js";
13
- const ALL_STEPS = [
14
- "prune",
15
- "consolidate",
16
- "orphans",
17
- "duplicates",
18
- "backfill",
19
- ];
20
- const MS_PER_DAY = 1000 * 60 * 60 * 24;
21
- const MAX_ENTITIES_FETCH = 200;
22
- const DUPLICATE_SIMILARITY_THRESHOLD = 0.85;
23
- const CONCURRENCY_LIMIT = 5;
24
- // ---------------------------------------------------------------------------
25
- // Main orchestrator
26
- // ---------------------------------------------------------------------------
27
- export async function runMemoryCleanup(client, workspaceId, projectId, options) {
28
- const dryRun = options?.dryRun !== false;
29
- const steps = options?.steps ?? ALL_STEPS;
30
- const maxAgeDays = options?.maxAgeDays ?? 30;
31
- const minClusterSize = options?.minClusterSize ?? 3;
32
- const orphanAgeDays = options?.orphanAgeDays ?? 14;
33
- const report = {
34
- success: true,
35
- dryRun,
36
- timestamp: new Date().toISOString(),
37
- workspace: { id: workspaceId, projectId },
38
- summary: { totalEntities: 0, issuesFound: 0, actionsTaken: 0 },
39
- steps: {},
40
- errors: [],
41
- healthReport: "",
42
- };
43
- // Fetch all entities once (shared across steps)
44
- let entities = [];
45
- try {
46
- const listResult = await client.listMemoryEntities({
47
- workspace_id: workspaceId,
48
- project_id: projectId,
49
- limit: MAX_ENTITIES_FETCH,
50
- });
51
- entities = (listResult.entities || []);
52
- report.summary.totalEntities = entities.length;
53
- }
54
- catch (err) {
55
- report.errors.push({
56
- step: "init",
57
- message: `Failed to fetch entities: ${err.message}`,
58
- });
59
- report.success = false;
60
- report.healthReport = generateHealthReport(report);
61
- return report;
62
- }
63
- // Stage 1: Prune stale drafts
64
- if (steps.includes("prune")) {
65
- try {
66
- report.steps.prune = runPruneStep(entities, maxAgeDays);
67
- if (!dryRun) {
68
- for (const item of report.steps.prune.items) {
69
- try {
70
- await client.deleteMemoryEntity(item.id);
71
- report.steps.prune.pruned++;
72
- }
73
- catch (err) {
74
- report.errors.push({
75
- step: "prune",
76
- message: `Failed to delete ${item.id}: ${err.message}`,
77
- });
78
- }
79
- }
80
- report.summary.actionsTaken += report.steps.prune.pruned;
81
- }
82
- report.summary.issuesFound += report.steps.prune.staleDraftsFound;
83
- }
84
- catch (err) {
85
- report.errors.push({
86
- step: "prune",
87
- message: err.message,
88
- });
89
- }
90
- }
91
- // Stage 2: Consolidate similar memories
92
- if (steps.includes("consolidate")) {
93
- try {
94
- const result = await consolidateMemories(client, workspaceId, projectId, {
95
- dryRun,
96
- minClusterSize,
97
- });
98
- report.steps.consolidate = {
99
- clustersFound: result.clustersFound,
100
- entitiesProcessed: result.entitiesProcessed,
101
- consolidated: result.consolidated,
102
- details: result.details,
103
- };
104
- report.summary.issuesFound += result.clustersFound;
105
- if (!dryRun)
106
- report.summary.actionsTaken += result.consolidated;
107
- }
108
- catch (err) {
109
- report.errors.push({
110
- step: "consolidate",
111
- message: err.message,
112
- });
113
- }
114
- }
115
- // Stage 3: Detect orphans
116
- if (steps.includes("orphans")) {
117
- try {
118
- report.steps.orphans = await runOrphanStep(client, entities, orphanAgeDays);
119
- if (!dryRun) {
120
- for (const item of report.steps.orphans.items) {
121
- try {
122
- await client.deleteMemoryEntity(item.id);
123
- report.steps.orphans.removed++;
124
- }
125
- catch (err) {
126
- report.errors.push({
127
- step: "orphans",
128
- message: `Failed to delete ${item.id}: ${err.message}`,
129
- });
130
- }
131
- }
132
- report.summary.actionsTaken += report.steps.orphans.removed;
133
- }
134
- report.summary.issuesFound += report.steps.orphans.orphansFound;
135
- }
136
- catch (err) {
137
- report.errors.push({
138
- step: "orphans",
139
- message: err.message,
140
- });
141
- }
142
- }
143
- // Stage 4: Detect duplicates
144
- if (steps.includes("duplicates")) {
145
- try {
146
- report.steps.duplicates = await runDuplicateStep(client, entities, workspaceId, projectId);
147
- if (!dryRun) {
148
- for (const pair of report.steps.duplicates.pairs) {
149
- try {
150
- await client.deleteMemoryEntity(pair.removeId);
151
- report.steps.duplicates.resolved++;
152
- }
153
- catch (err) {
154
- report.errors.push({
155
- step: "duplicates",
156
- message: `Failed to delete ${pair.removeId}: ${err.message}`,
157
- });
158
- }
159
- }
160
- report.summary.actionsTaken += report.steps.duplicates.resolved;
161
- }
162
- report.summary.issuesFound += report.steps.duplicates.duplicatePairsFound;
163
- }
164
- catch (err) {
165
- report.errors.push({
166
- step: "duplicates",
167
- message: err.message,
168
- });
169
- }
170
- }
171
- // Stage 5: Backfill embeddings
172
- if (steps.includes("backfill")) {
173
- try {
174
- if (dryRun) {
175
- // In dry-run, just report that backfill would run
176
- report.steps.backfill = {
177
- processed: 0,
178
- remaining: -1,
179
- errors: [],
180
- };
181
- }
182
- else {
183
- const result = await client.backfillEmbeddings(workspaceId);
184
- report.steps.backfill = {
185
- processed: result.processed,
186
- remaining: result.remaining,
187
- errors: result.errors || [],
188
- };
189
- report.summary.actionsTaken += result.processed;
190
- }
191
- }
192
- catch (err) {
193
- report.errors.push({
194
- step: "backfill",
195
- message: err.message,
196
- });
197
- }
198
- }
199
- report.healthReport = generateHealthReport(report);
200
- return report;
201
- }
202
- // ---------------------------------------------------------------------------
203
- // Step implementations
204
- // ---------------------------------------------------------------------------
205
- function runPruneStep(entities, maxAgeDays) {
206
- const now = Date.now();
207
- const drafts = entities.filter((e) => e.memory_tier === "draft");
208
- const stale = [];
209
- for (const entity of drafts) {
210
- const ageDays = (now - new Date(entity.created_at).getTime()) / MS_PER_DAY;
211
- if (ageDays < maxAgeDays)
212
- continue;
213
- const lifecycle = evaluateLifecycle(entity);
214
- stale.push({
215
- id: entity.id,
216
- title: entity.title,
217
- ageDays: Math.round(ageDays),
218
- decayScore: Math.round(lifecycle.decay.score * 100) / 100,
219
- });
220
- }
221
- return { staleDraftsFound: stale.length, pruned: 0, items: stale };
222
- }
223
- async function runOrphanStep(client, entities, orphanAgeDays) {
224
- const now = Date.now();
225
- const result = { orphansFound: 0, removed: 0, items: [] };
226
- // Pre-filter: only check entities that look like orphan candidates
227
- const candidates = entities.filter((e) => {
228
- if (e.memory_tier === "reference")
229
- return false;
230
- if (e.access_count >= 2)
231
- return false;
232
- const ageDays = (now - new Date(e.created_at).getTime()) / MS_PER_DAY;
233
- return ageDays >= orphanAgeDays;
234
- });
235
- // Check relations in concurrent batches
236
- for (let i = 0; i < candidates.length; i += CONCURRENCY_LIMIT) {
237
- const batch = candidates.slice(i, i + CONCURRENCY_LIMIT);
238
- const results = await Promise.allSettled(batch.map(async (entity) => {
239
- const related = await client.getRelatedEntities(entity.id);
240
- const totalRelations = (related.outgoing?.length || 0) + (related.incoming?.length || 0);
241
- if (totalRelations > 0)
242
- return null;
243
- const ageDays = (now - new Date(entity.created_at).getTime()) / MS_PER_DAY;
244
- return {
245
- id: entity.id,
246
- title: entity.title,
247
- type: entity.type,
248
- tier: entity.memory_tier,
249
- ageDays: Math.round(ageDays),
250
- accessCount: entity.access_count,
251
- };
252
- }));
253
- for (const r of results) {
254
- if (r.status === "fulfilled" && r.value) {
255
- result.items.push(r.value);
256
- result.orphansFound++;
257
- }
258
- }
259
- }
260
- return result;
261
- }
262
- async function runDuplicateStep(client, entities, workspaceId, projectId) {
263
- const result = {
264
- duplicatePairsFound: 0,
265
- resolved: 0,
266
- pairs: [],
267
- };
268
- const seenPairs = new Set();
269
- const flaggedForRemoval = new Set();
270
- const entityMap = new Map(entities.map((e) => [e.id, e]));
271
- const similarityMap = new Map();
272
- for (let i = 0; i < entities.length; i += CONCURRENCY_LIMIT) {
273
- const batch = entities.slice(i, i + CONCURRENCY_LIMIT);
274
- const results = await Promise.allSettled(batch.map(async (entity) => {
275
- const similar = await findSimilarEntities(client, entity.title, entity.content, workspaceId, { projectId, limit: 5, minRrfScore: 0.05, excludeIds: [entity.id] });
276
- return { entityId: entity.id, similar };
277
- }));
278
- for (const r of results) {
279
- if (r.status === "fulfilled") {
280
- similarityMap.set(r.value.entityId, r.value.similar);
281
- }
282
- }
283
- }
284
- // Process pairs sequentially (flaggedForRemoval creates dependencies)
285
- for (const entity of entities) {
286
- if (flaggedForRemoval.has(entity.id))
287
- continue;
288
- const similar = similarityMap.get(entity.id) || [];
289
- for (const match of similar) {
290
- if (flaggedForRemoval.has(match.id))
291
- continue;
292
- const pairKey = [entity.id, match.id].sort().join(":");
293
- if (seenPairs.has(pairKey))
294
- continue;
295
- seenPairs.add(pairKey);
296
- const sim = titleSimilarity(entity.title, match.title);
297
- if (sim < DUPLICATE_SIMILARITY_THRESHOLD)
298
- continue;
299
- // Keep the one with higher confidence, more accesses, or higher tier
300
- const entityScore = entityQualityScore(entity);
301
- const matchEntity = entityMap.get(match.id);
302
- const matchScore = matchEntity
303
- ? entityQualityScore(matchEntity)
304
- : match.confidence;
305
- const [keep, remove] = entityScore >= matchScore
306
- ? [entity, { id: match.id, title: match.title }]
307
- : [{ id: match.id, title: match.title }, entity];
308
- flaggedForRemoval.add(remove.id);
309
- result.pairs.push({
310
- keepId: keep.id,
311
- keepTitle: keep.title,
312
- removeId: remove.id,
313
- removeTitle: remove.title,
314
- similarity: Math.round(sim * 100) / 100,
315
- });
316
- result.duplicatePairsFound++;
317
- }
318
- }
319
- return result;
320
- }
321
- // ---------------------------------------------------------------------------
322
- // Helpers
323
- // ---------------------------------------------------------------------------
324
- const TIER_WEIGHTS = {
325
- reference: 3,
326
- episode: 2,
327
- draft: 1,
328
- };
329
- function entityQualityScore(entity) {
330
- return (entity.confidence +
331
- (TIER_WEIGHTS[entity.memory_tier] || 0) +
332
- Math.min(entity.access_count, 10) * 0.1);
333
- }
334
- function titleSimilarity(a, b) {
335
- const na = a.toLowerCase().trim();
336
- const nb = b.toLowerCase().trim();
337
- if (na === nb)
338
- return 1;
339
- const wordsA = new Set(na.split(/\W+/).filter(Boolean));
340
- const wordsB = new Set(nb.split(/\W+/).filter(Boolean));
341
- if (wordsA.size === 0 || wordsB.size === 0)
342
- return 0;
343
- let intersection = 0;
344
- for (const w of wordsA) {
345
- if (wordsB.has(w))
346
- intersection++;
347
- }
348
- // Jaccard similarity
349
- const union = wordsA.size + wordsB.size - intersection;
350
- return union > 0 ? intersection / union : 0;
351
- }
352
- // ---------------------------------------------------------------------------
353
- // Health report renderer
354
- // ---------------------------------------------------------------------------
355
- function generateHealthReport(report) {
356
- const mode = report.dryRun ? "Dry Run (preview)" : "Executed";
357
- const lines = [
358
- "# Memory Health Report\n",
359
- `**Mode:** ${mode} | **Entities:** ${report.summary.totalEntities} | **Issues:** ${report.summary.issuesFound} | **Actions:** ${report.summary.actionsTaken}`,
360
- "",
361
- ];
362
- if (report.summary.totalEntities >= MAX_ENTITIES_FETCH) {
363
- lines.push(`> **Note:** Entity count hit the ${MAX_ENTITIES_FETCH} fetch limit. Some entities may not have been analyzed.\n`);
364
- }
365
- // Prune
366
- if (report.steps.prune) {
367
- const p = report.steps.prune;
368
- lines.push("## Stale Drafts");
369
- if (p.staleDraftsFound === 0) {
370
- lines.push("No stale drafts found.\n");
371
- }
372
- else {
373
- lines.push(`Found **${p.staleDraftsFound}** stale drafts${!report.dryRun ? ` (pruned ${p.pruned})` : ""}:`);
374
- lines.push("| Title | Age | Decay |");
375
- lines.push("|-------|-----|-------|");
376
- for (const item of p.items.slice(0, 20)) {
377
- lines.push(`| ${item.title} | ${item.ageDays}d | ${item.decayScore} |`);
378
- }
379
- lines.push("");
380
- }
381
- }
382
- // Consolidate
383
- if (report.steps.consolidate) {
384
- const c = report.steps.consolidate;
385
- lines.push("## Consolidation");
386
- if (c.clustersFound === 0) {
387
- lines.push(`Scanned ${c.entitiesProcessed} draft/episode entities — no clusters found.\n`);
388
- }
389
- else {
390
- lines.push(`Found **${c.clustersFound}** clusters across ${c.entitiesProcessed} entities:`);
391
- for (const d of c.details.slice(0, 10)) {
392
- lines.push(`- **${d.mergedTitle}** — ${d.clusterSize} entities`);
393
- }
394
- lines.push("");
395
- }
396
- }
397
- // Orphans
398
- if (report.steps.orphans) {
399
- const o = report.steps.orphans;
400
- lines.push("## Orphaned Entities");
401
- if (o.orphansFound === 0) {
402
- lines.push("No orphans found.\n");
403
- }
404
- else {
405
- lines.push(`Found **${o.orphansFound}** orphans${!report.dryRun ? ` (removed ${o.removed})` : ""}:`);
406
- lines.push("| Title | Type | Tier | Age | Accesses |");
407
- lines.push("|-------|------|------|-----|----------|");
408
- for (const item of o.items.slice(0, 20)) {
409
- lines.push(`| ${item.title} | ${item.type} | ${item.tier} | ${item.ageDays}d | ${item.accessCount} |`);
410
- }
411
- lines.push("");
412
- }
413
- }
414
- // Duplicates
415
- if (report.steps.duplicates) {
416
- const d = report.steps.duplicates;
417
- lines.push("## Near-Duplicates");
418
- if (d.duplicatePairsFound === 0) {
419
- lines.push("No duplicates found.\n");
420
- }
421
- else {
422
- lines.push(`Found **${d.duplicatePairsFound}** duplicate pairs${!report.dryRun ? ` (resolved ${d.resolved})` : ""}:`);
423
- for (const pair of d.pairs.slice(0, 20)) {
424
- lines.push(`- "${pair.keepTitle}" ~ "${pair.removeTitle}" (${Math.round(pair.similarity * 100)}% similar, keep first)`);
425
- }
426
- lines.push("");
427
- }
428
- }
429
- // Backfill
430
- if (report.steps.backfill) {
431
- const b = report.steps.backfill;
432
- lines.push("## Embedding Coverage");
433
- if (report.dryRun) {
434
- lines.push("Backfill will run when executed with `dryRun: false`.\n");
435
- }
436
- else if (b.remaining === 0) {
437
- lines.push(`All embeddings up to date (processed ${b.processed}).\n`);
438
- }
439
- else {
440
- lines.push(`Processed ${b.processed} entities. ${b.remaining} still need embeddings.\n`);
441
- }
442
- }
443
- // Errors
444
- if (report.errors.length > 0) {
445
- lines.push("## Errors");
446
- for (const e of report.errors) {
447
- lines.push(`- **${e.step}:** ${e.message}`);
448
- }
449
- lines.push("");
450
- }
451
- if (report.dryRun) {
452
- lines.push("---\n*Run with `dryRun: false` to execute cleanup.*");
453
- }
454
- return lines.join("\n");
455
- }
@@ -1,36 +0,0 @@
1
- import { requestWithBearer, signupUser } from "./api-client.js";
2
- import { getApiUrl } from "./config.js";
3
- export async function onboardNewUser(params) {
4
- const { email, password, fullName, workspaceName = `${fullName}'s Workspace`, projectName = "My First Board", template = "kanban", keyName = "mcp-agent", apiUrl = getApiUrl(), } = params;
5
- // 1. Signup
6
- const signupResult = await signupUser(apiUrl, {
7
- email,
8
- password,
9
- full_name: fullName,
10
- });
11
- const token = signupResult.session.access_token;
12
- // 2. Create workspace
13
- const workspaceResult = await requestWithBearer(apiUrl, token, "POST", "/workspaces", {
14
- name: workspaceName,
15
- });
16
- // 3. Create project
17
- const projectResult = await requestWithBearer(apiUrl, token, "POST", "/projects", {
18
- workspaceId: workspaceResult.workspace.id,
19
- name: projectName,
20
- template,
21
- });
22
- // 4. Generate API key
23
- const keyResult = await requestWithBearer(apiUrl, token, "POST", "/api-keys", {
24
- name: keyName,
25
- });
26
- return {
27
- user: signupResult.user,
28
- workspace: workspaceResult.workspace,
29
- project: projectResult.project,
30
- columns: projectResult.columns,
31
- apiKey: {
32
- rawKey: keyResult.rawKey,
33
- prefix: keyResult.apiKey.prefix,
34
- },
35
- };
36
- }