@gethmy/mcp 2.3.3 → 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.
package/dist/cli.js CHANGED
@@ -28465,6 +28465,85 @@ function generateHealthReport(report) {
28465
28465
  return lines.join(`
28466
28466
  `);
28467
28467
  }
28468
+ async function purgeMemories(client3, workspaceId, projectId, options) {
28469
+ const dryRun = options.dryRun !== false;
28470
+ const { filters } = options;
28471
+ const hasFilter = filters.tier || filters.scope || filters.type || filters.olderThanDays !== undefined || filters.maxConfidence !== undefined || filters.tags && filters.tags.length > 0;
28472
+ if (!hasFilter) {
28473
+ throw new Error("At least one narrowing filter (tier, scope, type, olderThanDays, maxConfidence, tags) is required.");
28474
+ }
28475
+ const allMatches = [];
28476
+ let offset = 0;
28477
+ const pageSize = 100;
28478
+ const now = Date.now();
28479
+ while (true) {
28480
+ const result = await client3.listMemoryEntities({
28481
+ workspace_id: workspaceId,
28482
+ project_id: projectId,
28483
+ type: filters.type,
28484
+ scope: filters.scope,
28485
+ tags: filters.tags,
28486
+ limit: pageSize,
28487
+ offset
28488
+ });
28489
+ const entities = result.entities || [];
28490
+ if (entities.length === 0)
28491
+ break;
28492
+ for (const entity of entities) {
28493
+ if (filters.tier && entity.memory_tier !== filters.tier)
28494
+ continue;
28495
+ if (filters.maxConfidence !== undefined && entity.confidence > filters.maxConfidence)
28496
+ continue;
28497
+ if (filters.olderThanDays !== undefined) {
28498
+ const ref = entity.last_accessed_at || entity.created_at;
28499
+ const ageDays = (now - new Date(ref).getTime()) / MS_PER_DAY;
28500
+ if (ageDays < filters.olderThanDays)
28501
+ continue;
28502
+ }
28503
+ allMatches.push(entity);
28504
+ }
28505
+ if (entities.length < pageSize)
28506
+ break;
28507
+ offset += pageSize;
28508
+ }
28509
+ const items = allMatches.map((e) => ({
28510
+ id: e.id,
28511
+ title: e.title,
28512
+ type: e.type,
28513
+ tier: e.memory_tier,
28514
+ confidence: e.confidence,
28515
+ ageDays: Math.round((now - new Date(e.last_accessed_at || e.created_at).getTime()) / MS_PER_DAY)
28516
+ }));
28517
+ const errors3 = [];
28518
+ let purged = 0;
28519
+ if (!dryRun) {
28520
+ for (let i = 0;i < allMatches.length; i += CONCURRENCY_LIMIT) {
28521
+ const batch = allMatches.slice(i, i + CONCURRENCY_LIMIT);
28522
+ const results = await Promise.allSettled(batch.map((e) => client3.deleteMemoryEntity(e.id)));
28523
+ for (let j = 0;j < results.length; j++) {
28524
+ if (results[j].status === "fulfilled") {
28525
+ purged++;
28526
+ } else {
28527
+ errors3.push({
28528
+ entityId: batch[j].id,
28529
+ message: results[j].status === "rejected" ? String(results[j].reason) : "Unknown error"
28530
+ });
28531
+ }
28532
+ }
28533
+ }
28534
+ }
28535
+ return {
28536
+ success: errors3.length === 0,
28537
+ dryRun,
28538
+ timestamp: new Date().toISOString(),
28539
+ workspace: { id: workspaceId, projectId },
28540
+ filters,
28541
+ matched: allMatches.length,
28542
+ purged: dryRun ? 0 : purged,
28543
+ items,
28544
+ errors: errors3
28545
+ };
28546
+ }
28468
28547
 
28469
28548
  // src/onboard.ts
28470
28549
  async function onboardNewUser(params) {
@@ -29060,6 +29139,20 @@ var TOOLS = {
29060
29139
  estimatedMinutesRemaining: {
29061
29140
  type: "number",
29062
29141
  description: "Updated time estimate"
29142
+ },
29143
+ actions: {
29144
+ type: "array",
29145
+ items: {
29146
+ type: "object",
29147
+ properties: {
29148
+ description: {
29149
+ type: "string",
29150
+ description: "What was done, e.g. 'Edited CardDetailSheet.tsx — added done toggle'"
29151
+ }
29152
+ },
29153
+ required: ["description"]
29154
+ },
29155
+ description: "Actions performed since last update. Each becomes a visible activity log entry."
29063
29156
  }
29064
29157
  },
29065
29158
  required: ["cardId", "agentIdentifier", "agentName"]
@@ -29941,6 +30034,53 @@ var TOOLS = {
29941
30034
  },
29942
30035
  required: []
29943
30036
  }
30037
+ },
30038
+ harmony_purge_memories: {
30039
+ description: "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.",
30040
+ inputSchema: {
30041
+ type: "object",
30042
+ properties: {
30043
+ workspaceId: {
30044
+ type: "string",
30045
+ description: "Workspace ID (optional if context set)"
30046
+ },
30047
+ projectId: {
30048
+ type: "string",
30049
+ description: "Project ID (required — purge is project-scoped). Falls back to active project context."
30050
+ },
30051
+ dryRun: {
30052
+ type: "boolean",
30053
+ description: "Preview what would be deleted without executing (default: true)"
30054
+ },
30055
+ tier: {
30056
+ type: "string",
30057
+ enum: ["draft", "episode", "reference"],
30058
+ description: 'Filter by memory tier (e.g. "draft")'
30059
+ },
30060
+ scope: {
30061
+ type: "string",
30062
+ description: 'Filter by scope (e.g. "private", "project", "workspace")'
30063
+ },
30064
+ type: {
30065
+ type: "string",
30066
+ description: 'Filter by entity type (e.g. "error", "pattern", "lesson", "decision")'
30067
+ },
30068
+ olderThanDays: {
30069
+ type: "number",
30070
+ description: "Only include entities not accessed in at least this many days"
30071
+ },
30072
+ maxConfidence: {
30073
+ type: "number",
30074
+ description: "Only include entities with confidence at or below this value (e.g. 0.3 for low-confidence junk)"
30075
+ },
30076
+ tags: {
30077
+ type: "array",
30078
+ items: { type: "string" },
30079
+ description: "Only include entities matching these tags"
30080
+ }
30081
+ },
30082
+ required: []
30083
+ }
29944
30084
  }
29945
30085
  };
29946
30086
  var RESOURCES = [
@@ -30570,12 +30710,20 @@ async function handleToolCall(name, args, deps) {
30570
30710
  const agentIdentifier = exports_external.string().min(1).max(100).parse(args.agentIdentifier);
30571
30711
  const agentName = exports_external.string().min(1).max(100).parse(args.agentName);
30572
30712
  const progressPercent = args.progressPercent !== undefined ? exports_external.number().min(0).max(100).parse(args.progressPercent) : undefined;
30573
- const callerRecentActions = args.recentActions;
30713
+ const callerActions = args.actions;
30714
+ const now = new Date().toISOString();
30715
+ const callerRecentActions = [
30716
+ ...args.recentActions || [],
30717
+ ...(callerActions || []).map((a) => ({
30718
+ action: a.description,
30719
+ ts: now
30720
+ }))
30721
+ ];
30574
30722
  const memSession = getMemorySession(cardId);
30575
30723
  let mergedRecentActions;
30576
30724
  if (memSession?.dirty) {
30577
- mergedRecentActions = mergeMemoryActionsInto(cardId, callerRecentActions || []);
30578
- } else if (callerRecentActions) {
30725
+ mergedRecentActions = mergeMemoryActionsInto(cardId, callerRecentActions);
30726
+ } else if (callerRecentActions.length > 0) {
30579
30727
  mergedRecentActions = callerRecentActions;
30580
30728
  }
30581
30729
  const result = await client3.updateAgentProgress(cardId, {
@@ -31485,6 +31633,42 @@ async function handleToolCall(name, args, deps) {
31485
31633
  healthReport: report.healthReport
31486
31634
  };
31487
31635
  }
31636
+ case "harmony_purge_memories": {
31637
+ const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
31638
+ if (!workspaceId) {
31639
+ throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
31640
+ }
31641
+ const projectId = args.projectId || deps.getActiveProjectId();
31642
+ if (!projectId) {
31643
+ throw new Error("No project specified. Purge requires a project scope. Use harmony_set_project_context or provide projectId.");
31644
+ }
31645
+ const filters = {};
31646
+ if (args.tier)
31647
+ filters.tier = args.tier;
31648
+ if (args.scope)
31649
+ filters.scope = args.scope;
31650
+ if (args.type)
31651
+ filters.type = args.type;
31652
+ if (args.olderThanDays !== undefined)
31653
+ filters.olderThanDays = args.olderThanDays;
31654
+ if (args.maxConfidence !== undefined)
31655
+ filters.maxConfidence = args.maxConfidence;
31656
+ if (args.tags)
31657
+ filters.tags = args.tags;
31658
+ const report = await purgeMemories(client3, workspaceId, projectId, {
31659
+ dryRun: args.dryRun,
31660
+ filters
31661
+ });
31662
+ return {
31663
+ success: report.success,
31664
+ dryRun: report.dryRun,
31665
+ matched: report.matched,
31666
+ purged: report.purged,
31667
+ items: report.items,
31668
+ errors: report.errors,
31669
+ message: report.dryRun ? `Found ${report.matched} entities matching filters. Run with dryRun=false to delete.` : `Purged ${report.purged} of ${report.matched} matching entities.`
31670
+ };
31671
+ }
31488
31672
  default:
31489
31673
  throw new Error(`Unknown tool: ${name}`);
31490
31674
  }
@@ -31522,15 +31706,26 @@ class HarmonyMCPServer {
31522
31706
  const cv = this.server.getClientVersion();
31523
31707
  return cv ? { name: cv.name, version: cv.version } : null;
31524
31708
  });
31709
+ let exitCode = 0;
31525
31710
  const handleShutdown = async () => {
31526
31711
  try {
31527
31712
  await shutdownAllSessions();
31528
31713
  } catch {}
31529
31714
  destroyAutoSession();
31530
- process.exit(0);
31715
+ process.exit(exitCode);
31531
31716
  };
31532
31717
  process.on("SIGINT", handleShutdown);
31533
31718
  process.on("SIGTERM", handleShutdown);
31719
+ process.on("uncaughtException", (err) => {
31720
+ console.error("MCP server uncaught exception:", err);
31721
+ exitCode = 1;
31722
+ handleShutdown();
31723
+ });
31724
+ process.on("unhandledRejection", (reason) => {
31725
+ console.error("MCP server unhandled rejection:", reason);
31726
+ exitCode = 1;
31727
+ handleShutdown();
31728
+ });
31534
31729
  try {
31535
31730
  if (isConfigured()) {
31536
31731
  const workspaceId = getActiveWorkspaceId();
package/dist/index.js CHANGED
@@ -26225,6 +26225,85 @@ function generateHealthReport(report) {
26225
26225
  return lines.join(`
26226
26226
  `);
26227
26227
  }
26228
+ async function purgeMemories(client3, workspaceId, projectId, options) {
26229
+ const dryRun = options.dryRun !== false;
26230
+ const { filters } = options;
26231
+ const hasFilter = filters.tier || filters.scope || filters.type || filters.olderThanDays !== undefined || filters.maxConfidence !== undefined || filters.tags && filters.tags.length > 0;
26232
+ if (!hasFilter) {
26233
+ throw new Error("At least one narrowing filter (tier, scope, type, olderThanDays, maxConfidence, tags) is required.");
26234
+ }
26235
+ const allMatches = [];
26236
+ let offset = 0;
26237
+ const pageSize = 100;
26238
+ const now = Date.now();
26239
+ while (true) {
26240
+ const result = await client3.listMemoryEntities({
26241
+ workspace_id: workspaceId,
26242
+ project_id: projectId,
26243
+ type: filters.type,
26244
+ scope: filters.scope,
26245
+ tags: filters.tags,
26246
+ limit: pageSize,
26247
+ offset
26248
+ });
26249
+ const entities = result.entities || [];
26250
+ if (entities.length === 0)
26251
+ break;
26252
+ for (const entity of entities) {
26253
+ if (filters.tier && entity.memory_tier !== filters.tier)
26254
+ continue;
26255
+ if (filters.maxConfidence !== undefined && entity.confidence > filters.maxConfidence)
26256
+ continue;
26257
+ if (filters.olderThanDays !== undefined) {
26258
+ const ref = entity.last_accessed_at || entity.created_at;
26259
+ const ageDays = (now - new Date(ref).getTime()) / MS_PER_DAY;
26260
+ if (ageDays < filters.olderThanDays)
26261
+ continue;
26262
+ }
26263
+ allMatches.push(entity);
26264
+ }
26265
+ if (entities.length < pageSize)
26266
+ break;
26267
+ offset += pageSize;
26268
+ }
26269
+ const items = allMatches.map((e) => ({
26270
+ id: e.id,
26271
+ title: e.title,
26272
+ type: e.type,
26273
+ tier: e.memory_tier,
26274
+ confidence: e.confidence,
26275
+ ageDays: Math.round((now - new Date(e.last_accessed_at || e.created_at).getTime()) / MS_PER_DAY)
26276
+ }));
26277
+ const errors3 = [];
26278
+ let purged = 0;
26279
+ if (!dryRun) {
26280
+ for (let i = 0;i < allMatches.length; i += CONCURRENCY_LIMIT) {
26281
+ const batch = allMatches.slice(i, i + CONCURRENCY_LIMIT);
26282
+ const results = await Promise.allSettled(batch.map((e) => client3.deleteMemoryEntity(e.id)));
26283
+ for (let j = 0;j < results.length; j++) {
26284
+ if (results[j].status === "fulfilled") {
26285
+ purged++;
26286
+ } else {
26287
+ errors3.push({
26288
+ entityId: batch[j].id,
26289
+ message: results[j].status === "rejected" ? String(results[j].reason) : "Unknown error"
26290
+ });
26291
+ }
26292
+ }
26293
+ }
26294
+ }
26295
+ return {
26296
+ success: errors3.length === 0,
26297
+ dryRun,
26298
+ timestamp: new Date().toISOString(),
26299
+ workspace: { id: workspaceId, projectId },
26300
+ filters,
26301
+ matched: allMatches.length,
26302
+ purged: dryRun ? 0 : purged,
26303
+ items,
26304
+ errors: errors3
26305
+ };
26306
+ }
26228
26307
 
26229
26308
  // src/onboard.ts
26230
26309
  async function onboardNewUser(params) {
@@ -26820,6 +26899,20 @@ var TOOLS = {
26820
26899
  estimatedMinutesRemaining: {
26821
26900
  type: "number",
26822
26901
  description: "Updated time estimate"
26902
+ },
26903
+ actions: {
26904
+ type: "array",
26905
+ items: {
26906
+ type: "object",
26907
+ properties: {
26908
+ description: {
26909
+ type: "string",
26910
+ description: "What was done, e.g. 'Edited CardDetailSheet.tsx — added done toggle'"
26911
+ }
26912
+ },
26913
+ required: ["description"]
26914
+ },
26915
+ description: "Actions performed since last update. Each becomes a visible activity log entry."
26823
26916
  }
26824
26917
  },
26825
26918
  required: ["cardId", "agentIdentifier", "agentName"]
@@ -27701,6 +27794,53 @@ var TOOLS = {
27701
27794
  },
27702
27795
  required: []
27703
27796
  }
27797
+ },
27798
+ harmony_purge_memories: {
27799
+ description: "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.",
27800
+ inputSchema: {
27801
+ type: "object",
27802
+ properties: {
27803
+ workspaceId: {
27804
+ type: "string",
27805
+ description: "Workspace ID (optional if context set)"
27806
+ },
27807
+ projectId: {
27808
+ type: "string",
27809
+ description: "Project ID (required — purge is project-scoped). Falls back to active project context."
27810
+ },
27811
+ dryRun: {
27812
+ type: "boolean",
27813
+ description: "Preview what would be deleted without executing (default: true)"
27814
+ },
27815
+ tier: {
27816
+ type: "string",
27817
+ enum: ["draft", "episode", "reference"],
27818
+ description: 'Filter by memory tier (e.g. "draft")'
27819
+ },
27820
+ scope: {
27821
+ type: "string",
27822
+ description: 'Filter by scope (e.g. "private", "project", "workspace")'
27823
+ },
27824
+ type: {
27825
+ type: "string",
27826
+ description: 'Filter by entity type (e.g. "error", "pattern", "lesson", "decision")'
27827
+ },
27828
+ olderThanDays: {
27829
+ type: "number",
27830
+ description: "Only include entities not accessed in at least this many days"
27831
+ },
27832
+ maxConfidence: {
27833
+ type: "number",
27834
+ description: "Only include entities with confidence at or below this value (e.g. 0.3 for low-confidence junk)"
27835
+ },
27836
+ tags: {
27837
+ type: "array",
27838
+ items: { type: "string" },
27839
+ description: "Only include entities matching these tags"
27840
+ }
27841
+ },
27842
+ required: []
27843
+ }
27704
27844
  }
27705
27845
  };
27706
27846
  var RESOURCES = [
@@ -28330,12 +28470,20 @@ async function handleToolCall(name, args, deps) {
28330
28470
  const agentIdentifier = exports_external.string().min(1).max(100).parse(args.agentIdentifier);
28331
28471
  const agentName = exports_external.string().min(1).max(100).parse(args.agentName);
28332
28472
  const progressPercent = args.progressPercent !== undefined ? exports_external.number().min(0).max(100).parse(args.progressPercent) : undefined;
28333
- const callerRecentActions = args.recentActions;
28473
+ const callerActions = args.actions;
28474
+ const now = new Date().toISOString();
28475
+ const callerRecentActions = [
28476
+ ...args.recentActions || [],
28477
+ ...(callerActions || []).map((a) => ({
28478
+ action: a.description,
28479
+ ts: now
28480
+ }))
28481
+ ];
28334
28482
  const memSession = getMemorySession(cardId);
28335
28483
  let mergedRecentActions;
28336
28484
  if (memSession?.dirty) {
28337
- mergedRecentActions = mergeMemoryActionsInto(cardId, callerRecentActions || []);
28338
- } else if (callerRecentActions) {
28485
+ mergedRecentActions = mergeMemoryActionsInto(cardId, callerRecentActions);
28486
+ } else if (callerRecentActions.length > 0) {
28339
28487
  mergedRecentActions = callerRecentActions;
28340
28488
  }
28341
28489
  const result = await client3.updateAgentProgress(cardId, {
@@ -29245,6 +29393,42 @@ async function handleToolCall(name, args, deps) {
29245
29393
  healthReport: report.healthReport
29246
29394
  };
29247
29395
  }
29396
+ case "harmony_purge_memories": {
29397
+ const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
29398
+ if (!workspaceId) {
29399
+ throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
29400
+ }
29401
+ const projectId = args.projectId || deps.getActiveProjectId();
29402
+ if (!projectId) {
29403
+ throw new Error("No project specified. Purge requires a project scope. Use harmony_set_project_context or provide projectId.");
29404
+ }
29405
+ const filters = {};
29406
+ if (args.tier)
29407
+ filters.tier = args.tier;
29408
+ if (args.scope)
29409
+ filters.scope = args.scope;
29410
+ if (args.type)
29411
+ filters.type = args.type;
29412
+ if (args.olderThanDays !== undefined)
29413
+ filters.olderThanDays = args.olderThanDays;
29414
+ if (args.maxConfidence !== undefined)
29415
+ filters.maxConfidence = args.maxConfidence;
29416
+ if (args.tags)
29417
+ filters.tags = args.tags;
29418
+ const report = await purgeMemories(client3, workspaceId, projectId, {
29419
+ dryRun: args.dryRun,
29420
+ filters
29421
+ });
29422
+ return {
29423
+ success: report.success,
29424
+ dryRun: report.dryRun,
29425
+ matched: report.matched,
29426
+ purged: report.purged,
29427
+ items: report.items,
29428
+ errors: report.errors,
29429
+ message: report.dryRun ? `Found ${report.matched} entities matching filters. Run with dryRun=false to delete.` : `Purged ${report.purged} of ${report.matched} matching entities.`
29430
+ };
29431
+ }
29248
29432
  default:
29249
29433
  throw new Error(`Unknown tool: ${name}`);
29250
29434
  }
@@ -29282,15 +29466,26 @@ class HarmonyMCPServer {
29282
29466
  const cv = this.server.getClientVersion();
29283
29467
  return cv ? { name: cv.name, version: cv.version } : null;
29284
29468
  });
29469
+ let exitCode = 0;
29285
29470
  const handleShutdown = async () => {
29286
29471
  try {
29287
29472
  await shutdownAllSessions();
29288
29473
  } catch {}
29289
29474
  destroyAutoSession();
29290
- process.exit(0);
29475
+ process.exit(exitCode);
29291
29476
  };
29292
29477
  process.on("SIGINT", handleShutdown);
29293
29478
  process.on("SIGTERM", handleShutdown);
29479
+ process.on("uncaughtException", (err) => {
29480
+ console.error("MCP server uncaught exception:", err);
29481
+ exitCode = 1;
29482
+ handleShutdown();
29483
+ });
29484
+ process.on("unhandledRejection", (reason) => {
29485
+ console.error("MCP server unhandled rejection:", reason);
29486
+ exitCode = 1;
29487
+ handleShutdown();
29488
+ });
29294
29489
  try {
29295
29490
  if (isConfigured()) {
29296
29491
  const workspaceId = getActiveWorkspaceId();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.3.3",
3
+ "version": "2.3.4",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/src/api-client.ts CHANGED
@@ -502,6 +502,9 @@ export class HarmonyApiClient {
502
502
  phase?: string;
503
503
  filesChanged?: number;
504
504
  costCents?: number;
505
+ inputTokens?: number;
506
+ outputTokens?: number;
507
+ recentActions?: { action: string; ts: string }[];
505
508
  },
506
509
  ): Promise<{ session: unknown; created: boolean }> {
507
510
  return this.request("POST", `/cards/${cardId}/agent-context`, data);
@@ -512,6 +515,9 @@ export class HarmonyApiClient {
512
515
  data?: {
513
516
  status?: "completed" | "paused";
514
517
  progressPercent?: number;
518
+ costCents?: number;
519
+ inputTokens?: number;
520
+ outputTokens?: number;
515
521
  },
516
522
  ): Promise<{ session: unknown }> {
517
523
  return this.request("DELETE", `/cards/${cardId}/agent-context`, data);
@@ -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 {