@gethmy/mcp 2.3.2 → 2.3.4

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.
@@ -49,6 +49,43 @@ export interface CleanupOptions {
49
49
  orphanAgeDays?: number;
50
50
  }
51
51
 
52
+ // ---------------------------------------------------------------------------
53
+ // Purge types
54
+ // ---------------------------------------------------------------------------
55
+
56
+ export interface PurgeFilters {
57
+ tier?: "draft" | "episode" | "reference";
58
+ scope?: string;
59
+ type?: string;
60
+ olderThanDays?: number;
61
+ maxConfidence?: number;
62
+ tags?: string[];
63
+ }
64
+
65
+ export interface PurgeOptions {
66
+ dryRun?: boolean;
67
+ filters: PurgeFilters;
68
+ }
69
+
70
+ export interface PurgeReport {
71
+ success: boolean;
72
+ dryRun: boolean;
73
+ timestamp: string;
74
+ workspace: { id: string; projectId: string };
75
+ filters: PurgeFilters;
76
+ matched: number;
77
+ purged: number;
78
+ items: Array<{
79
+ id: string;
80
+ title: string;
81
+ type: string;
82
+ tier: string;
83
+ confidence: number;
84
+ ageDays: number;
85
+ }>;
86
+ errors: Array<{ entityId: string; message: string }>;
87
+ }
88
+
52
89
  interface PruneStepResult {
53
90
  staleDraftsFound: number;
54
91
  pruned: number;
@@ -654,3 +691,124 @@ function generateHealthReport(report: CleanupReport): string {
654
691
 
655
692
  return lines.join("\n");
656
693
  }
694
+
695
+ // ---------------------------------------------------------------------------
696
+ // Purge — filtered bulk deletion
697
+ // ---------------------------------------------------------------------------
698
+
699
+ export async function purgeMemories(
700
+ client: HarmonyApiClient,
701
+ workspaceId: string,
702
+ projectId: string,
703
+ options: PurgeOptions,
704
+ ): Promise<PurgeReport> {
705
+ const dryRun = options.dryRun !== false;
706
+ const { filters } = options;
707
+
708
+ // Safety: require at least one narrowing filter
709
+ const hasFilter =
710
+ filters.tier ||
711
+ filters.scope ||
712
+ filters.type ||
713
+ filters.olderThanDays !== undefined ||
714
+ filters.maxConfidence !== undefined ||
715
+ (filters.tags && filters.tags.length > 0);
716
+
717
+ if (!hasFilter) {
718
+ throw new Error(
719
+ "At least one narrowing filter (tier, scope, type, olderThanDays, maxConfidence, tags) is required.",
720
+ );
721
+ }
722
+
723
+ // Paginate through all matching entities
724
+ const allMatches: MemoryEntity[] = [];
725
+ let offset = 0;
726
+ const pageSize = 100;
727
+ const now = Date.now();
728
+
729
+ while (true) {
730
+ const result = await client.listMemoryEntities({
731
+ workspace_id: workspaceId,
732
+ project_id: projectId,
733
+ type: filters.type,
734
+ scope: filters.scope,
735
+ tags: filters.tags,
736
+ limit: pageSize,
737
+ offset,
738
+ });
739
+
740
+ const entities = (result.entities || []) as MemoryEntity[];
741
+ if (entities.length === 0) break;
742
+
743
+ // Client-side filtering for fields the API doesn't support natively
744
+ for (const entity of entities) {
745
+ if (filters.tier && entity.memory_tier !== filters.tier) continue;
746
+ if (
747
+ filters.maxConfidence !== undefined &&
748
+ entity.confidence > filters.maxConfidence
749
+ )
750
+ continue;
751
+ if (filters.olderThanDays !== undefined) {
752
+ const ref = entity.last_accessed_at || entity.created_at;
753
+ const ageDays = (now - new Date(ref).getTime()) / MS_PER_DAY;
754
+ if (ageDays < filters.olderThanDays) continue;
755
+ }
756
+ allMatches.push(entity);
757
+ }
758
+
759
+ if (entities.length < pageSize) break;
760
+ offset += pageSize;
761
+ }
762
+
763
+ // Build preview items
764
+ const items = allMatches.map((e) => ({
765
+ id: e.id,
766
+ title: e.title,
767
+ type: e.type,
768
+ tier: e.memory_tier,
769
+ confidence: e.confidence,
770
+ ageDays: Math.round(
771
+ (now - new Date(e.last_accessed_at || e.created_at).getTime()) /
772
+ MS_PER_DAY,
773
+ ),
774
+ }));
775
+
776
+ // Execute deletions if not dry-run
777
+ const errors: Array<{ entityId: string; message: string }> = [];
778
+ let purged = 0;
779
+
780
+ if (!dryRun) {
781
+ // Delete in batches to avoid overwhelming the API
782
+ for (let i = 0; i < allMatches.length; i += CONCURRENCY_LIMIT) {
783
+ const batch = allMatches.slice(i, i + CONCURRENCY_LIMIT);
784
+ const results = await Promise.allSettled(
785
+ batch.map((e) => client.deleteMemoryEntity(e.id)),
786
+ );
787
+ for (let j = 0; j < results.length; j++) {
788
+ if (results[j].status === "fulfilled") {
789
+ purged++;
790
+ } else {
791
+ errors.push({
792
+ entityId: batch[j].id,
793
+ message:
794
+ results[j].status === "rejected"
795
+ ? String((results[j] as PromiseRejectedResult).reason)
796
+ : "Unknown error",
797
+ });
798
+ }
799
+ }
800
+ }
801
+ }
802
+
803
+ return {
804
+ success: errors.length === 0,
805
+ dryRun,
806
+ timestamp: new Date().toISOString(),
807
+ workspace: { id: workspaceId, projectId },
808
+ filters,
809
+ matched: allMatches.length,
810
+ purged: dryRun ? 0 : purged,
811
+ items,
812
+ errors,
813
+ };
814
+ }
package/src/server.ts CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  } from "./context-assembly.js";
59
59
  import { autoExpandGraph } from "./graph-expansion.js";
60
60
  import { runLifecycleMaintenance } from "./lifecycle-maintenance.js";
61
- import { runMemoryCleanup } from "./memory-cleanup.js";
61
+ import { runMemoryCleanup, purgeMemories, type PurgeFilters } from "./memory-cleanup.js";
62
62
  import { onboardNewUser } from "./onboard.js";
63
63
  import type { PromptVariant } from "./prompt-builder.js";
64
64
 
@@ -721,6 +721,22 @@ const TOOLS = {
721
721
  type: "number",
722
722
  description: "Updated time estimate",
723
723
  },
724
+ actions: {
725
+ type: "array",
726
+ items: {
727
+ type: "object",
728
+ properties: {
729
+ description: {
730
+ type: "string",
731
+ description:
732
+ "What was done, e.g. 'Edited CardDetailSheet.tsx — added done toggle'",
733
+ },
734
+ },
735
+ required: ["description"],
736
+ },
737
+ description:
738
+ "Actions performed since last update. Each becomes a visible activity log entry.",
739
+ },
724
740
  },
725
741
  required: ["cardId", "agentIdentifier", "agentName"],
726
742
  },
@@ -1683,6 +1699,60 @@ const TOOLS = {
1683
1699
  required: [],
1684
1700
  },
1685
1701
  },
1702
+ harmony_purge_memories: {
1703
+ description:
1704
+ "Bulk-delete memory entities matching filters within a project. Requires at least one narrowing filter (tier, scope, type, olderThanDays, maxConfidence, tags). Dry-run by default — preview what would be deleted before executing.",
1705
+ inputSchema: {
1706
+ type: "object",
1707
+ properties: {
1708
+ workspaceId: {
1709
+ type: "string",
1710
+ description: "Workspace ID (optional if context set)",
1711
+ },
1712
+ projectId: {
1713
+ type: "string",
1714
+ description:
1715
+ "Project ID (required — purge is project-scoped). Falls back to active project context.",
1716
+ },
1717
+ dryRun: {
1718
+ type: "boolean",
1719
+ description:
1720
+ "Preview what would be deleted without executing (default: true)",
1721
+ },
1722
+ tier: {
1723
+ type: "string",
1724
+ enum: ["draft", "episode", "reference"],
1725
+ description: 'Filter by memory tier (e.g. "draft")',
1726
+ },
1727
+ scope: {
1728
+ type: "string",
1729
+ description:
1730
+ 'Filter by scope (e.g. "private", "project", "workspace")',
1731
+ },
1732
+ type: {
1733
+ type: "string",
1734
+ description:
1735
+ 'Filter by entity type (e.g. "error", "pattern", "lesson", "decision")',
1736
+ },
1737
+ olderThanDays: {
1738
+ type: "number",
1739
+ description:
1740
+ "Only include entities not accessed in at least this many days",
1741
+ },
1742
+ maxConfidence: {
1743
+ type: "number",
1744
+ description:
1745
+ "Only include entities with confidence at or below this value (e.g. 0.3 for low-confidence junk)",
1746
+ },
1747
+ tags: {
1748
+ type: "array",
1749
+ items: { type: "string" },
1750
+ description: "Only include entities matching these tags",
1751
+ },
1752
+ },
1753
+ required: [],
1754
+ },
1755
+ },
1686
1756
  };
1687
1757
 
1688
1758
  // Resource URIs
@@ -2643,18 +2713,26 @@ async function handleToolCall(
2643
2713
  args.progressPercent !== undefined
2644
2714
  ? z.number().min(0).max(100).parse(args.progressPercent)
2645
2715
  : undefined;
2646
- // Merge any pending memory actions into the progress update
2647
- const callerRecentActions = args.recentActions as
2648
- | { action: string; ts: string }[]
2716
+ // Convert actions parameter to recentActions format and merge with memory actions
2717
+ const callerActions = args.actions as
2718
+ | { description: string }[]
2649
2719
  | undefined;
2720
+ const now = new Date().toISOString();
2721
+ const callerRecentActions: { action: string; ts: string }[] = [
2722
+ ...((args.recentActions as { action: string; ts: string }[]) || []),
2723
+ ...(callerActions || []).map((a) => ({
2724
+ action: a.description,
2725
+ ts: now,
2726
+ })),
2727
+ ];
2650
2728
  const memSession = getMemorySession(cardId);
2651
2729
  let mergedRecentActions: { action: string; ts: string }[] | undefined;
2652
2730
  if (memSession?.dirty) {
2653
2731
  mergedRecentActions = mergeMemoryActionsInto(
2654
2732
  cardId,
2655
- callerRecentActions || [],
2733
+ callerRecentActions,
2656
2734
  );
2657
- } else if (callerRecentActions) {
2735
+ } else if (callerRecentActions.length > 0) {
2658
2736
  mergedRecentActions = callerRecentActions;
2659
2737
  }
2660
2738
 
@@ -4056,6 +4134,50 @@ async function handleToolCall(
4056
4134
  };
4057
4135
  }
4058
4136
 
4137
+ case "harmony_purge_memories": {
4138
+ const workspaceId =
4139
+ (args.workspaceId as string) || deps.getActiveWorkspaceId();
4140
+ if (!workspaceId) {
4141
+ throw new Error(
4142
+ "No workspace specified. Use harmony_set_workspace_context or provide workspaceId.",
4143
+ );
4144
+ }
4145
+ const projectId =
4146
+ (args.projectId as string) || deps.getActiveProjectId();
4147
+ if (!projectId) {
4148
+ throw new Error(
4149
+ "No project specified. Purge requires a project scope. Use harmony_set_project_context or provide projectId.",
4150
+ );
4151
+ }
4152
+
4153
+ const filters: PurgeFilters = {};
4154
+ if (args.tier) filters.tier = args.tier as PurgeFilters["tier"];
4155
+ if (args.scope) filters.scope = args.scope as string;
4156
+ if (args.type) filters.type = args.type as string;
4157
+ if (args.olderThanDays !== undefined)
4158
+ filters.olderThanDays = args.olderThanDays as number;
4159
+ if (args.maxConfidence !== undefined)
4160
+ filters.maxConfidence = args.maxConfidence as number;
4161
+ if (args.tags) filters.tags = args.tags as string[];
4162
+
4163
+ const report = await purgeMemories(client, workspaceId, projectId, {
4164
+ dryRun: args.dryRun as boolean | undefined,
4165
+ filters,
4166
+ });
4167
+
4168
+ return {
4169
+ success: report.success,
4170
+ dryRun: report.dryRun,
4171
+ matched: report.matched,
4172
+ purged: report.purged,
4173
+ items: report.items,
4174
+ errors: report.errors,
4175
+ message: report.dryRun
4176
+ ? `Found ${report.matched} entities matching filters. Run with dryRun=false to delete.`
4177
+ : `Purged ${report.purged} of ${report.matched} matching entities.`,
4178
+ };
4179
+ }
4180
+
4059
4181
  default:
4060
4182
  throw new Error(`Unknown tool: ${name}`);
4061
4183
  }
@@ -4109,6 +4231,7 @@ export class HarmonyMCPServer {
4109
4231
  );
4110
4232
 
4111
4233
  // Graceful shutdown: end all auto-sessions
4234
+ let exitCode = 0;
4112
4235
  const handleShutdown = async () => {
4113
4236
  try {
4114
4237
  await shutdownAllSessions();
@@ -4116,10 +4239,20 @@ export class HarmonyMCPServer {
4116
4239
  // Best-effort
4117
4240
  }
4118
4241
  destroyAutoSession();
4119
- process.exit(0);
4242
+ process.exit(exitCode);
4120
4243
  };
4121
4244
  process.on("SIGINT", handleShutdown);
4122
4245
  process.on("SIGTERM", handleShutdown);
4246
+ process.on("uncaughtException", (err) => {
4247
+ console.error("MCP server uncaught exception:", err);
4248
+ exitCode = 1;
4249
+ handleShutdown();
4250
+ });
4251
+ process.on("unhandledRejection", (reason) => {
4252
+ console.error("MCP server unhandled rejection:", reason);
4253
+ exitCode = 1;
4254
+ handleShutdown();
4255
+ });
4123
4256
 
4124
4257
  // Attempt startup sync (non-blocking, best-effort)
4125
4258
  try {