@gethmy/mcp 2.3.0 → 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 (38) hide show
  1. package/dist/cli.js +80 -23
  2. package/dist/index.js +80 -23
  3. package/dist/lib/active-learning.js +939 -787
  4. package/dist/lib/api-client.js +2527 -638
  5. package/dist/lib/auto-session.js +177 -196
  6. package/dist/lib/cli.js +34954 -128
  7. package/dist/lib/config.js +235 -201
  8. package/dist/lib/consolidation.js +374 -289
  9. package/dist/lib/context-assembly.js +1265 -838
  10. package/dist/lib/graph-expansion.js +139 -155
  11. package/dist/lib/http.js +1917 -130
  12. package/dist/lib/index.js +29525 -5
  13. package/dist/lib/lifecycle-maintenance.js +663 -79
  14. package/dist/lib/memory-cleanup.js +1316 -381
  15. package/dist/lib/onboard.js +2588 -32
  16. package/dist/lib/prompt-builder.js +438 -445
  17. package/dist/lib/remote.js +31733 -143
  18. package/dist/lib/server.js +29389 -3216
  19. package/dist/lib/skills.js +315 -132
  20. package/dist/lib/tui/agents.js +128 -107
  21. package/dist/lib/tui/docs.js +1590 -687
  22. package/dist/lib/tui/setup.js +5698 -804
  23. package/dist/lib/tui/theme.js +183 -86
  24. package/dist/lib/tui/writer.js +1149 -176
  25. package/package.json +2 -2
  26. package/src/api-client.ts +37 -1
  27. package/src/memory-cleanup.ts +92 -52
  28. package/src/server.ts +16 -1
  29. package/dist/lib/__tests__/active-learning.test.js +0 -386
  30. package/dist/lib/__tests__/agent-performance-profiles.test.js +0 -325
  31. package/dist/lib/__tests__/auto-session.test.js +0 -661
  32. package/dist/lib/__tests__/context-assembly.test.js +0 -362
  33. package/dist/lib/__tests__/graph-expansion.test.js +0 -150
  34. package/dist/lib/__tests__/integration-memory-crud.test.js +0 -797
  35. package/dist/lib/__tests__/integration-memory-system.test.js +0 -281
  36. package/dist/lib/__tests__/lifecycle-maintenance.test.js +0 -207
  37. package/dist/lib/__tests__/pattern-detection.test.js +0 -295
  38. package/dist/lib/__tests__/prompt-builder.test.js +0 -418
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -50,7 +50,7 @@
50
50
  "bun": ">=1.0.0"
51
51
  },
52
52
  "scripts": {
53
- "build": "bun build src/index.ts src/cli.ts --outdir dist --target node && tsc --outDir dist/lib --declaration false --skipLibCheck --noCheck",
53
+ "build": "bun build src/index.ts src/cli.ts --outdir dist --target node && bun build src/*.ts src/tui/*.ts --outdir dist/lib --root src --target node --external bun",
54
54
  "build:bun": "bun build src/index.ts src/http.ts src/remote.ts src/cli.ts --outdir dist --target bun",
55
55
  "serve:remote": "bun src/remote.ts",
56
56
  "dev": "bun --watch src/index.ts",
package/src/api-client.ts CHANGED
@@ -502,7 +502,6 @@ export class HarmonyApiClient {
502
502
  phase?: string;
503
503
  filesChanged?: number;
504
504
  costCents?: number;
505
- recentActions?: { action: string; ts: string }[];
506
505
  },
507
506
  ): Promise<{ session: unknown; created: boolean }> {
508
507
  return this.request("POST", `/cards/${cardId}/agent-context`, data);
@@ -518,6 +517,43 @@ export class HarmonyApiClient {
518
517
  return this.request("DELETE", `/cards/${cardId}/agent-context`, data);
519
518
  }
520
519
 
520
+ async flushActivityLog(
521
+ cardId: string,
522
+ data: {
523
+ sessionId: string;
524
+ entries: {
525
+ phase?: string;
526
+ eventType: string;
527
+ toolName?: string;
528
+ description: string;
529
+ metadata?: Record<string, unknown>;
530
+ createdAt?: string;
531
+ }[];
532
+ },
533
+ ): Promise<{ inserted: number }> {
534
+ return this.request("POST", `/cards/${cardId}/agent-activity-log`, data);
535
+ }
536
+
537
+ async getActivityLog(
538
+ cardId: string,
539
+ sessionId: string,
540
+ ): Promise<{
541
+ entries: {
542
+ id: string;
543
+ phase: string | null;
544
+ eventType: string;
545
+ toolName: string | null;
546
+ description: string;
547
+ metadata: Record<string, unknown>;
548
+ createdAt: string;
549
+ }[];
550
+ }> {
551
+ return this.request(
552
+ "GET",
553
+ `/cards/${cardId}/agent-activity-log?sessionId=${sessionId}`,
554
+ );
555
+ }
556
+
521
557
  async getAgentSession(
522
558
  cardId: string,
523
559
  options?: { includeEnded?: boolean },
@@ -130,6 +130,11 @@ const ALL_STEPS: CleanupStep[] = [
130
130
  "backfill",
131
131
  ];
132
132
 
133
+ const MS_PER_DAY = 1000 * 60 * 60 * 24;
134
+ const MAX_ENTITIES_FETCH = 200;
135
+ const DUPLICATE_SIMILARITY_THRESHOLD = 0.85;
136
+ const CONCURRENCY_LIMIT = 5;
137
+
133
138
  // ---------------------------------------------------------------------------
134
139
  // Main orchestrator
135
140
  // ---------------------------------------------------------------------------
@@ -163,7 +168,7 @@ export async function runMemoryCleanup(
163
168
  const listResult = await client.listMemoryEntities({
164
169
  workspace_id: workspaceId,
165
170
  project_id: projectId,
166
- limit: 200,
171
+ limit: MAX_ENTITIES_FETCH,
167
172
  });
168
173
  entities = (listResult.entities || []) as MemoryEntity[];
169
174
  report.summary.totalEntities = entities.length;
@@ -186,8 +191,11 @@ export async function runMemoryCleanup(
186
191
  try {
187
192
  await client.deleteMemoryEntity(item.id);
188
193
  report.steps.prune.pruned++;
189
- } catch {
190
- // Non-fatal
194
+ } catch (err) {
195
+ report.errors.push({
196
+ step: "prune",
197
+ message: `Failed to delete ${item.id}: ${(err as Error).message}`,
198
+ });
191
199
  }
192
200
  }
193
201
  report.summary.actionsTaken += report.steps.prune.pruned;
@@ -237,8 +245,11 @@ export async function runMemoryCleanup(
237
245
  try {
238
246
  await client.deleteMemoryEntity(item.id);
239
247
  report.steps.orphans.removed++;
240
- } catch {
241
- // Non-fatal
248
+ } catch (err) {
249
+ report.errors.push({
250
+ step: "orphans",
251
+ message: `Failed to delete ${item.id}: ${(err as Error).message}`,
252
+ });
242
253
  }
243
254
  }
244
255
  report.summary.actionsTaken += report.steps.orphans.removed;
@@ -266,8 +277,11 @@ export async function runMemoryCleanup(
266
277
  try {
267
278
  await client.deleteMemoryEntity(pair.removeId);
268
279
  report.steps.duplicates.resolved++;
269
- } catch {
270
- // Non-fatal
280
+ } catch (err) {
281
+ report.errors.push({
282
+ step: "duplicates",
283
+ message: `Failed to delete ${pair.removeId}: ${(err as Error).message}`,
284
+ });
271
285
  }
272
286
  }
273
287
  report.summary.actionsTaken += report.steps.duplicates.resolved;
@@ -325,8 +339,7 @@ function runPruneStep(
325
339
  const stale: PruneStepResult["items"] = [];
326
340
 
327
341
  for (const entity of drafts) {
328
- const ageDays =
329
- (now - new Date(entity.created_at).getTime()) / (1000 * 60 * 60 * 24);
342
+ const ageDays = (now - new Date(entity.created_at).getTime()) / MS_PER_DAY;
330
343
  if (ageDays < maxAgeDays) continue;
331
344
 
332
345
  const lifecycle = evaluateLifecycle(entity);
@@ -353,31 +366,38 @@ async function runOrphanStep(
353
366
  const candidates = entities.filter((e) => {
354
367
  if (e.memory_tier === "reference") return false;
355
368
  if (e.access_count >= 2) return false;
356
- const ageDays =
357
- (now - new Date(e.created_at).getTime()) / (1000 * 60 * 60 * 24);
369
+ const ageDays = (now - new Date(e.created_at).getTime()) / MS_PER_DAY;
358
370
  return ageDays >= orphanAgeDays;
359
371
  });
360
372
 
361
- for (const entity of candidates) {
362
- try {
363
- const related = await client.getRelatedEntities(entity.id);
364
- const totalRelations =
365
- (related.outgoing?.length || 0) + (related.incoming?.length || 0);
366
- if (totalRelations > 0) continue;
367
-
368
- const ageDays =
369
- (now - new Date(entity.created_at).getTime()) / (1000 * 60 * 60 * 24);
370
- result.items.push({
371
- id: entity.id,
372
- title: entity.title,
373
- type: entity.type,
374
- tier: entity.memory_tier,
375
- ageDays: Math.round(ageDays),
376
- accessCount: entity.access_count,
377
- });
378
- result.orphansFound++;
379
- } catch {
380
- // Non-fatal: skip this entity
373
+ // Check relations in concurrent batches
374
+ for (let i = 0; i < candidates.length; i += CONCURRENCY_LIMIT) {
375
+ const batch = candidates.slice(i, i + CONCURRENCY_LIMIT);
376
+ const results = await Promise.allSettled(
377
+ batch.map(async (entity) => {
378
+ const related = await client.getRelatedEntities(entity.id);
379
+ const totalRelations =
380
+ (related.outgoing?.length || 0) + (related.incoming?.length || 0);
381
+ if (totalRelations > 0) return null;
382
+
383
+ const ageDays =
384
+ (now - new Date(entity.created_at).getTime()) / MS_PER_DAY;
385
+ return {
386
+ id: entity.id,
387
+ title: entity.title,
388
+ type: entity.type,
389
+ tier: entity.memory_tier,
390
+ ageDays: Math.round(ageDays),
391
+ accessCount: entity.access_count,
392
+ };
393
+ }),
394
+ );
395
+
396
+ for (const r of results) {
397
+ if (r.status === "fulfilled" && r.value) {
398
+ result.items.push(r.value);
399
+ result.orphansFound++;
400
+ }
381
401
  }
382
402
  }
383
403
 
@@ -398,28 +418,42 @@ async function runDuplicateStep(
398
418
 
399
419
  const seenPairs = new Set<string>();
400
420
  const flaggedForRemoval = new Set<string>();
421
+ const entityMap = new Map(entities.map((e) => [e.id, e]));
401
422
 
423
+ // Pre-fetch similarities in concurrent batches
424
+ type SimilarMatch = {
425
+ id: string;
426
+ type: string;
427
+ title: string;
428
+ content: string;
429
+ confidence: number;
430
+ };
431
+ const similarityMap = new Map<string, SimilarMatch[]>();
432
+ for (let i = 0; i < entities.length; i += CONCURRENCY_LIMIT) {
433
+ const batch = entities.slice(i, i + CONCURRENCY_LIMIT);
434
+ const results = await Promise.allSettled(
435
+ batch.map(async (entity) => {
436
+ const similar = await findSimilarEntities(
437
+ client,
438
+ entity.title,
439
+ entity.content,
440
+ workspaceId,
441
+ { projectId, limit: 5, minRrfScore: 0.05, excludeIds: [entity.id] },
442
+ );
443
+ return { entityId: entity.id, similar };
444
+ }),
445
+ );
446
+ for (const r of results) {
447
+ if (r.status === "fulfilled") {
448
+ similarityMap.set(r.value.entityId, r.value.similar);
449
+ }
450
+ }
451
+ }
452
+
453
+ // Process pairs sequentially (flaggedForRemoval creates dependencies)
402
454
  for (const entity of entities) {
403
455
  if (flaggedForRemoval.has(entity.id)) continue;
404
-
405
- let similar: Array<{
406
- id: string;
407
- type: string;
408
- title: string;
409
- content: string;
410
- confidence: number;
411
- }>;
412
- try {
413
- similar = await findSimilarEntities(
414
- client,
415
- entity.title,
416
- entity.content,
417
- workspaceId,
418
- { projectId, limit: 5, minRrfScore: 0.05, excludeIds: [entity.id] },
419
- );
420
- } catch {
421
- continue;
422
- }
456
+ const similar = similarityMap.get(entity.id) || [];
423
457
 
424
458
  for (const match of similar) {
425
459
  if (flaggedForRemoval.has(match.id)) continue;
@@ -429,11 +463,11 @@ async function runDuplicateStep(
429
463
  seenPairs.add(pairKey);
430
464
 
431
465
  const sim = titleSimilarity(entity.title, match.title);
432
- if (sim < 0.85) continue;
466
+ if (sim < DUPLICATE_SIMILARITY_THRESHOLD) continue;
433
467
 
434
468
  // Keep the one with higher confidence, more accesses, or higher tier
435
469
  const entityScore = entityQualityScore(entity);
436
- const matchEntity = entities.find((e) => e.id === match.id);
470
+ const matchEntity = entityMap.get(match.id);
437
471
  const matchScore = matchEntity
438
472
  ? entityQualityScore(matchEntity)
439
473
  : match.confidence;
@@ -506,6 +540,12 @@ function generateHealthReport(report: CleanupReport): string {
506
540
  "",
507
541
  ];
508
542
 
543
+ if (report.summary.totalEntities >= MAX_ENTITIES_FETCH) {
544
+ lines.push(
545
+ `> **Note:** Entity count hit the ${MAX_ENTITIES_FETCH} fetch limit. Some entities may not have been analyzed.\n`,
546
+ );
547
+ }
548
+
509
549
  // Prune
510
550
  if (report.steps.prune) {
511
551
  const p = report.steps.prune;
package/src/server.ts CHANGED
@@ -4024,9 +4024,24 @@ async function handleToolCall(
4024
4024
  const projectId =
4025
4025
  (args.projectId as string) || deps.getActiveProjectId() || undefined;
4026
4026
 
4027
+ const validSteps = [
4028
+ "prune",
4029
+ "consolidate",
4030
+ "orphans",
4031
+ "duplicates",
4032
+ "backfill",
4033
+ ];
4034
+ const rawSteps = args.steps as string[] | undefined;
4035
+ const steps = rawSteps?.filter((s) => validSteps.includes(s));
4036
+ if (rawSteps && steps && steps.length < rawSteps.length) {
4037
+ const invalid = rawSteps.filter((s) => !validSteps.includes(s));
4038
+ // Will appear in report.errors via the healthReport
4039
+ console.warn(`Unknown cleanup steps ignored: ${invalid.join(", ")}`);
4040
+ }
4041
+
4027
4042
  const report = await runMemoryCleanup(client, workspaceId, projectId, {
4028
4043
  dryRun: args.dryRun as boolean | undefined,
4029
- steps: args.steps as string[] | undefined,
4044
+ steps,
4030
4045
  maxAgeDays: args.maxAgeDays as number | undefined,
4031
4046
  minClusterSize: args.minClusterSize as number | undefined,
4032
4047
  orphanAgeDays: args.orphanAgeDays as number | undefined,