@gethmy/mcp 2.3.4 → 2.4.0

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/index.js CHANGED
@@ -25823,6 +25823,334 @@ async function runLifecycleMaintenance(client3, workspaceId, projectId) {
25823
25823
  return result;
25824
25824
  }
25825
25825
 
25826
+ // src/memory-audit.ts
25827
+ init_dist();
25828
+ var EMBEDDINGS_MIGRATION_AT = Date.parse("2026-02-18T00:00:00Z");
25829
+ var MS_PER_DAY = 1000 * 60 * 60 * 24;
25830
+ var BATCH_SIZE = 100;
25831
+ var CONCURRENCY_LIMIT = 5;
25832
+ var BOILERPLATE_PATTERNS = [
25833
+ /^todo:?$/i,
25834
+ /^placeholder/i,
25835
+ /^\.\.\.$/,
25836
+ /^untitled/i,
25837
+ /^(note|memo|draft)\s*\d*$/i
25838
+ ];
25839
+ function isBoilerplate(title, content) {
25840
+ const t = title.trim();
25841
+ const c = content.trim();
25842
+ if (c.length === 0)
25843
+ return true;
25844
+ for (const pat of BOILERPLATE_PATTERNS) {
25845
+ if (pat.test(t))
25846
+ return true;
25847
+ }
25848
+ return false;
25849
+ }
25850
+ function scoreEntity(entity, relationCount, archiveBelow, deleteBelow) {
25851
+ const now = Date.now();
25852
+ const ageDays = (now - Date.parse(entity.created_at)) / MS_PER_DAY;
25853
+ const effectiveLastAccess = entity.last_accessed_at ?? entity.created_at;
25854
+ const lifecycle2 = evaluateLifecycle({
25855
+ memory_tier: entity.memory_tier,
25856
+ confidence: entity.confidence,
25857
+ access_count: entity.access_count,
25858
+ last_accessed_at: effectiveLastAccess,
25859
+ created_at: entity.created_at
25860
+ });
25861
+ const reasons = [];
25862
+ const legacyReasons = [];
25863
+ const confidence = Math.max(0, Math.min(1, entity.confidence)) * 25;
25864
+ const decay = Math.max(0, Math.min(1, lifecycle2.decay.score)) * 20;
25865
+ if (lifecycle2.decay.score < 0.2)
25866
+ reasons.push(`decay score ${lifecycle2.decay.score.toFixed(2)}`);
25867
+ const hasEmbedding = entity.embedding != null;
25868
+ const hasTags = (entity.tags?.length || 0) >= 1;
25869
+ const hasRelations = relationCount > 0;
25870
+ let structural = 0;
25871
+ if (hasEmbedding)
25872
+ structural += 6;
25873
+ if (hasTags)
25874
+ structural += 4;
25875
+ if (hasRelations)
25876
+ structural += 5;
25877
+ if (!hasEmbedding)
25878
+ reasons.push("no embedding");
25879
+ if (!hasTags)
25880
+ reasons.push("no tags");
25881
+ if (!hasRelations)
25882
+ reasons.push("no relations");
25883
+ let content = 0;
25884
+ const contentLen = entity.content?.length || 0;
25885
+ if (contentLen >= 80)
25886
+ content += 8;
25887
+ const titleOk = entity.title.trim().length >= 4 && !/^(untitled|draft|note)\b/i.test(entity.title.trim());
25888
+ if (titleOk)
25889
+ content += 4;
25890
+ if (!isBoilerplate(entity.title, entity.content))
25891
+ content += 3;
25892
+ if (contentLen < 80)
25893
+ reasons.push(`thin content (${contentLen} chars)`);
25894
+ if (isBoilerplate(entity.title, entity.content))
25895
+ reasons.push("boilerplate title/content");
25896
+ let tierAgeFit = 15;
25897
+ if (entity.memory_tier === "draft" && ageDays > 60 && !entity.promoted_from_id) {
25898
+ tierAgeFit = 0;
25899
+ reasons.push("stuck draft >60d never promoted");
25900
+ }
25901
+ if (entity.promoted_from_id) {
25902
+ tierAgeFit = Math.min(15, tierAgeFit + 5);
25903
+ }
25904
+ const access = Math.min(10, Math.log10((entity.access_count || 0) + 1) * 5);
25905
+ if (entity.access_count === 0 && ageDays > 14)
25906
+ reasons.push("never accessed");
25907
+ const raw = confidence + decay + structural + content + tierAgeFit + access;
25908
+ const score = Math.round(Math.max(0, Math.min(100, raw)));
25909
+ let legacy = false;
25910
+ if (entity.confidence === 1 && entity.access_count === 0 && ageDays > 30) {
25911
+ legacy = true;
25912
+ legacyReasons.push("default confidence never validated");
25913
+ }
25914
+ if (!hasEmbedding && Date.parse(entity.created_at) < EMBEDDINGS_MIGRATION_AT) {
25915
+ legacy = true;
25916
+ legacyReasons.push("pre-embeddings migration");
25917
+ }
25918
+ if (entity.memory_tier === "draft" && ageDays > 60 && !entity.promoted_from_id) {
25919
+ legacy = true;
25920
+ legacyReasons.push("stuck draft");
25921
+ }
25922
+ if (!hasTags && !hasRelations) {
25923
+ legacy = true;
25924
+ legacyReasons.push("no graph presence");
25925
+ }
25926
+ let bucket;
25927
+ if (score < deleteBelow)
25928
+ bucket = "delete";
25929
+ else if (score < archiveBelow)
25930
+ bucket = "archive";
25931
+ else if (score < 70)
25932
+ bucket = "review";
25933
+ else
25934
+ bucket = "keep";
25935
+ return {
25936
+ id: entity.id,
25937
+ title: entity.title,
25938
+ type: entity.type,
25939
+ tier: entity.memory_tier,
25940
+ ageDays: Math.round(ageDays),
25941
+ score,
25942
+ bucket,
25943
+ reasons,
25944
+ legacy,
25945
+ legacyReasons,
25946
+ subScores: {
25947
+ confidence: Math.round(confidence),
25948
+ decay: Math.round(decay),
25949
+ structural,
25950
+ content,
25951
+ tierAgeFit,
25952
+ access: Math.round(access)
25953
+ }
25954
+ };
25955
+ }
25956
+ async function runMemoryAudit(client3, workspaceId, projectId, options) {
25957
+ const dryRun = options?.dryRun !== false;
25958
+ const archiveBelow = options?.archiveBelow ?? 40;
25959
+ const deleteBelow = options?.deleteBelow ?? 20;
25960
+ const limit = options?.limit ?? 500;
25961
+ const report = {
25962
+ success: true,
25963
+ dryRun,
25964
+ timestamp: new Date().toISOString(),
25965
+ workspace: { id: workspaceId, projectId },
25966
+ summary: {
25967
+ totalEntities: 0,
25968
+ scanned: 0,
25969
+ keep: 0,
25970
+ review: 0,
25971
+ archive: 0,
25972
+ delete: 0,
25973
+ legacyCount: 0
25974
+ },
25975
+ actionsTaken: { flaggedReview: 0, archived: 0, deleted: 0 },
25976
+ distribution: { "0-20": 0, "20-40": 0, "40-70": 0, "70-100": 0 },
25977
+ legacyBreakdown: {
25978
+ defaultConfidence: 0,
25979
+ missingEmbedding: 0,
25980
+ stuckDraft: 0,
25981
+ noGraphPresence: 0
25982
+ },
25983
+ lowest: [],
25984
+ errors: [],
25985
+ healthReport: ""
25986
+ };
25987
+ const entities = [];
25988
+ let offset = 0;
25989
+ try {
25990
+ while (entities.length < limit) {
25991
+ const pageSize = Math.min(BATCH_SIZE, limit - entities.length);
25992
+ const result = await client3.listMemoryEntities({
25993
+ workspace_id: workspaceId,
25994
+ project_id: projectId,
25995
+ limit: pageSize,
25996
+ offset
25997
+ });
25998
+ const page = result.entities || [];
25999
+ if (page.length === 0)
26000
+ break;
26001
+ entities.push(...page);
26002
+ if (page.length < pageSize)
26003
+ break;
26004
+ offset += pageSize;
26005
+ }
26006
+ } catch (err) {
26007
+ report.errors.push({
26008
+ step: "fetch",
26009
+ message: `Failed to fetch entities: ${err.message}`
26010
+ });
26011
+ report.success = false;
26012
+ report.healthReport = renderReport(report);
26013
+ return report;
26014
+ }
26015
+ report.summary.totalEntities = entities.length;
26016
+ const relationCounts = new Map;
26017
+ for (let i = 0;i < entities.length; i += CONCURRENCY_LIMIT) {
26018
+ const batch = entities.slice(i, i + CONCURRENCY_LIMIT);
26019
+ const results = await Promise.allSettled(batch.map(async (e) => {
26020
+ const related = await client3.getRelatedEntities(e.id);
26021
+ const count = (related.outgoing?.length || 0) + (related.incoming?.length || 0);
26022
+ return { id: e.id, count };
26023
+ }));
26024
+ for (const r of results) {
26025
+ if (r.status === "fulfilled") {
26026
+ relationCounts.set(r.value.id, r.value.count);
26027
+ }
26028
+ }
26029
+ }
26030
+ const audits = [];
26031
+ for (const entity of entities) {
26032
+ const relCount = relationCounts.get(entity.id) ?? 0;
26033
+ const audit = scoreEntity(entity, relCount, archiveBelow, deleteBelow);
26034
+ audits.push(audit);
26035
+ report.summary.scanned++;
26036
+ report.summary[audit.bucket]++;
26037
+ if (audit.legacy)
26038
+ report.summary.legacyCount++;
26039
+ if (audit.score < 20)
26040
+ report.distribution["0-20"]++;
26041
+ else if (audit.score < 40)
26042
+ report.distribution["20-40"]++;
26043
+ else if (audit.score < 70)
26044
+ report.distribution["40-70"]++;
26045
+ else
26046
+ report.distribution["70-100"]++;
26047
+ for (const reason of audit.legacyReasons) {
26048
+ if (reason.startsWith("default confidence"))
26049
+ report.legacyBreakdown.defaultConfidence++;
26050
+ else if (reason.startsWith("pre-embeddings"))
26051
+ report.legacyBreakdown.missingEmbedding++;
26052
+ else if (reason.startsWith("stuck draft"))
26053
+ report.legacyBreakdown.stuckDraft++;
26054
+ else if (reason.startsWith("no graph"))
26055
+ report.legacyBreakdown.noGraphPresence++;
26056
+ }
26057
+ }
26058
+ report.lowest = [...audits].sort((a, b) => a.score - b.score).slice(0, 10);
26059
+ if (!dryRun) {
26060
+ for (const audit of audits) {
26061
+ try {
26062
+ if (audit.bucket === "delete") {
26063
+ await client3.deleteMemoryEntity(audit.id);
26064
+ report.actionsTaken.deleted++;
26065
+ } else if (audit.bucket === "archive") {
26066
+ await client3.updateMemoryEntity(audit.id, {
26067
+ confidence: 0.25,
26068
+ metadata: {
26069
+ audit_archived_at: new Date().toISOString(),
26070
+ audit_score: audit.score,
26071
+ audit_reasons: audit.reasons
26072
+ }
26073
+ });
26074
+ report.actionsTaken.archived++;
26075
+ } else if (audit.bucket === "review") {
26076
+ await client3.updateMemoryEntity(audit.id, {
26077
+ metadata: {
26078
+ needs_review: true,
26079
+ audit_score: audit.score,
26080
+ audit_reasons: audit.reasons,
26081
+ audit_at: new Date().toISOString()
26082
+ }
26083
+ });
26084
+ report.actionsTaken.flaggedReview++;
26085
+ }
26086
+ } catch (err) {
26087
+ report.errors.push({
26088
+ entityId: audit.id,
26089
+ step: audit.bucket,
26090
+ message: err.message
26091
+ });
26092
+ }
26093
+ }
26094
+ }
26095
+ report.healthReport = renderReport(report);
26096
+ return report;
26097
+ }
26098
+ function renderReport(report) {
26099
+ const mode = report.dryRun ? "Dry Run (preview)" : "Executed";
26100
+ const s = report.summary;
26101
+ const lines = [
26102
+ `# Memory Quality Audit
26103
+ `,
26104
+ `**Mode:** ${mode} | **Scanned:** ${s.scanned}/${s.totalEntities} | **Legacy:** ${s.legacyCount}`,
26105
+ "",
26106
+ "## Distribution",
26107
+ `- 70-100 (keep): ${report.distribution["70-100"]}`,
26108
+ `- 40-69 (review): ${report.distribution["40-70"]}`,
26109
+ `- 20-39 (archive): ${report.distribution["20-40"]}`,
26110
+ `- 0-19 (delete): ${report.distribution["0-20"]}`,
26111
+ "",
26112
+ "## Buckets",
26113
+ `- **Keep:** ${s.keep}`,
26114
+ `- **Review:** ${s.review}${!report.dryRun ? ` (flagged ${report.actionsTaken.flaggedReview})` : ""}`,
26115
+ `- **Archive:** ${s.archive}${!report.dryRun ? ` (archived ${report.actionsTaken.archived})` : ""}`,
26116
+ `- **Delete:** ${s.delete}${!report.dryRun ? ` (deleted ${report.actionsTaken.deleted})` : ""}`,
26117
+ ""
26118
+ ];
26119
+ const l = report.legacyBreakdown;
26120
+ if (s.legacyCount > 0) {
26121
+ lines.push("## Legacy Breakdown");
26122
+ lines.push(`- Default confidence, never validated: ${l.defaultConfidence}`);
26123
+ lines.push(`- Pre-embeddings migration: ${l.missingEmbedding}`);
26124
+ lines.push(`- Stuck drafts (>60d, no promotion): ${l.stuckDraft}`);
26125
+ lines.push(`- No tags + no relations: ${l.noGraphPresence}`);
26126
+ lines.push("");
26127
+ }
26128
+ if (report.lowest.length > 0) {
26129
+ lines.push("## Lowest-Scoring (top 10)");
26130
+ lines.push("| Score | Bucket | Tier | Age | Title | Reasons |");
26131
+ lines.push("|-------|--------|------|-----|-------|---------|");
26132
+ for (const a of report.lowest) {
26133
+ const reasonStr = a.reasons.slice(0, 3).join(", ") || "—";
26134
+ const titleTrunc = a.title.length > 40 ? `${a.title.slice(0, 37)}...` : a.title;
26135
+ lines.push(`| ${a.score} | ${a.bucket} | ${a.tier} | ${a.ageDays}d | ${titleTrunc} | ${reasonStr} |`);
26136
+ }
26137
+ lines.push("");
26138
+ }
26139
+ if (report.errors.length > 0) {
26140
+ lines.push("## Errors");
26141
+ for (const e of report.errors.slice(0, 10)) {
26142
+ lines.push(`- **${e.step}${e.entityId ? ` ${e.entityId}` : ""}:** ${e.message}`);
26143
+ }
26144
+ lines.push("");
26145
+ }
26146
+ if (report.dryRun) {
26147
+ lines.push("---");
26148
+ lines.push("*Run with `dryRun: false` to flag review entries, archive low-quality memories, and delete worst offenders.*");
26149
+ }
26150
+ return lines.join(`
26151
+ `);
26152
+ }
26153
+
25826
26154
  // src/memory-cleanup.ts
25827
26155
  init_dist();
25828
26156
  var ALL_STEPS = [
@@ -25830,12 +26158,13 @@ var ALL_STEPS = [
25830
26158
  "consolidate",
25831
26159
  "orphans",
25832
26160
  "duplicates",
25833
- "backfill"
26161
+ "backfill",
26162
+ "audit"
25834
26163
  ];
25835
- var MS_PER_DAY = 1000 * 60 * 60 * 24;
26164
+ var MS_PER_DAY2 = 1000 * 60 * 60 * 24;
25836
26165
  var MAX_ENTITIES_FETCH = 200;
25837
26166
  var DUPLICATE_SIMILARITY_THRESHOLD = 0.85;
25838
- var CONCURRENCY_LIMIT = 5;
26167
+ var CONCURRENCY_LIMIT2 = 5;
25839
26168
  async function runMemoryCleanup(client3, workspaceId, projectId, options) {
25840
26169
  const dryRun = options?.dryRun !== false;
25841
26170
  const steps = options?.steps ?? ALL_STEPS;
@@ -25991,6 +26320,44 @@ async function runMemoryCleanup(client3, workspaceId, projectId, options) {
25991
26320
  });
25992
26321
  }
25993
26322
  }
26323
+ if (steps.includes("audit")) {
26324
+ try {
26325
+ const auditReport = await runMemoryAudit(client3, workspaceId, projectId, {
26326
+ dryRun,
26327
+ archiveBelow: options?.auditArchiveBelow,
26328
+ deleteBelow: options?.auditDeleteBelow
26329
+ });
26330
+ const low = auditReport.lowest.length > 0 ? auditReport.lowest[0].score : null;
26331
+ report.steps.audit = {
26332
+ scanned: auditReport.summary.scanned,
26333
+ legacyCount: auditReport.summary.legacyCount,
26334
+ buckets: {
26335
+ keep: auditReport.summary.keep,
26336
+ review: auditReport.summary.review,
26337
+ archive: auditReport.summary.archive,
26338
+ delete: auditReport.summary.delete
26339
+ },
26340
+ actions: auditReport.actionsTaken,
26341
+ lowestScore: low,
26342
+ report: auditReport
26343
+ };
26344
+ report.summary.issuesFound += auditReport.summary.review + auditReport.summary.archive + auditReport.summary.delete;
26345
+ if (!dryRun) {
26346
+ report.summary.actionsTaken += auditReport.actionsTaken.flaggedReview + auditReport.actionsTaken.archived + auditReport.actionsTaken.deleted;
26347
+ }
26348
+ for (const err of auditReport.errors) {
26349
+ report.errors.push({
26350
+ step: `audit:${err.step}`,
26351
+ message: err.entityId ? `${err.entityId}: ${err.message}` : err.message
26352
+ });
26353
+ }
26354
+ } catch (err) {
26355
+ report.errors.push({
26356
+ step: "audit",
26357
+ message: err.message
26358
+ });
26359
+ }
26360
+ }
25994
26361
  report.healthReport = generateHealthReport(report);
25995
26362
  return report;
25996
26363
  }
@@ -25999,7 +26366,7 @@ function runPruneStep(entities, maxAgeDays) {
25999
26366
  const drafts = entities.filter((e) => e.memory_tier === "draft");
26000
26367
  const stale = [];
26001
26368
  for (const entity of drafts) {
26002
- const ageDays = (now - new Date(entity.created_at).getTime()) / MS_PER_DAY;
26369
+ const ageDays = (now - new Date(entity.created_at).getTime()) / MS_PER_DAY2;
26003
26370
  if (ageDays < maxAgeDays)
26004
26371
  continue;
26005
26372
  const lifecycle2 = evaluateLifecycle(entity);
@@ -26020,17 +26387,17 @@ async function runOrphanStep(client3, entities, orphanAgeDays) {
26020
26387
  return false;
26021
26388
  if (e.access_count >= 2)
26022
26389
  return false;
26023
- const ageDays = (now - new Date(e.created_at).getTime()) / MS_PER_DAY;
26390
+ const ageDays = (now - new Date(e.created_at).getTime()) / MS_PER_DAY2;
26024
26391
  return ageDays >= orphanAgeDays;
26025
26392
  });
26026
- for (let i = 0;i < candidates.length; i += CONCURRENCY_LIMIT) {
26027
- const batch = candidates.slice(i, i + CONCURRENCY_LIMIT);
26393
+ for (let i = 0;i < candidates.length; i += CONCURRENCY_LIMIT2) {
26394
+ const batch = candidates.slice(i, i + CONCURRENCY_LIMIT2);
26028
26395
  const results = await Promise.allSettled(batch.map(async (entity) => {
26029
26396
  const related = await client3.getRelatedEntities(entity.id);
26030
26397
  const totalRelations = (related.outgoing?.length || 0) + (related.incoming?.length || 0);
26031
26398
  if (totalRelations > 0)
26032
26399
  return null;
26033
- const ageDays = (now - new Date(entity.created_at).getTime()) / MS_PER_DAY;
26400
+ const ageDays = (now - new Date(entity.created_at).getTime()) / MS_PER_DAY2;
26034
26401
  return {
26035
26402
  id: entity.id,
26036
26403
  title: entity.title,
@@ -26059,8 +26426,8 @@ async function runDuplicateStep(client3, entities, workspaceId, projectId) {
26059
26426
  const flaggedForRemoval = new Set;
26060
26427
  const entityMap = new Map(entities.map((e) => [e.id, e]));
26061
26428
  const similarityMap = new Map;
26062
- for (let i = 0;i < entities.length; i += CONCURRENCY_LIMIT) {
26063
- const batch = entities.slice(i, i + CONCURRENCY_LIMIT);
26429
+ for (let i = 0;i < entities.length; i += CONCURRENCY_LIMIT2) {
26430
+ const batch = entities.slice(i, i + CONCURRENCY_LIMIT2);
26064
26431
  const results = await Promise.allSettled(batch.map(async (entity) => {
26065
26432
  const similar = await findSimilarEntities(client3, entity.title, entity.content, workspaceId, { projectId, limit: 5, minRrfScore: 0.05, excludeIds: [entity.id] });
26066
26433
  return { entityId: entity.id, similar };
@@ -26212,6 +26579,20 @@ function generateHealthReport(report) {
26212
26579
  `);
26213
26580
  }
26214
26581
  }
26582
+ if (report.steps.audit) {
26583
+ const a = report.steps.audit;
26584
+ lines.push("## Quality Audit");
26585
+ lines.push(`Scanned ${a.scanned} entities. Legacy signals on ${a.legacyCount}.`);
26586
+ lines.push(`Buckets — keep: ${a.buckets.keep}, review: ${a.buckets.review}, archive: ${a.buckets.archive}, delete: ${a.buckets.delete}.`);
26587
+ if (!report.dryRun) {
26588
+ lines.push(`Actions — flagged: ${a.actions.flaggedReview}, archived: ${a.actions.archived}, deleted: ${a.actions.deleted}.`);
26589
+ }
26590
+ if (a.report.lowest.length > 0) {
26591
+ const worst = a.report.lowest[0];
26592
+ lines.push(`Lowest score: **${worst.score}** — "${worst.title}" (${worst.reasons.slice(0, 2).join(", ") || "—"}).`);
26593
+ }
26594
+ lines.push("");
26595
+ }
26215
26596
  if (report.errors.length > 0) {
26216
26597
  lines.push("## Errors");
26217
26598
  for (const e of report.errors) {
@@ -26256,7 +26637,7 @@ async function purgeMemories(client3, workspaceId, projectId, options) {
26256
26637
  continue;
26257
26638
  if (filters.olderThanDays !== undefined) {
26258
26639
  const ref = entity.last_accessed_at || entity.created_at;
26259
- const ageDays = (now - new Date(ref).getTime()) / MS_PER_DAY;
26640
+ const ageDays = (now - new Date(ref).getTime()) / MS_PER_DAY2;
26260
26641
  if (ageDays < filters.olderThanDays)
26261
26642
  continue;
26262
26643
  }
@@ -26272,13 +26653,13 @@ async function purgeMemories(client3, workspaceId, projectId, options) {
26272
26653
  type: e.type,
26273
26654
  tier: e.memory_tier,
26274
26655
  confidence: e.confidence,
26275
- ageDays: Math.round((now - new Date(e.last_accessed_at || e.created_at).getTime()) / MS_PER_DAY)
26656
+ ageDays: Math.round((now - new Date(e.last_accessed_at || e.created_at).getTime()) / MS_PER_DAY2)
26276
26657
  }));
26277
26658
  const errors3 = [];
26278
26659
  let purged = 0;
26279
26660
  if (!dryRun) {
26280
- for (let i = 0;i < allMatches.length; i += CONCURRENCY_LIMIT) {
26281
- const batch = allMatches.slice(i, i + CONCURRENCY_LIMIT);
26661
+ for (let i = 0;i < allMatches.length; i += CONCURRENCY_LIMIT2) {
26662
+ const batch = allMatches.slice(i, i + CONCURRENCY_LIMIT2);
26282
26663
  const results = await Promise.allSettled(batch.map((e) => client3.deleteMemoryEntity(e.id)));
26283
26664
  for (let j = 0;j < results.length; j++) {
26284
26665
  if (results[j].status === "fulfilled") {
@@ -27755,7 +28136,7 @@ var TOOLS = {
27755
28136
  }
27756
28137
  },
27757
28138
  harmony_cleanup_memories: {
27758
- description: "Run a unified memory cleanup: prune stale drafts, consolidate similar memories, detect orphans and duplicates, and backfill embeddings. Returns a health report. Dry-run by default — run with dryRun=false to execute.",
28139
+ description: "Run a unified memory cleanup: prune stale drafts, consolidate similar memories, detect orphans and duplicates, backfill embeddings, and optionally run a quality audit. Returns a health report. Dry-run by default — run with dryRun=false to execute.",
27759
28140
  inputSchema: {
27760
28141
  type: "object",
27761
28142
  properties: {
@@ -27775,9 +28156,16 @@ var TOOLS = {
27775
28156
  type: "array",
27776
28157
  items: {
27777
28158
  type: "string",
27778
- enum: ["prune", "consolidate", "orphans", "duplicates", "backfill"]
28159
+ enum: [
28160
+ "prune",
28161
+ "consolidate",
28162
+ "orphans",
28163
+ "duplicates",
28164
+ "backfill",
28165
+ "audit"
28166
+ ]
27779
28167
  },
27780
- description: "Which cleanup steps to run (default: all). Options: prune, consolidate, orphans, duplicates, backfill."
28168
+ description: "Which cleanup steps to run (default: all). Options: prune, consolidate, orphans, duplicates, backfill, audit."
27781
28169
  },
27782
28170
  maxAgeDays: {
27783
28171
  type: "number",
@@ -27790,6 +28178,47 @@ var TOOLS = {
27790
28178
  orphanAgeDays: {
27791
28179
  type: "number",
27792
28180
  description: "Min age in days for orphan detection (default: 14)"
28181
+ },
28182
+ auditArchiveBelow: {
28183
+ type: "number",
28184
+ description: "Audit: archive entities scoring below this (default: 40)"
28185
+ },
28186
+ auditDeleteBelow: {
28187
+ type: "number",
28188
+ description: "Audit: delete entities scoring below this (default: 20)"
28189
+ }
28190
+ },
28191
+ required: []
28192
+ }
28193
+ },
28194
+ harmony_audit_memories: {
28195
+ description: "Rate every memory against state-of-the-art quality standards (confidence, decay, structural completeness, content, tier-age fit, access). Flags legacy entities from before recent optimizations (default confidence, missing embeddings, stuck drafts). Buckets: keep (≥70), review (40-69), archive (20-39), delete (<20). Dry-run by default.",
28196
+ inputSchema: {
28197
+ type: "object",
28198
+ properties: {
28199
+ workspaceId: {
28200
+ type: "string",
28201
+ description: "Workspace ID (optional if context set)"
28202
+ },
28203
+ projectId: {
28204
+ type: "string",
28205
+ description: "Project ID (optional)"
28206
+ },
28207
+ dryRun: {
28208
+ type: "boolean",
28209
+ description: "Preview audit without flagging/archiving/deleting (default: true)"
28210
+ },
28211
+ archiveBelow: {
28212
+ type: "number",
28213
+ description: "Score threshold below which entities are archived (confidence set to 0.25). Default: 40"
28214
+ },
28215
+ deleteBelow: {
28216
+ type: "number",
28217
+ description: "Score threshold below which entities are hard-deleted. Default: 20. Set to 0 to never delete."
28218
+ },
28219
+ limit: {
28220
+ type: "number",
28221
+ description: "Max number of entities to audit (default: 500). Paginated fetch."
27793
28222
  }
27794
28223
  },
27795
28224
  required: []
@@ -29359,6 +29788,30 @@ async function handleToolCall(name, args, deps) {
29359
29788
  message: `Processed ${entitiesProcessed} entities, created ${relationsCreated} new relations.`
29360
29789
  };
29361
29790
  }
29791
+ case "harmony_audit_memories": {
29792
+ const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
29793
+ if (!workspaceId) {
29794
+ throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
29795
+ }
29796
+ const projectId = args.projectId || deps.getActiveProjectId() || undefined;
29797
+ const report = await runMemoryAudit(client3, workspaceId, projectId, {
29798
+ dryRun: args.dryRun,
29799
+ archiveBelow: args.archiveBelow,
29800
+ deleteBelow: args.deleteBelow,
29801
+ limit: args.limit
29802
+ });
29803
+ return {
29804
+ success: report.success,
29805
+ dryRun: report.dryRun,
29806
+ summary: report.summary,
29807
+ distribution: report.distribution,
29808
+ legacyBreakdown: report.legacyBreakdown,
29809
+ actionsTaken: report.actionsTaken,
29810
+ lowest: report.lowest,
29811
+ errors: report.errors,
29812
+ healthReport: report.healthReport
29813
+ };
29814
+ }
29362
29815
  case "harmony_cleanup_memories": {
29363
29816
  const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
29364
29817
  if (!workspaceId) {
@@ -29370,7 +29823,8 @@ async function handleToolCall(name, args, deps) {
29370
29823
  "consolidate",
29371
29824
  "orphans",
29372
29825
  "duplicates",
29373
- "backfill"
29826
+ "backfill",
29827
+ "audit"
29374
29828
  ];
29375
29829
  const rawSteps = args.steps;
29376
29830
  const steps = rawSteps?.filter((s) => validSteps.includes(s));
@@ -29383,7 +29837,9 @@ async function handleToolCall(name, args, deps) {
29383
29837
  steps,
29384
29838
  maxAgeDays: args.maxAgeDays,
29385
29839
  minClusterSize: args.minClusterSize,
29386
- orphanAgeDays: args.orphanAgeDays
29840
+ orphanAgeDays: args.orphanAgeDays,
29841
+ auditArchiveBelow: args.auditArchiveBelow,
29842
+ auditDeleteBelow: args.auditDeleteBelow
29387
29843
  });
29388
29844
  return {
29389
29845
  success: report.success,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.3.4",
3
+ "version": "2.4.0",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -59,7 +59,7 @@
59
59
  "serve:remote": "bun src/remote.ts",
60
60
  "dev": "bun --watch src/index.ts",
61
61
  "test": "bun run test:unit && bun run test:integration",
62
- "test:unit": "bun test src/__tests__/active-learning.test.ts src/__tests__/context-assembly.test.ts src/__tests__/prompt-builder.test.ts",
62
+ "test:unit": "bun test src/__tests__/active-learning.test.ts src/__tests__/context-assembly.test.ts src/__tests__/prompt-builder.test.ts src/__tests__/memory-audit.test.ts",
63
63
  "test:integration": "bun test src/__tests__/integration-memory-system.test.ts src/__tests__/integration-memory-crud.test.ts",
64
64
  "typecheck": "tsc --noEmit",
65
65
  "prepublishOnly": "bun run build"