@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,3365 +0,0 @@
1
- import { discoverRelatedContext, evaluateLifecycle, syncFull, syncPull, syncPush, } from "@harmony/memory";
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
- import { z } from "zod";
6
- import { detectContradictions, extractLearnings, extractMidSessionLearnings, } from "./active-learning.js";
7
- import { getClient, resetClient, signupUser, } from "./api-client.js";
8
- import { AUTO_START_TRIGGERS, destroyAutoSession, initAutoSession, markExplicit, shutdownAllSessions, trackActivity, untrack, } from "./auto-session.js";
9
- import { getActiveProjectId, getActiveWorkspaceId, getApiUrl, getMemoryDir, getUserEmail, isConfigured, saveConfig, setActiveProject, setActiveWorkspace, } from "./config.js";
10
- import { consolidateMemories } from "./consolidation.js";
11
- import { assembleContext, cacheManifest, computeRelevanceScore, getCachedManifest, mapToContextEntity, recordContextFeedback, trackSessionAssembly, } from "./context-assembly.js";
12
- import { autoExpandGraph } from "./graph-expansion.js";
13
- import { runLifecycleMaintenance } from "./lifecycle-maintenance.js";
14
- import { runMemoryCleanup } from "./memory-cleanup.js";
15
- import { onboardNewUser } from "./onboard.js";
16
- const memorySessions = new Map();
17
- function initMemorySession(cardId, agentIdentifier, agentName) {
18
- memorySessions.set(cardId, {
19
- cardId,
20
- agentIdentifier,
21
- agentName,
22
- memoryReadCount: 0,
23
- pendingActions: [],
24
- allActions: [],
25
- dirty: false,
26
- });
27
- }
28
- function getMemorySession(cardId) {
29
- return memorySessions.get(cardId);
30
- }
31
- function appendMemoryAction(cardId, action) {
32
- const session = memorySessions.get(cardId);
33
- if (!session)
34
- return;
35
- const truncated = action.length > 512 ? action.slice(0, 509) + "..." : action;
36
- const entry = { action: truncated, ts: new Date().toISOString() };
37
- session.pendingActions.push(entry);
38
- session.dirty = true;
39
- }
40
- function incrementMemoryReads(cardId) {
41
- const session = memorySessions.get(cardId);
42
- if (!session)
43
- return;
44
- session.memoryReadCount++;
45
- session.dirty = true;
46
- }
47
- /**
48
- * Flush pending memory actions to the backend via updateAgentProgress.
49
- * Fire-and-forget: errors are logged but never thrown.
50
- */
51
- async function flushMemoryActions(client, cardId) {
52
- const session = memorySessions.get(cardId);
53
- if (!session || !session.dirty)
54
- return;
55
- try {
56
- // Batch reads into a single summary line
57
- if (session.memoryReadCount > 0) {
58
- session.allActions.push({
59
- action: `Recalled ${session.memoryReadCount} memor${session.memoryReadCount === 1 ? "y" : "ies"}`,
60
- ts: new Date().toISOString(),
61
- });
62
- session.memoryReadCount = 0;
63
- }
64
- // Move pending writes to allActions
65
- if (session.pendingActions.length > 0) {
66
- session.allActions.push(...session.pendingActions);
67
- session.pendingActions = [];
68
- }
69
- // Trim to max 10 (drop oldest)
70
- if (session.allActions.length > 10) {
71
- session.allActions = session.allActions.slice(-10);
72
- }
73
- await client.updateAgentProgress(cardId, {
74
- agentIdentifier: session.agentIdentifier,
75
- agentName: session.agentName,
76
- recentActions: session.allActions,
77
- });
78
- session.dirty = false;
79
- }
80
- catch (err) {
81
- // Fire-and-forget: log but don't propagate
82
- console.error("[memory-session] flush failed:", err);
83
- }
84
- }
85
- /**
86
- * Merge memory actions into an existing recentActions array from a progress update.
87
- * Returns the merged array (caller's actions first, then memory actions appended).
88
- */
89
- function mergeMemoryActionsInto(cardId, callerActions) {
90
- const session = memorySessions.get(cardId);
91
- if (!session)
92
- return callerActions;
93
- // Flush reads into allActions
94
- if (session.memoryReadCount > 0) {
95
- session.allActions.push({
96
- action: `Recalled ${session.memoryReadCount} memor${session.memoryReadCount === 1 ? "y" : "ies"}`,
97
- ts: new Date().toISOString(),
98
- });
99
- session.memoryReadCount = 0;
100
- }
101
- // Move pending to allActions
102
- if (session.pendingActions.length > 0) {
103
- session.allActions.push(...session.pendingActions);
104
- session.pendingActions = [];
105
- }
106
- // Merge: caller actions + memory actions, trim to 10
107
- const merged = [...callerActions, ...session.allActions];
108
- const trimmed = merged.length > 10 ? merged.slice(-10) : merged;
109
- // Update allActions to the merged state
110
- session.allActions = trimmed;
111
- session.dirty = false;
112
- return trimmed;
113
- }
114
- /**
115
- * Get the first (and typically only) active memory session.
116
- * Memory tools don't carry a cardId, so we find the active session.
117
- */
118
- function getActiveMemorySession() {
119
- // Return the first active session (there should be at most one)
120
- for (const session of memorySessions.values()) {
121
- return session;
122
- }
123
- return undefined;
124
- }
125
- function cleanupMemorySession(cardId) {
126
- memorySessions.delete(cardId);
127
- }
128
- // Tool definitions
129
- const TOOLS = {
130
- // Card operations
131
- harmony_create_card: {
132
- description: "Create a new card in a Kanban column",
133
- inputSchema: {
134
- type: "object",
135
- properties: {
136
- title: { type: "string", description: "Card title" },
137
- columnId: {
138
- type: "string",
139
- description: "Target column ID (optional, defaults to first column)",
140
- },
141
- projectId: {
142
- type: "string",
143
- description: "Project ID (optional if context set)",
144
- },
145
- description: { type: "string", description: "Card description" },
146
- priority: {
147
- type: "string",
148
- enum: ["low", "medium", "high", "urgent"],
149
- description: "Priority level",
150
- },
151
- assigneeId: { type: "string", description: "Assignee user ID" },
152
- },
153
- required: ["title"],
154
- },
155
- },
156
- harmony_update_card: {
157
- description: "Update an existing card",
158
- inputSchema: {
159
- type: "object",
160
- properties: {
161
- cardId: { type: "string", description: "Card ID to update" },
162
- title: { type: "string" },
163
- description: { type: "string" },
164
- priority: { type: "string", enum: ["low", "medium", "high", "urgent"] },
165
- assigneeId: { type: "string", nullable: true },
166
- dueDate: { type: "string", nullable: true },
167
- done: { type: "boolean", description: "Mark card as done or not done" },
168
- },
169
- required: ["cardId"],
170
- },
171
- },
172
- harmony_move_card: {
173
- description: "Move a card to a different column or position. Provide either columnId (UUID) or columnName (e.g. 'Review', 'Done').",
174
- inputSchema: {
175
- type: "object",
176
- properties: {
177
- cardId: { type: "string", description: "Card ID to move" },
178
- columnId: { type: "string", description: "Target column ID (UUID)" },
179
- columnName: {
180
- type: "string",
181
- description: "Target column name (e.g. 'To Do', 'In Progress', 'Review', 'Done'). Used if columnId is not provided.",
182
- },
183
- position: {
184
- type: "number",
185
- description: "Position in column (0-indexed)",
186
- },
187
- },
188
- required: ["cardId"],
189
- },
190
- },
191
- harmony_archive_card: {
192
- description: "Archive a card (soft-delete). Card can be restored later with unarchive.",
193
- inputSchema: {
194
- type: "object",
195
- properties: {
196
- cardId: { type: "string", description: "Card ID to archive" },
197
- },
198
- required: ["cardId"],
199
- },
200
- },
201
- harmony_unarchive_card: {
202
- description: "Restore an archived card back to the board.",
203
- inputSchema: {
204
- type: "object",
205
- properties: {
206
- cardId: { type: "string", description: "Card ID to unarchive" },
207
- },
208
- required: ["cardId"],
209
- },
210
- },
211
- harmony_delete_card: {
212
- description: "Delete a card",
213
- inputSchema: {
214
- type: "object",
215
- properties: {
216
- cardId: { type: "string", description: "Card ID to delete" },
217
- },
218
- required: ["cardId"],
219
- },
220
- },
221
- harmony_assign_card: {
222
- description: "Assign a card to a team member",
223
- inputSchema: {
224
- type: "object",
225
- properties: {
226
- cardId: { type: "string", description: "Card ID" },
227
- assigneeId: {
228
- type: "string",
229
- nullable: true,
230
- description: "User ID (null to unassign)",
231
- },
232
- },
233
- required: ["cardId"],
234
- },
235
- },
236
- harmony_search_cards: {
237
- description: "Search for cards by title or description",
238
- inputSchema: {
239
- type: "object",
240
- properties: {
241
- query: { type: "string", description: "Search query" },
242
- projectId: { type: "string", description: "Limit to project" },
243
- },
244
- required: ["query"],
245
- },
246
- },
247
- harmony_get_card: {
248
- description: "Get detailed information about a specific card by UUID",
249
- inputSchema: {
250
- type: "object",
251
- properties: {
252
- cardId: { type: "string", description: "Card UUID" },
253
- },
254
- required: ["cardId"],
255
- },
256
- },
257
- harmony_get_card_by_short_id: {
258
- description: "Get a card by its short ID (e.g., #42) within a project",
259
- inputSchema: {
260
- type: "object",
261
- properties: {
262
- projectId: {
263
- type: "string",
264
- description: "Project ID (optional if context set)",
265
- },
266
- shortId: {
267
- type: "number",
268
- description: "Short ID number (e.g., 42 for card #42)",
269
- },
270
- },
271
- required: ["shortId"],
272
- },
273
- },
274
- // Column operations
275
- harmony_create_column: {
276
- description: "Create a new column in a project",
277
- inputSchema: {
278
- type: "object",
279
- properties: {
280
- name: { type: "string", description: "Column name" },
281
- projectId: { type: "string", description: "Project ID" },
282
- },
283
- required: ["name"],
284
- },
285
- },
286
- harmony_update_column: {
287
- description: "Update column properties",
288
- inputSchema: {
289
- type: "object",
290
- properties: {
291
- columnId: { type: "string", description: "Column ID" },
292
- name: { type: "string" },
293
- },
294
- required: ["columnId"],
295
- },
296
- },
297
- harmony_delete_column: {
298
- description: "Delete a column (cards in it will be deleted)",
299
- inputSchema: {
300
- type: "object",
301
- properties: {
302
- columnId: { type: "string", description: "Column ID to delete" },
303
- },
304
- required: ["columnId"],
305
- },
306
- },
307
- // Label operations
308
- harmony_create_label: {
309
- description: "Create a new label in a project",
310
- inputSchema: {
311
- type: "object",
312
- properties: {
313
- name: { type: "string", description: "Label name" },
314
- color: { type: "string", description: "Hex color code" },
315
- projectId: { type: "string", description: "Project ID" },
316
- },
317
- required: ["name", "color"],
318
- },
319
- },
320
- harmony_add_label_to_card: {
321
- description: "Add a label to a card. Provide labelId directly, or labelName to look up (or auto-create) the label by name.",
322
- inputSchema: {
323
- type: "object",
324
- properties: {
325
- cardId: { type: "string" },
326
- labelId: {
327
- type: "string",
328
- description: "Label ID (optional if labelName provided)",
329
- },
330
- labelName: {
331
- type: "string",
332
- description: "Label name — will look up or create if not found",
333
- },
334
- },
335
- required: ["cardId"],
336
- },
337
- },
338
- harmony_remove_label_from_card: {
339
- description: "Remove a label from a card",
340
- inputSchema: {
341
- type: "object",
342
- properties: {
343
- cardId: { type: "string" },
344
- labelId: { type: "string" },
345
- },
346
- required: ["cardId", "labelId"],
347
- },
348
- },
349
- // Card link operations
350
- harmony_add_link_to_card: {
351
- description: "Create a link between two cards. Link types: relates_to, blocks, duplicates, is_part_of",
352
- inputSchema: {
353
- type: "object",
354
- properties: {
355
- sourceCardId: {
356
- type: "string",
357
- description: "The card creating the link from",
358
- },
359
- targetCardId: {
360
- type: "string",
361
- description: "The card being linked to",
362
- },
363
- linkType: {
364
- type: "string",
365
- enum: ["relates_to", "blocks", "duplicates", "is_part_of"],
366
- description: "Type of relationship: relates_to (generic), blocks (source blocks target), duplicates (source duplicates target), is_part_of (source is part of target)",
367
- },
368
- },
369
- required: ["sourceCardId", "targetCardId", "linkType"],
370
- },
371
- },
372
- harmony_remove_link_from_card: {
373
- description: "Remove a link between cards",
374
- inputSchema: {
375
- type: "object",
376
- properties: {
377
- linkId: { type: "string", description: "The link ID to remove" },
378
- },
379
- required: ["linkId"],
380
- },
381
- },
382
- harmony_get_card_links: {
383
- description: "Get all links for a card",
384
- inputSchema: {
385
- type: "object",
386
- properties: {
387
- cardId: { type: "string", description: "Card ID to get links for" },
388
- },
389
- required: ["cardId"],
390
- },
391
- },
392
- // Subtask operations
393
- harmony_create_subtask: {
394
- description: "Create a subtask on a card",
395
- inputSchema: {
396
- type: "object",
397
- properties: {
398
- cardId: { type: "string" },
399
- title: { type: "string" },
400
- },
401
- required: ["cardId", "title"],
402
- },
403
- },
404
- harmony_toggle_subtask: {
405
- description: "Toggle subtask completion status",
406
- inputSchema: {
407
- type: "object",
408
- properties: {
409
- subtaskId: { type: "string" },
410
- },
411
- required: ["subtaskId"],
412
- },
413
- },
414
- harmony_delete_subtask: {
415
- description: "Delete a subtask",
416
- inputSchema: {
417
- type: "object",
418
- properties: {
419
- subtaskId: { type: "string" },
420
- },
421
- required: ["subtaskId"],
422
- },
423
- },
424
- // Context operations
425
- harmony_list_workspaces: {
426
- description: "List all workspaces the user has access to",
427
- inputSchema: {
428
- type: "object",
429
- properties: {},
430
- },
431
- },
432
- harmony_list_projects: {
433
- description: "List all projects in a workspace",
434
- inputSchema: {
435
- type: "object",
436
- properties: {
437
- workspaceId: {
438
- type: "string",
439
- description: "Workspace ID (optional if context set)",
440
- },
441
- },
442
- },
443
- },
444
- harmony_get_board: {
445
- description: "Get board state (columns, cards, labels). Use limit/offset for pagination on large boards. Use summary=true for just column counts without card details.",
446
- inputSchema: {
447
- type: "object",
448
- properties: {
449
- projectId: {
450
- type: "string",
451
- description: "Project ID (optional if context set)",
452
- },
453
- limit: {
454
- type: "number",
455
- description: "Max cards to return (default: 50)",
456
- },
457
- offset: {
458
- type: "number",
459
- description: "Skip N cards for pagination (default: 0)",
460
- },
461
- columnId: { type: "string", description: "Filter cards by column ID" },
462
- summary: {
463
- type: "boolean",
464
- description: "Return only columns with card counts, no card details",
465
- },
466
- includeArchived: {
467
- type: "boolean",
468
- description: "Include archived cards in results (default: false). Archived cards are excluded by default.",
469
- },
470
- },
471
- },
472
- },
473
- harmony_get_workspace_members: {
474
- description: "Get members of a workspace",
475
- inputSchema: {
476
- type: "object",
477
- properties: {
478
- workspaceId: {
479
- type: "string",
480
- description: "Workspace ID (optional if context set)",
481
- },
482
- },
483
- },
484
- },
485
- harmony_set_workspace_context: {
486
- description: "Set the active workspace context for subsequent operations",
487
- inputSchema: {
488
- type: "object",
489
- properties: {
490
- workspaceId: { type: "string" },
491
- },
492
- required: ["workspaceId"],
493
- },
494
- },
495
- harmony_set_project_context: {
496
- description: "Set the active project context for subsequent operations",
497
- inputSchema: {
498
- type: "object",
499
- properties: {
500
- projectId: { type: "string" },
501
- },
502
- required: ["projectId"],
503
- },
504
- },
505
- harmony_get_context: {
506
- description: "Get the current active workspace and project context",
507
- inputSchema: {
508
- type: "object",
509
- properties: {},
510
- },
511
- },
512
- // Natural language processing
513
- harmony_process_command: {
514
- description: 'Process a natural language command (e.g., "Create a card called Test in To Do")',
515
- inputSchema: {
516
- type: "object",
517
- properties: {
518
- command: { type: "string", description: "Natural language command" },
519
- projectId: { type: "string", description: "Project context" },
520
- workspaceId: { type: "string", description: "Workspace context" },
521
- language: {
522
- type: "string",
523
- enum: ["en-US", "de-DE"],
524
- description: "Language (default: en-US)",
525
- },
526
- execute: {
527
- type: "boolean",
528
- description: "Execute the resolved action (default: false)",
529
- },
530
- },
531
- required: ["command"],
532
- },
533
- },
534
- // Agent context operations
535
- harmony_start_agent_session: {
536
- description: "Start an agent work session on a card. Tracks progress, status, and blockers. Call this when beginning work on a card.",
537
- inputSchema: {
538
- type: "object",
539
- properties: {
540
- cardId: { type: "string", description: "Card ID to start working on" },
541
- agentIdentifier: {
542
- type: "string",
543
- description: "Unique agent identifier (e.g., claude-code)",
544
- },
545
- agentName: {
546
- type: "string",
547
- description: "Human-readable agent name (e.g., Claude Code)",
548
- },
549
- currentTask: {
550
- type: "string",
551
- description: "Initial task description",
552
- },
553
- estimatedMinutesRemaining: {
554
- type: "number",
555
- description: "Estimated time to completion in minutes",
556
- },
557
- moveToColumn: {
558
- type: "string",
559
- description: 'Column name to move card to (e.g., "In Progress"). Case-insensitive partial match.',
560
- },
561
- addLabels: {
562
- type: "array",
563
- items: { type: "string" },
564
- description: 'Label names to add to card (e.g., ["agent"]). Case-insensitive match.',
565
- },
566
- },
567
- required: ["cardId", "agentIdentifier", "agentName"],
568
- },
569
- },
570
- harmony_update_agent_progress: {
571
- description: "Update progress on an active agent session. Use to report progress percentage, current task, blockers, or status changes.",
572
- inputSchema: {
573
- type: "object",
574
- properties: {
575
- cardId: { type: "string", description: "Card ID with active session" },
576
- agentIdentifier: { type: "string", description: "Agent identifier" },
577
- agentName: { type: "string", description: "Agent name" },
578
- status: {
579
- type: "string",
580
- enum: ["working", "blocked", "waiting", "paused"],
581
- description: "Current status",
582
- },
583
- progressPercent: {
584
- type: "number",
585
- description: "Progress percentage (0-100)",
586
- },
587
- currentTask: {
588
- type: "string",
589
- description: "What the agent is currently doing",
590
- },
591
- blockers: {
592
- type: "array",
593
- items: { type: "string" },
594
- description: "List of blocking issues",
595
- },
596
- estimatedMinutesRemaining: {
597
- type: "number",
598
- description: "Updated time estimate",
599
- },
600
- },
601
- required: ["cardId", "agentIdentifier", "agentName"],
602
- },
603
- },
604
- harmony_end_agent_session: {
605
- description: "End an agent work session on a card. Call this when work is complete or paused.",
606
- inputSchema: {
607
- type: "object",
608
- properties: {
609
- cardId: { type: "string", description: "Card ID to end session on" },
610
- status: {
611
- type: "string",
612
- enum: ["completed", "paused"],
613
- description: "Final status (default: completed)",
614
- },
615
- progressPercent: {
616
- type: "number",
617
- description: "Final progress percentage",
618
- },
619
- moveToColumn: {
620
- type: "string",
621
- description: 'Column name to move card to after ending session (e.g., "Review"). Case-insensitive partial match.',
622
- },
623
- },
624
- required: ["cardId"],
625
- },
626
- },
627
- harmony_get_agent_session: {
628
- description: "Get the current agent session for a card, including progress, status, and blockers.",
629
- inputSchema: {
630
- type: "object",
631
- properties: {
632
- cardId: { type: "string", description: "Card ID to check" },
633
- includeEnded: {
634
- type: "boolean",
635
- description: "Include ended sessions in history",
636
- },
637
- },
638
- required: ["cardId"],
639
- },
640
- },
641
- harmony_get_agent_profile: {
642
- description: "Get aggregate performance profile for an agent. Shows total sessions, completion rate, average duration, and more. Defaults to the current agent if no identifier provided.",
643
- inputSchema: {
644
- type: "object",
645
- properties: {
646
- agentIdentifier: {
647
- type: "string",
648
- description: "Agent identifier (e.g., 'claude-code'). Defaults to current agent.",
649
- },
650
- workspaceId: {
651
- type: "string",
652
- description: "Workspace ID (optional if context set)",
653
- },
654
- },
655
- },
656
- },
657
- // Prompt generation
658
- harmony_generate_prompt: {
659
- description: "Generate an AI-ready prompt from a card. Automatically infers role and focus based on labels (bug, feature, design, etc.). Use this to create context-rich prompts for working on cards.",
660
- inputSchema: {
661
- type: "object",
662
- properties: {
663
- cardId: {
664
- type: "string",
665
- description: "Card ID (UUID) to generate prompt from",
666
- },
667
- shortId: {
668
- type: "number",
669
- description: "Card short ID (e.g., 42 for #42) - alternative to cardId",
670
- },
671
- projectId: {
672
- type: "string",
673
- description: "Project ID (required if using shortId, optional if context set)",
674
- },
675
- variant: {
676
- type: "string",
677
- enum: ["analysis", "draft", "execute"],
678
- description: "Prompt variant: analysis (understand/plan), draft (design solution), execute (implement fully). Default: execute",
679
- },
680
- includeSubtasks: {
681
- type: "boolean",
682
- description: "Include subtasks in prompt (default: true)",
683
- },
684
- includeLinks: {
685
- type: "boolean",
686
- description: "Include linked cards in prompt (default: true)",
687
- },
688
- includeDescription: {
689
- type: "boolean",
690
- description: "Include description in prompt (default: true)",
691
- },
692
- customConstraints: {
693
- type: "string",
694
- description: "Additional instructions to append to the prompt",
695
- },
696
- },
697
- required: [],
698
- },
699
- },
700
- // Plan operations
701
- // Memory / Knowledge Graph operations
702
- harmony_remember: {
703
- description: "Store a memory entity (knowledge, pattern, decision, error/solution pair, etc.) for persistence across sessions. Accepts markdown with YAML frontmatter or structured fields.",
704
- inputSchema: {
705
- type: "object",
706
- properties: {
707
- title: { type: "string", description: "Memory title" },
708
- content: {
709
- type: "string",
710
- description: "Memory content (markdown body)",
711
- },
712
- type: {
713
- type: "string",
714
- enum: [
715
- "agent",
716
- "task",
717
- "decision",
718
- "context",
719
- "pattern",
720
- "error",
721
- "solution",
722
- "preference",
723
- "relationship",
724
- "commitment",
725
- "lesson",
726
- "project",
727
- "handoff",
728
- "procedure",
729
- ],
730
- description: "Entity type (default: context)",
731
- },
732
- scope: {
733
- type: "string",
734
- enum: ["private", "project", "workspace", "global"],
735
- description: "Visibility scope (default: project). Private = only creator can see.",
736
- },
737
- tier: {
738
- type: "string",
739
- enum: ["draft", "episode", "reference"],
740
- description: "Memory tier: draft (working notes), episode (session summaries), reference (durable knowledge). Auto-inferred from type if not set.",
741
- },
742
- tags: {
743
- type: "array",
744
- items: { type: "string" },
745
- description: "Tags for categorization and filtering",
746
- },
747
- confidence: {
748
- type: "number",
749
- description: "Confidence score 0-1 (default: 1.0)",
750
- },
751
- metadata: {
752
- type: "object",
753
- description: "Additional structured metadata",
754
- },
755
- workspaceId: {
756
- type: "string",
757
- description: "Workspace ID (optional if context set)",
758
- },
759
- projectId: {
760
- type: "string",
761
- description: "Project ID (optional, required if scope is 'project')",
762
- },
763
- },
764
- required: ["title", "content"],
765
- },
766
- },
767
- harmony_recall: {
768
- description: "Retrieve memories by type, tags, scope, or text query. Returns matching knowledge entities.",
769
- inputSchema: {
770
- type: "object",
771
- properties: {
772
- type: {
773
- type: "string",
774
- enum: [
775
- "agent",
776
- "task",
777
- "decision",
778
- "context",
779
- "pattern",
780
- "error",
781
- "solution",
782
- "preference",
783
- "relationship",
784
- "commitment",
785
- "lesson",
786
- "project",
787
- "handoff",
788
- "procedure",
789
- ],
790
- description: "Filter by entity type",
791
- },
792
- tags: {
793
- type: "array",
794
- items: { type: "string" },
795
- description: "Filter by tags (matches any)",
796
- },
797
- scope: {
798
- type: "string",
799
- enum: ["private", "project", "workspace", "global"],
800
- description: "Filter by scope",
801
- },
802
- query: {
803
- type: "string",
804
- description: "Text query to filter by title",
805
- },
806
- minConfidence: {
807
- type: "number",
808
- description: "Minimum confidence threshold (0-1)",
809
- },
810
- workspaceId: {
811
- type: "string",
812
- description: "Workspace ID (optional if context set)",
813
- },
814
- projectId: {
815
- type: "string",
816
- description: "Project ID (optional)",
817
- },
818
- limit: { type: "number", description: "Max results (default: 20)" },
819
- },
820
- },
821
- },
822
- harmony_update_memory: {
823
- description: "Update an existing memory entity. Can change title, content, tags, confidence, scope, type, or metadata.",
824
- inputSchema: {
825
- type: "object",
826
- properties: {
827
- entityId: {
828
- type: "string",
829
- description: "Memory entity UUID to update",
830
- },
831
- title: { type: "string", description: "New title" },
832
- content: { type: "string", description: "New content (markdown body)" },
833
- type: {
834
- type: "string",
835
- enum: [
836
- "agent",
837
- "task",
838
- "decision",
839
- "context",
840
- "pattern",
841
- "error",
842
- "solution",
843
- "preference",
844
- "relationship",
845
- "commitment",
846
- "lesson",
847
- "project",
848
- "handoff",
849
- "procedure",
850
- ],
851
- description: "New entity type",
852
- },
853
- scope: {
854
- type: "string",
855
- enum: ["private", "project", "workspace", "global"],
856
- description: "New visibility scope",
857
- },
858
- tags: {
859
- type: "array",
860
- items: { type: "string" },
861
- description: "New tags (replaces existing)",
862
- },
863
- confidence: {
864
- type: "number",
865
- description: "New confidence score 0-1",
866
- },
867
- metadata: {
868
- type: "object",
869
- description: "New metadata (merged with existing)",
870
- },
871
- },
872
- required: ["entityId"],
873
- },
874
- },
875
- harmony_forget: {
876
- description: "Archive or delete a memory entity by ID.",
877
- inputSchema: {
878
- type: "object",
879
- properties: {
880
- entityId: {
881
- type: "string",
882
- description: "Memory entity UUID to delete",
883
- },
884
- },
885
- required: ["entityId"],
886
- },
887
- },
888
- harmony_relate: {
889
- description: "Create a typed relationship between two memory entities. Relation types: learned_from, resolved_by, contradicts, supports, depends_on, part_of, caused_by, relates_to.",
890
- inputSchema: {
891
- type: "object",
892
- properties: {
893
- sourceId: {
894
- type: "string",
895
- description: "Source entity UUID",
896
- },
897
- targetId: {
898
- type: "string",
899
- description: "Target entity UUID",
900
- },
901
- relationType: {
902
- type: "string",
903
- enum: [
904
- "learned_from",
905
- "resolved_by",
906
- "contradicts",
907
- "supports",
908
- "depends_on",
909
- "part_of",
910
- "caused_by",
911
- "relates_to",
912
- ],
913
- description: "Type of relationship between entities",
914
- },
915
- confidence: {
916
- type: "number",
917
- description: "Relation confidence 0-1 (default: 1.0)",
918
- },
919
- },
920
- required: ["sourceId", "targetId", "relationType"],
921
- },
922
- },
923
- harmony_memory_search: {
924
- description: "Full-text and semantic search across the knowledge base. Uses hybrid vector+FTS search (when embeddings are available) for best results. Returns entities ranked by relevance.",
925
- inputSchema: {
926
- type: "object",
927
- properties: {
928
- query: {
929
- type: "string",
930
- description: "Search query (full-text search)",
931
- },
932
- type: {
933
- type: "string",
934
- enum: [
935
- "agent",
936
- "task",
937
- "decision",
938
- "context",
939
- "pattern",
940
- "error",
941
- "solution",
942
- "preference",
943
- "relationship",
944
- "commitment",
945
- "lesson",
946
- "project",
947
- "handoff",
948
- "procedure",
949
- ],
950
- description: "Filter results by entity type",
951
- },
952
- workspaceId: {
953
- type: "string",
954
- description: "Workspace ID (optional if context set)",
955
- },
956
- projectId: {
957
- type: "string",
958
- description: "Project ID (optional)",
959
- },
960
- limit: { type: "number", description: "Max results (default: 20)" },
961
- },
962
- required: ["query"],
963
- },
964
- },
965
- // Vault index
966
- harmony_vault_index: {
967
- description: "Get a compact index of all memory entities in the vault. Returns a markdown table with title, type, scope, confidence, tags, summary, and last updated date. Use this for quick overview of stored knowledge.",
968
- inputSchema: {
969
- type: "object",
970
- properties: {
971
- workspaceId: {
972
- type: "string",
973
- description: "Workspace ID (optional if context set)",
974
- },
975
- projectId: {
976
- type: "string",
977
- description: "Project ID (optional, filters to project scope)",
978
- },
979
- type: {
980
- type: "string",
981
- enum: [
982
- "agent",
983
- "task",
984
- "decision",
985
- "context",
986
- "pattern",
987
- "error",
988
- "solution",
989
- "preference",
990
- "relationship",
991
- "commitment",
992
- "lesson",
993
- "project",
994
- "handoff",
995
- "procedure",
996
- ],
997
- description: "Filter by entity type",
998
- },
999
- limit: {
1000
- type: "number",
1001
- description: "Max entities to return (default: 200)",
1002
- },
1003
- },
1004
- required: [],
1005
- },
1006
- },
1007
- // Wiki-link resolution
1008
- harmony_resolve_links: {
1009
- description: "Batch-scan all memory entities in a workspace/project for [[wiki-links]] and auto-create 'relates_to' relations. Use this to retroactively link existing entities.",
1010
- inputSchema: {
1011
- type: "object",
1012
- properties: {
1013
- workspaceId: {
1014
- type: "string",
1015
- description: "Workspace ID (optional if context set)",
1016
- },
1017
- projectId: {
1018
- type: "string",
1019
- description: "Project ID (optional, limits scan to project)",
1020
- },
1021
- },
1022
- required: [],
1023
- },
1024
- },
1025
- // Memory sync
1026
- harmony_sync: {
1027
- description: "Synchronize local markdown memory files with the shared database. Pull downloads remote entities as .md files, push uploads local changes, full does both (pull first, then push). Server wins on conflicts.",
1028
- inputSchema: {
1029
- type: "object",
1030
- properties: {
1031
- direction: {
1032
- type: "string",
1033
- enum: ["pull", "push", "full"],
1034
- description: "Sync direction: pull (remote->local), push (local->remote), full (pull then push). Default: full",
1035
- },
1036
- workspaceId: {
1037
- type: "string",
1038
- description: "Workspace ID (optional if context set)",
1039
- },
1040
- projectId: {
1041
- type: "string",
1042
- description: "Project ID (optional, limits pull scope)",
1043
- },
1044
- },
1045
- required: [],
1046
- },
1047
- },
1048
- // Plan operations
1049
- harmony_list_plans: {
1050
- description: "List plans in a project. Search by name, filter by status or workflow phase. Returns recent plans ordered by last updated.",
1051
- inputSchema: {
1052
- type: "object",
1053
- properties: {
1054
- projectId: {
1055
- type: "string",
1056
- description: "Project ID (optional if context set)",
1057
- },
1058
- search: {
1059
- type: "string",
1060
- description: "Search plans by title (partial match)",
1061
- },
1062
- status: {
1063
- type: "string",
1064
- enum: ["draft", "active", "archived"],
1065
- description: "Filter by plan status",
1066
- },
1067
- },
1068
- required: [],
1069
- },
1070
- },
1071
- harmony_create_plan: {
1072
- description: "Create a new project plan. Use this to upload implementation plans created during planning. Returns a URL where the plan can be viewed and edited in Harmony.",
1073
- inputSchema: {
1074
- type: "object",
1075
- properties: {
1076
- projectId: {
1077
- type: "string",
1078
- description: "Project ID (optional if context set)",
1079
- },
1080
- title: { type: "string", description: "Plan title" },
1081
- content: {
1082
- type: "string",
1083
- description: "Plan content in Markdown format",
1084
- },
1085
- source: {
1086
- type: "string",
1087
- enum: ["user", "agent", "imported"],
1088
- description: "Plan source: agent for AI-generated plans (default: agent)",
1089
- },
1090
- tasks: {
1091
- type: "array",
1092
- items: {
1093
- type: "object",
1094
- properties: {
1095
- content: { type: "string", description: "Task description" },
1096
- priority: {
1097
- type: "string",
1098
- enum: ["high", "medium", "low"],
1099
- description: "Task priority (default: medium)",
1100
- },
1101
- status: {
1102
- type: "string",
1103
- enum: ["pending", "in_progress", "completed"],
1104
- description: "Task status (default: pending)",
1105
- },
1106
- },
1107
- required: ["content"],
1108
- },
1109
- description: "Optional list of tasks to create with the plan",
1110
- },
1111
- },
1112
- required: ["title"],
1113
- },
1114
- },
1115
- harmony_get_plan: {
1116
- description: "Get a plan by ID or by card ID. Returns the plan content and all associated tasks.",
1117
- inputSchema: {
1118
- type: "object",
1119
- properties: {
1120
- planId: { type: "string", description: "Plan ID (UUID)" },
1121
- cardId: {
1122
- type: "string",
1123
- description: "Card ID - get the plan linked to this card",
1124
- },
1125
- },
1126
- required: [],
1127
- },
1128
- },
1129
- harmony_update_plan: {
1130
- description: "Update an existing plan. Can update title, content, or status.",
1131
- inputSchema: {
1132
- type: "object",
1133
- properties: {
1134
- planId: { type: "string", description: "Plan ID to update" },
1135
- title: { type: "string", description: "New title" },
1136
- content: {
1137
- type: "string",
1138
- description: "New content in Markdown format",
1139
- },
1140
- status: {
1141
- type: "string",
1142
- enum: ["draft", "active", "archived"],
1143
- description: "New status",
1144
- },
1145
- },
1146
- required: ["planId"],
1147
- },
1148
- },
1149
- // Plan archival (deprecated: was harmony_advance_plan)
1150
- harmony_advance_plan: {
1151
- description: "Archive a plan. Plan progress is now derived from card states — this tool just archives the plan.",
1152
- inputSchema: {
1153
- type: "object",
1154
- properties: {
1155
- planId: { type: "string", description: "Plan ID to archive" },
1156
- phase: {
1157
- type: "string",
1158
- enum: ["done"],
1159
- description: "Only 'done' is supported (archives the plan)",
1160
- },
1161
- summary: {
1162
- type: "string",
1163
- description: "Optional summary stored as a memory entity.",
1164
- },
1165
- },
1166
- required: ["planId"],
1167
- },
1168
- },
1169
- // Memory lifecycle
1170
- harmony_prune_draft: {
1171
- description: "Remove stale draft memories that haven't been accessed recently. Runs in dry-run mode by default to preview what would be pruned.",
1172
- inputSchema: {
1173
- type: "object",
1174
- properties: {
1175
- workspaceId: {
1176
- type: "string",
1177
- description: "Workspace ID (optional if context set)",
1178
- },
1179
- projectId: {
1180
- type: "string",
1181
- description: "Project ID (optional)",
1182
- },
1183
- dryRun: {
1184
- type: "boolean",
1185
- description: "Preview what would be pruned without deleting (default: true)",
1186
- },
1187
- maxAgeDays: {
1188
- type: "number",
1189
- description: "Maximum age in days for draft memories to keep (default: 30)",
1190
- },
1191
- },
1192
- required: [],
1193
- },
1194
- },
1195
- harmony_consolidate_memories: {
1196
- description: "Consolidate similar draft/episode memories into reference entities. Groups similar memories by embedding similarity and merges clusters. Use dryRun to preview before executing.",
1197
- inputSchema: {
1198
- type: "object",
1199
- properties: {
1200
- workspaceId: {
1201
- type: "string",
1202
- description: "Workspace ID (optional if context set)",
1203
- },
1204
- projectId: {
1205
- type: "string",
1206
- description: "Project ID (optional)",
1207
- },
1208
- dryRun: {
1209
- type: "boolean",
1210
- description: "Preview what would be consolidated without creating entities (default: true)",
1211
- },
1212
- minClusterSize: {
1213
- type: "number",
1214
- description: "Minimum number of similar entities to form a cluster (default: 2)",
1215
- },
1216
- },
1217
- required: [],
1218
- },
1219
- },
1220
- // Context assembly
1221
- harmony_get_context_manifest: {
1222
- description: "Retrieve the context assembly manifest for a given assembly ID to debug what memories were loaded or excluded and why.",
1223
- inputSchema: {
1224
- type: "object",
1225
- properties: {
1226
- assemblyId: {
1227
- type: "string",
1228
- description: "Assembly ID from a previous harmony_generate_prompt call",
1229
- },
1230
- },
1231
- required: ["assemblyId"],
1232
- },
1233
- },
1234
- // Debug & visualization
1235
- harmony_debug_context: {
1236
- description: "Explain why a specific memory was or wasn't included in context assembly for a given task. Returns relevance score breakdown.",
1237
- inputSchema: {
1238
- type: "object",
1239
- properties: {
1240
- entityId: {
1241
- type: "string",
1242
- description: "Memory entity ID to analyze",
1243
- },
1244
- taskContext: {
1245
- type: "string",
1246
- description: "Task context (card title + description) to score against",
1247
- },
1248
- cardLabels: {
1249
- type: "array",
1250
- items: { type: "string" },
1251
- description: "Card labels for tag matching",
1252
- },
1253
- },
1254
- required: ["entityId", "taskContext"],
1255
- },
1256
- },
1257
- harmony_export_memory_graph: {
1258
- description: "Export the knowledge graph as DOT format for Graphviz visualization. Nodes are colored by tier (draft=yellow, episode=blue, reference=green).",
1259
- inputSchema: {
1260
- type: "object",
1261
- properties: {
1262
- workspaceId: {
1263
- type: "string",
1264
- description: "Workspace ID (optional if context set)",
1265
- },
1266
- projectId: {
1267
- type: "string",
1268
- description: "Project ID (optional)",
1269
- },
1270
- limit: {
1271
- type: "number",
1272
- description: "Max entities to include (default: 50)",
1273
- },
1274
- },
1275
- required: [],
1276
- },
1277
- },
1278
- // ============ ONBOARDING TOOLS ============
1279
- harmony_signup: {
1280
- description: "Create a new user account. Returns a JWT session for subsequent authenticated calls. No API key required.",
1281
- inputSchema: {
1282
- type: "object",
1283
- properties: {
1284
- email: { type: "string", description: "User email address" },
1285
- password: {
1286
- type: "string",
1287
- description: "Password (min 8 characters)",
1288
- },
1289
- fullName: { type: "string", description: "User's full name" },
1290
- },
1291
- required: ["email", "password", "fullName"],
1292
- },
1293
- },
1294
- harmony_create_workspace: {
1295
- description: "Create a new workspace. Requires authentication.",
1296
- inputSchema: {
1297
- type: "object",
1298
- properties: {
1299
- name: { type: "string", description: "Workspace name" },
1300
- description: { type: "string", description: "Workspace description" },
1301
- },
1302
- required: ["name"],
1303
- },
1304
- },
1305
- harmony_create_project: {
1306
- description: "Create a new project with template columns. Requires authentication.",
1307
- inputSchema: {
1308
- type: "object",
1309
- properties: {
1310
- workspaceId: { type: "string", description: "Workspace ID" },
1311
- name: { type: "string", description: "Project name" },
1312
- description: { type: "string", description: "Project description" },
1313
- color: { type: "string", description: "Project color" },
1314
- template: {
1315
- type: "string",
1316
- enum: ["kanban", "scrum", "simple"],
1317
- description: "Column template: kanban (To Do/In Progress/Done), scrum (Backlog/Sprint/In Progress/Review/Done), simple (Todo/Done). Default: kanban",
1318
- },
1319
- },
1320
- required: ["workspaceId", "name"],
1321
- },
1322
- },
1323
- harmony_send_invitations: {
1324
- description: "Send workspace invitations to one or more email addresses. Requires admin/owner role.",
1325
- inputSchema: {
1326
- type: "object",
1327
- properties: {
1328
- workspaceId: { type: "string", description: "Workspace ID" },
1329
- emails: {
1330
- type: "array",
1331
- items: { type: "string" },
1332
- description: "Email addresses to invite",
1333
- },
1334
- role: {
1335
- type: "string",
1336
- enum: ["member", "admin"],
1337
- description: "Role for invited users (default: member)",
1338
- },
1339
- sendEmail: {
1340
- type: "boolean",
1341
- description: "Send invitation email (default: false)",
1342
- },
1343
- },
1344
- required: ["workspaceId", "emails"],
1345
- },
1346
- },
1347
- harmony_generate_api_key: {
1348
- description: "Generate an API key for the authenticated user. The raw key is returned once and cannot be retrieved again.",
1349
- inputSchema: {
1350
- type: "object",
1351
- properties: {
1352
- name: {
1353
- type: "string",
1354
- description: "Name for the API key (e.g., 'claude-code')",
1355
- },
1356
- },
1357
- required: ["name"],
1358
- },
1359
- },
1360
- harmony_onboard: {
1361
- description: "Complete end-to-end onboarding: signup → workspace → project → API key. Saves the API key to config automatically. No prior configuration required.",
1362
- inputSchema: {
1363
- type: "object",
1364
- properties: {
1365
- email: { type: "string", description: "User email address" },
1366
- password: {
1367
- type: "string",
1368
- description: "Password (min 8 characters)",
1369
- },
1370
- fullName: { type: "string", description: "User's full name" },
1371
- workspaceName: {
1372
- type: "string",
1373
- description: "Workspace name (default: derived from fullName)",
1374
- },
1375
- projectName: {
1376
- type: "string",
1377
- description: "Project name (default: 'My First Project')",
1378
- },
1379
- template: {
1380
- type: "string",
1381
- enum: ["kanban", "scrum", "simple"],
1382
- description: "Column template (default: kanban)",
1383
- },
1384
- keyName: {
1385
- type: "string",
1386
- description: "API key name (default: 'mcp-agent')",
1387
- },
1388
- },
1389
- required: ["email", "password", "fullName"],
1390
- },
1391
- },
1392
- // Memory tier promotion
1393
- harmony_promote_memory: {
1394
- description: "Promote a memory entity to a higher tier: draft→episode or episode→reference. Tracks promotion reason and source.",
1395
- inputSchema: {
1396
- type: "object",
1397
- properties: {
1398
- entityId: {
1399
- type: "string",
1400
- description: "Memory entity ID to promote",
1401
- },
1402
- targetTier: {
1403
- type: "string",
1404
- enum: ["episode", "reference"],
1405
- description: "Target tier to promote to",
1406
- },
1407
- reason: {
1408
- type: "string",
1409
- description: "Reason for promotion (e.g., 'proven useful across 5+ sessions')",
1410
- },
1411
- },
1412
- required: ["entityId", "targetTier", "reason"],
1413
- },
1414
- },
1415
- // Vector embeddings
1416
- harmony_backfill_embeddings: {
1417
- description: "Generate vector embeddings for knowledge entities that are missing them. Run this after importing memories or when semantic search isn't finding expected results. Idempotent — only processes entities without embeddings.",
1418
- inputSchema: {
1419
- type: "object",
1420
- properties: {
1421
- workspaceId: {
1422
- type: "string",
1423
- description: "Workspace ID (optional if context set)",
1424
- },
1425
- batchSize: {
1426
- type: "number",
1427
- description: "Number of entities to process per batch (default: 50)",
1428
- },
1429
- },
1430
- required: [],
1431
- },
1432
- },
1433
- harmony_backfill_relations: {
1434
- description: "Retroactively create semantic relations across all existing memory entities. Iterates entities and runs graph expansion on each. Run after harmony_backfill_embeddings to connect a disconnected knowledge graph.",
1435
- inputSchema: {
1436
- type: "object",
1437
- properties: {
1438
- workspaceId: {
1439
- type: "string",
1440
- description: "Workspace ID (optional if context set)",
1441
- },
1442
- projectId: {
1443
- type: "string",
1444
- description: "Project ID (optional, limits to project)",
1445
- },
1446
- maxRelationsPerEntity: {
1447
- type: "number",
1448
- description: "Maximum relations to create per entity (default: 3)",
1449
- },
1450
- },
1451
- required: [],
1452
- },
1453
- },
1454
- harmony_cleanup_memories: {
1455
- 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.",
1456
- inputSchema: {
1457
- type: "object",
1458
- properties: {
1459
- workspaceId: {
1460
- type: "string",
1461
- description: "Workspace ID (optional if context set)",
1462
- },
1463
- projectId: {
1464
- type: "string",
1465
- description: "Project ID (optional)",
1466
- },
1467
- dryRun: {
1468
- type: "boolean",
1469
- description: "Preview cleanup without executing changes (default: true)",
1470
- },
1471
- steps: {
1472
- type: "array",
1473
- items: {
1474
- type: "string",
1475
- enum: ["prune", "consolidate", "orphans", "duplicates", "backfill"],
1476
- },
1477
- description: "Which cleanup steps to run (default: all). Options: prune, consolidate, orphans, duplicates, backfill.",
1478
- },
1479
- maxAgeDays: {
1480
- type: "number",
1481
- description: "Max age in days for stale draft pruning (default: 30)",
1482
- },
1483
- minClusterSize: {
1484
- type: "number",
1485
- description: "Min cluster size for consolidation (default: 3)",
1486
- },
1487
- orphanAgeDays: {
1488
- type: "number",
1489
- description: "Min age in days for orphan detection (default: 14)",
1490
- },
1491
- },
1492
- required: [],
1493
- },
1494
- },
1495
- };
1496
- // Resource URIs
1497
- const RESOURCES = [
1498
- {
1499
- uri: "harmony://context",
1500
- name: "Current Context",
1501
- description: "Current active workspace and project",
1502
- mimeType: "application/json",
1503
- },
1504
- ];
1505
- /**
1506
- * Reusable end-session pipeline: learning extraction, feedback scoring, lifecycle maintenance.
1507
- * Called by both explicit harmony_end_agent_session and auto-session timeout/card-switch.
1508
- */
1509
- export async function runEndSessionPipeline(client, deps, cardId, sessionStatus, endProgressPercent, sessionData) {
1510
- let learningsExtracted = 0;
1511
- let feedbackAdjusted = 0;
1512
- let maintenanceResult;
1513
- // Fetch card details for learning extraction
1514
- let cardTitle = "";
1515
- let cardLabels = [];
1516
- let cardDescription = "";
1517
- let cardSubtasks = [];
1518
- try {
1519
- const { card } = await client.getCard(cardId);
1520
- const typedCard = card;
1521
- cardTitle = typedCard.title || "";
1522
- cardLabels = (typedCard.labels || []).map((l) => l.name);
1523
- cardDescription = typedCard.description || "";
1524
- cardSubtasks = (typedCard.subtasks || []).map((s) => ({
1525
- title: s.title,
1526
- done: s.done,
1527
- }));
1528
- // Remove "agent" label when session is completed (not paused)
1529
- if (sessionStatus === "completed" && typedCard.labels?.length) {
1530
- const agentLabel = typedCard.labels.find((l) => l.name.toLowerCase() === "agent");
1531
- if (agentLabel) {
1532
- await client.removeLabelFromCard(cardId, agentLabel.id);
1533
- }
1534
- }
1535
- }
1536
- catch {
1537
- // Card fetch failed, continue with defaults
1538
- }
1539
- // Active learning: extract memories from session
1540
- try {
1541
- let sessionDurationMs;
1542
- if (sessionData?.created_at) {
1543
- const startTime = new Date(sessionData.created_at).getTime();
1544
- if (!Number.isNaN(startTime)) {
1545
- sessionDurationMs = Date.now() - startTime;
1546
- }
1547
- }
1548
- const sessionContext = {
1549
- cardId,
1550
- cardTitle,
1551
- cardLabels,
1552
- agentIdentifier: sessionData?.agent_identifier || "unknown",
1553
- agentName: sessionData?.agent_name || "Unknown Agent",
1554
- status: sessionStatus,
1555
- progressPercent: endProgressPercent,
1556
- blockers: sessionData?.blockers || undefined,
1557
- currentTask: sessionData?.current_task || undefined,
1558
- sessionDurationMs,
1559
- cardDescription: cardDescription || undefined,
1560
- cardSubtasks: cardSubtasks.length > 0 ? cardSubtasks : undefined,
1561
- };
1562
- const learningResult = await extractLearnings(client, sessionContext);
1563
- learningsExtracted = learningResult.count;
1564
- }
1565
- catch {
1566
- // Learning extraction failed, non-fatal
1567
- }
1568
- // Agent performance profile: refresh materialized view + upsert knowledge entity
1569
- try {
1570
- const workspaceId = deps.getActiveWorkspaceId();
1571
- const agentId = sessionData?.agent_identifier || "unknown";
1572
- if (workspaceId && agentId !== "unknown") {
1573
- // Fire-and-forget: refresh + upsert agent profile entity
1574
- (async () => {
1575
- try {
1576
- await client.refreshAgentProfiles(workspaceId);
1577
- const { profile } = await client.getAgentProfile(workspaceId, agentId);
1578
- if (profile) {
1579
- const p = profile;
1580
- const title = `Agent Profile: ${agentId}`;
1581
- const content = [
1582
- `## ${agentId} Performance Profile`,
1583
- "",
1584
- `- **Total sessions:** ${p.total_sessions}`,
1585
- `- **Completed:** ${p.completed_sessions} (${p.completion_rate_pct}%)`,
1586
- `- **Paused:** ${p.paused_sessions}`,
1587
- `- **Blocked:** ${p.blocked_sessions}`,
1588
- `- **Avg duration:** ${Math.round(Number(p.avg_active_duration_ms || 0) / 1000)}s`,
1589
- `- **Avg progress:** ${p.avg_completion_progress}%`,
1590
- `- **First session:** ${p.first_session_at}`,
1591
- `- **Last session:** ${p.last_session_at}`,
1592
- ].join("\n");
1593
- // Find existing agent entity to update, or create new
1594
- const existing = await client.listMemoryEntities({
1595
- workspace_id: workspaceId,
1596
- type: "agent",
1597
- limit: 50,
1598
- });
1599
- const entities = (existing.entities || []);
1600
- const match = entities.find((e) => e.title === title || e.agent_identifier === agentId);
1601
- if (match) {
1602
- await client.updateMemoryEntity(match.id, {
1603
- content,
1604
- confidence: 1.0,
1605
- metadata: p,
1606
- });
1607
- }
1608
- else {
1609
- await client.createMemoryEntity({
1610
- workspace_id: workspaceId,
1611
- type: "agent",
1612
- scope: "workspace",
1613
- memory_tier: "reference",
1614
- title,
1615
- content,
1616
- confidence: 1.0,
1617
- tags: ["agent-profile", agentId],
1618
- agent_identifier: agentId,
1619
- metadata: p,
1620
- });
1621
- }
1622
- }
1623
- }
1624
- catch {
1625
- // Non-fatal: profile upsert failed
1626
- }
1627
- })();
1628
- }
1629
- }
1630
- catch {
1631
- // Non-fatal: agent profile refresh failed
1632
- }
1633
- // Feedback-driven scoring
1634
- try {
1635
- const feedbackResult = await recordContextFeedback(client, cardId, sessionStatus, endProgressPercent, (sessionData?.blockers?.length ?? 0) > 0);
1636
- feedbackAdjusted = feedbackResult.adjusted;
1637
- }
1638
- catch {
1639
- // Feedback recording failed, non-fatal
1640
- }
1641
- // Lifecycle maintenance
1642
- try {
1643
- const workspaceId = deps.getActiveWorkspaceId();
1644
- if (workspaceId) {
1645
- const projectId = deps.getActiveProjectId() || undefined;
1646
- maintenanceResult = await runLifecycleMaintenance(client, workspaceId, projectId);
1647
- // Auto-consolidation: if workspace has many draft/episode entities
1648
- const listResult = await client.listMemoryEntities({
1649
- workspace_id: workspaceId,
1650
- project_id: projectId,
1651
- limit: 100,
1652
- });
1653
- const draftEpisodeCount = (listResult.entities || []).filter((e) => e.memory_tier === "draft" || e.memory_tier === "episode").length;
1654
- if (draftEpisodeCount > 50) {
1655
- consolidateMemories(client, workspaceId, projectId, {
1656
- dryRun: false,
1657
- minClusterSize: 2,
1658
- }).catch(() => { });
1659
- }
1660
- }
1661
- }
1662
- catch {
1663
- // Lifecycle maintenance failed, non-fatal
1664
- }
1665
- return {
1666
- learningsExtracted,
1667
- feedbackAdjusted,
1668
- ...(maintenanceResult && { maintenance: maintenanceResult }),
1669
- };
1670
- }
1671
- /**
1672
- * Register MCP tool/resource handlers on a Server instance using the given deps.
1673
- * This is shared between stdio (HarmonyMCPServer) and remote (HTTP) transports.
1674
- */
1675
- export function registerHandlers(server, deps) {
1676
- // List available tools
1677
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
1678
- tools: Object.entries(TOOLS).map(([name, tool]) => ({
1679
- name,
1680
- description: tool.description,
1681
- inputSchema: tool.inputSchema,
1682
- })),
1683
- }));
1684
- // Handle tool calls
1685
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
1686
- const { name, arguments: args } = request.params;
1687
- // Auto-session pre-hook: track activity on card-related tools
1688
- const toolArgs = args || {};
1689
- const cardIdArg = toolArgs.cardId;
1690
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1691
- if (cardIdArg && UUID_RE.test(cardIdArg) && deps.isConfigured()) {
1692
- const isAutoStartTrigger = AUTO_START_TRIGGERS.has(name);
1693
- trackActivity(cardIdArg, {
1694
- autoStart: isAutoStartTrigger,
1695
- client: deps.getClient(),
1696
- }).catch(() => { }); // fire-and-forget
1697
- }
1698
- try {
1699
- const result = await handleToolCall(name, toolArgs, deps);
1700
- // Auto-session post-hook: for generate_prompt resolved via shortId (no cardId in args)
1701
- if (name === "harmony_generate_prompt" &&
1702
- !cardIdArg &&
1703
- deps.isConfigured()) {
1704
- try {
1705
- const parsed = typeof result === "object" && result !== null ? result : {};
1706
- const resolvedCardId = parsed.cardId;
1707
- if (resolvedCardId && UUID_RE.test(resolvedCardId)) {
1708
- trackActivity(resolvedCardId, {
1709
- autoStart: true,
1710
- client: deps.getClient(),
1711
- }).catch(() => { });
1712
- }
1713
- }
1714
- catch {
1715
- // Non-fatal
1716
- }
1717
- }
1718
- const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
1719
- return {
1720
- content: [{ type: "text", text }],
1721
- };
1722
- }
1723
- catch (error) {
1724
- return {
1725
- content: [
1726
- {
1727
- type: "text",
1728
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
1729
- },
1730
- ],
1731
- isError: true,
1732
- };
1733
- }
1734
- });
1735
- // List resources
1736
- server.setRequestHandler(ListResourcesRequestSchema, async () => ({
1737
- resources: RESOURCES,
1738
- }));
1739
- // Read resource
1740
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1741
- const { uri } = request.params;
1742
- if (uri === "harmony://context") {
1743
- return {
1744
- contents: [
1745
- {
1746
- uri,
1747
- mimeType: "application/json",
1748
- text: JSON.stringify({
1749
- configured: deps.isConfigured(),
1750
- activeWorkspaceId: deps.getActiveWorkspaceId(),
1751
- activeProjectId: deps.getActiveProjectId(),
1752
- }, null, 2),
1753
- },
1754
- ],
1755
- };
1756
- }
1757
- throw new Error(`Unknown resource: ${uri}`);
1758
- });
1759
- }
1760
- /** Resolve a column name to its ID. Prefers exact match, falls back to substring. */
1761
- async function resolveColumnByName(client, projectId, columnName) {
1762
- const board = await client.getBoard(projectId, { summary: true });
1763
- const columns = board.columns;
1764
- const lower = columnName.toLowerCase();
1765
- const col = columns.find((c) => c.name.toLowerCase() === lower) ||
1766
- columns.find((c) => c.name.toLowerCase().includes(lower));
1767
- if (!col) {
1768
- const available = columns.map((c) => c.name).join(", ");
1769
- throw new Error(`Column "${columnName}" not found. Available columns: ${available}`);
1770
- }
1771
- return col;
1772
- }
1773
- async function handleToolCall(name, args, deps) {
1774
- // Unauthenticated tools that don't require an API key
1775
- const unauthenticatedTools = ["harmony_signup", "harmony_onboard"];
1776
- if (!unauthenticatedTools.includes(name) && !deps.isConfigured()) {
1777
- throw new Error('Not configured. Run "npx @gethmy/mcp setup" to set your API key.\n' +
1778
- "You can generate an API key at https://app.gethmy.com → Settings → API Keys.\n" +
1779
- 'Or use "harmony_onboard" to create an account and configure automatically.');
1780
- }
1781
- const client = deps.isConfigured()
1782
- ? deps.getClient()
1783
- : null;
1784
- // Helper to get project ID from args or context
1785
- const getProjectId = () => {
1786
- const id = args.projectId || deps.getActiveProjectId();
1787
- if (!id) {
1788
- throw new Error("No project specified. Use harmony_set_project_context or provide projectId.");
1789
- }
1790
- return id;
1791
- };
1792
- const getWorkspaceId = () => {
1793
- const id = args.workspaceId || deps.getActiveWorkspaceId();
1794
- if (!id) {
1795
- throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
1796
- }
1797
- return id;
1798
- };
1799
- switch (name) {
1800
- // Card operations
1801
- case "harmony_create_card": {
1802
- const title = z.string().min(1).max(500).parse(args.title);
1803
- const projectId = args.projectId || getProjectId();
1804
- const result = await client.createCard(projectId, {
1805
- title,
1806
- columnId: args.columnId,
1807
- description: args.description,
1808
- priority: args.priority,
1809
- assigneeId: args.assigneeId,
1810
- });
1811
- return { success: true, ...result };
1812
- }
1813
- case "harmony_update_card": {
1814
- const cardId = z.string().uuid().parse(args.cardId);
1815
- const result = await client.updateCard(cardId, {
1816
- title: args.title,
1817
- description: args.description,
1818
- priority: args.priority,
1819
- assigneeId: args.assigneeId,
1820
- dueDate: args.dueDate,
1821
- done: args.done,
1822
- });
1823
- return { success: true, ...result };
1824
- }
1825
- case "harmony_move_card": {
1826
- const cardId = z.string().uuid().parse(args.cardId);
1827
- const position = args.position !== undefined
1828
- ? z.number().int().min(0).parse(args.position)
1829
- : undefined;
1830
- // Resolve columnId — accept UUID directly or resolve from columnName
1831
- let columnId;
1832
- let resolvedProjectId;
1833
- if (args.columnId) {
1834
- columnId = z.string().uuid().parse(args.columnId);
1835
- }
1836
- else if (args.columnName) {
1837
- const columnName = z.string().parse(args.columnName);
1838
- const { card: cardForProject } = await client.getCard(cardId);
1839
- resolvedProjectId = cardForProject
1840
- ?.project_id;
1841
- if (!resolvedProjectId)
1842
- throw new Error("Card has no project");
1843
- const col = await resolveColumnByName(client, resolvedProjectId, columnName);
1844
- columnId = col.id;
1845
- }
1846
- else {
1847
- throw new Error("Either columnId or columnName is required");
1848
- }
1849
- const result = await client.moveCard(cardId, columnId, position);
1850
- // Auto-end active agent session when moving to Review or Done
1851
- let sessionEnded = false;
1852
- try {
1853
- const { card } = result;
1854
- const projectId = card?.project_id || resolvedProjectId;
1855
- if (projectId) {
1856
- const board = await client.getBoard(projectId, { summary: true });
1857
- const columns = board.columns;
1858
- const destCol = columns.find((c) => c.id === columnId);
1859
- const colName = destCol?.name?.toLowerCase() || "";
1860
- const isTerminal = destCol?.mark_cards_done ||
1861
- colName === "done" ||
1862
- colName === "completed" ||
1863
- colName === "review";
1864
- if (isTerminal) {
1865
- const { session } = await client.getAgentSession(cardId);
1866
- if (session) {
1867
- await client.endAgentSession(cardId, { status: "completed" });
1868
- untrack(cardId);
1869
- sessionEnded = true;
1870
- }
1871
- }
1872
- }
1873
- }
1874
- catch {
1875
- // Non-fatal — the move already succeeded
1876
- }
1877
- return { success: true, sessionEnded, ...result };
1878
- }
1879
- case "harmony_archive_card": {
1880
- const cardId = z.string().uuid().parse(args.cardId);
1881
- const result = await client.archiveCard(cardId);
1882
- return { success: true, ...result };
1883
- }
1884
- case "harmony_unarchive_card": {
1885
- const cardId = z.string().uuid().parse(args.cardId);
1886
- const result = await client.unarchiveCard(cardId);
1887
- return { success: true, ...result };
1888
- }
1889
- case "harmony_delete_card": {
1890
- const cardId = z.string().uuid().parse(args.cardId);
1891
- await client.deleteCard(cardId);
1892
- return { success: true };
1893
- }
1894
- case "harmony_assign_card": {
1895
- const cardId = z.string().uuid().parse(args.cardId);
1896
- const assigneeId = args.assigneeId
1897
- ? z.string().uuid().parse(args.assigneeId)
1898
- : null;
1899
- const result = await client.updateCard(cardId, { assigneeId });
1900
- return { success: true, ...result };
1901
- }
1902
- case "harmony_search_cards": {
1903
- const query = z.string().min(1).max(500).parse(args.query);
1904
- const result = await client.searchCards(query, {
1905
- projectId: args.projectId,
1906
- });
1907
- return { success: true, ...result, count: result.cards.length };
1908
- }
1909
- case "harmony_get_card": {
1910
- const cardId = z.string().uuid().parse(args.cardId);
1911
- const result = await client.getCard(cardId);
1912
- return { success: true, ...result };
1913
- }
1914
- case "harmony_get_card_by_short_id": {
1915
- const shortId = z.number().int().positive().parse(args.shortId);
1916
- const projectId = args.projectId || getProjectId();
1917
- const result = await client.getCardByShortId(projectId, shortId);
1918
- return { success: true, ...result };
1919
- }
1920
- // Column operations
1921
- case "harmony_create_column": {
1922
- const name = z.string().min(1).max(100).parse(args.name);
1923
- const projectId = args.projectId || getProjectId();
1924
- const result = await client.createColumn(projectId, name);
1925
- return { success: true, ...result };
1926
- }
1927
- case "harmony_update_column": {
1928
- const columnId = z.string().uuid().parse(args.columnId);
1929
- const name = z.string().min(1).max(100).parse(args.name);
1930
- const result = await client.updateColumn(columnId, name);
1931
- return { success: true, ...result };
1932
- }
1933
- case "harmony_delete_column": {
1934
- const columnId = z.string().uuid().parse(args.columnId);
1935
- await client.deleteColumn(columnId);
1936
- return { success: true };
1937
- }
1938
- // Label operations
1939
- case "harmony_create_label": {
1940
- const name = z.string().min(1).max(50).parse(args.name);
1941
- const color = z
1942
- .string()
1943
- .regex(/^#[0-9a-fA-F]{6}$/)
1944
- .parse(args.color);
1945
- const projectId = args.projectId || getProjectId();
1946
- // Check existing labels to avoid duplicates (case-insensitive)
1947
- const board = await client.getBoard(projectId, { summary: true });
1948
- const existing = board.labels.find((l) => l.name.toLowerCase() === name.toLowerCase());
1949
- if (existing) {
1950
- return { success: true, ...existing, deduplicated: true };
1951
- }
1952
- const result = await client.createLabel(projectId, { name, color });
1953
- return { success: true, ...result };
1954
- }
1955
- case "harmony_add_label_to_card": {
1956
- const cardId = z.string().uuid().parse(args.cardId);
1957
- let labelId = args.labelId
1958
- ? z.string().uuid().parse(args.labelId)
1959
- : undefined;
1960
- const labelName = args.labelName
1961
- ? z.string().min(1).max(100).parse(args.labelName)
1962
- : undefined;
1963
- // If labelName provided without labelId, look up or create the label
1964
- if (!labelId && labelName) {
1965
- const { card } = await client.getCard(cardId);
1966
- const projectId = card.project_id;
1967
- if (!projectId) {
1968
- throw new Error("Cannot resolve label by name: card has no project_id");
1969
- }
1970
- if (projectId) {
1971
- const board = await client.getBoard(projectId, { summary: true });
1972
- const labels = board.labels;
1973
- const existing = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
1974
- if (existing) {
1975
- labelId = existing.id;
1976
- }
1977
- else {
1978
- const created = await client.createLabel(projectId, {
1979
- name: labelName,
1980
- color: "#57b8a5",
1981
- });
1982
- labelId = created.label.id;
1983
- }
1984
- }
1985
- }
1986
- if (!labelId) {
1987
- throw new Error("Either labelId or labelName must be provided");
1988
- }
1989
- await client.addLabelToCard(cardId, labelId);
1990
- return { success: true };
1991
- }
1992
- case "harmony_remove_label_from_card": {
1993
- const cardId = z.string().uuid().parse(args.cardId);
1994
- const labelId = z.string().uuid().parse(args.labelId);
1995
- await client.removeLabelFromCard(cardId, labelId);
1996
- return { success: true };
1997
- }
1998
- // Card link operations
1999
- case "harmony_add_link_to_card": {
2000
- const sourceCardId = z.string().uuid().parse(args.sourceCardId);
2001
- const targetCardId = z.string().uuid().parse(args.targetCardId);
2002
- const linkType = z
2003
- .enum(["relates_to", "blocks", "duplicates", "is_part_of"])
2004
- .parse(args.linkType);
2005
- const result = await client.addLinkToCard(sourceCardId, targetCardId, linkType);
2006
- return { success: true, ...result };
2007
- }
2008
- case "harmony_remove_link_from_card": {
2009
- const linkId = z.string().uuid().parse(args.linkId);
2010
- await client.removeLinkFromCard(linkId);
2011
- return { success: true };
2012
- }
2013
- case "harmony_get_card_links": {
2014
- const cardId = z.string().uuid().parse(args.cardId);
2015
- const result = await client.getCardLinks(cardId);
2016
- return result;
2017
- }
2018
- // Subtask operations
2019
- case "harmony_create_subtask": {
2020
- const cardId = z.string().uuid().parse(args.cardId);
2021
- const title = z.string().min(1).max(500).parse(args.title);
2022
- const result = await client.createSubtask(cardId, title);
2023
- return { success: true, ...result };
2024
- }
2025
- case "harmony_toggle_subtask": {
2026
- const subtaskId = z.string().uuid().parse(args.subtaskId);
2027
- const result = await client.toggleSubtask(subtaskId);
2028
- return { success: true, ...result };
2029
- }
2030
- case "harmony_delete_subtask": {
2031
- const subtaskId = z.string().uuid().parse(args.subtaskId);
2032
- await client.deleteSubtask(subtaskId);
2033
- return { success: true };
2034
- }
2035
- // Context operations
2036
- case "harmony_list_workspaces": {
2037
- const result = await client.listWorkspaces();
2038
- return { success: true, ...result };
2039
- }
2040
- case "harmony_list_projects": {
2041
- const workspaceId = getWorkspaceId();
2042
- const result = await client.listProjects(workspaceId);
2043
- return { success: true, ...result };
2044
- }
2045
- case "harmony_get_board": {
2046
- const projectId = getProjectId();
2047
- const options = {};
2048
- // MCP tool arguments come as strings, parse numbers
2049
- if (args.limit !== undefined)
2050
- options.limit = Number(args.limit);
2051
- if (args.offset !== undefined)
2052
- options.offset = Number(args.offset);
2053
- if (args.columnId)
2054
- options.columnId = String(args.columnId);
2055
- if (args.summary === true || args.summary === "true")
2056
- options.summary = true;
2057
- if (args.includeArchived === true || args.includeArchived === "true")
2058
- options.includeArchived = true;
2059
- const result = await client.getBoard(projectId, options);
2060
- return { success: true, board: result };
2061
- }
2062
- case "harmony_get_workspace_members": {
2063
- const workspaceId = getWorkspaceId();
2064
- const result = await client.getWorkspaceMembers(workspaceId);
2065
- return { success: true, ...result };
2066
- }
2067
- case "harmony_set_workspace_context": {
2068
- const workspaceId = z.string().uuid().parse(args.workspaceId);
2069
- deps.setActiveWorkspace(workspaceId);
2070
- return { success: true, activeWorkspaceId: workspaceId };
2071
- }
2072
- case "harmony_set_project_context": {
2073
- const projectId = z.string().uuid().parse(args.projectId);
2074
- deps.setActiveProject(projectId);
2075
- return { success: true, activeProjectId: projectId };
2076
- }
2077
- case "harmony_get_context": {
2078
- return {
2079
- success: true,
2080
- context: {
2081
- activeWorkspaceId: deps.getActiveWorkspaceId(),
2082
- activeProjectId: deps.getActiveProjectId(),
2083
- },
2084
- };
2085
- }
2086
- // Natural language processing
2087
- case "harmony_process_command": {
2088
- const command = z.string().min(1).max(500).parse(args.command);
2089
- const result = await client.processNLU({
2090
- command,
2091
- projectId: args.projectId || deps.getActiveProjectId() || undefined,
2092
- workspaceId: args.workspaceId ||
2093
- deps.getActiveWorkspaceId() ||
2094
- undefined,
2095
- language: args.language || "en-US",
2096
- execute: args.execute === true,
2097
- });
2098
- return { success: true, ...result };
2099
- }
2100
- // Agent context operations
2101
- case "harmony_start_agent_session": {
2102
- const cardId = z.string().uuid().parse(args.cardId);
2103
- const agentIdentifier = z
2104
- .string()
2105
- .min(1)
2106
- .max(100)
2107
- .parse(args.agentIdentifier);
2108
- const agentName = z.string().min(1).max(100).parse(args.agentName);
2109
- const moveToColumn = args.moveToColumn;
2110
- const addLabels = args.addLabels;
2111
- let movedTo = null;
2112
- const labelsAdded = [];
2113
- // Handle card setup if moveToColumn or addLabels requested
2114
- if (moveToColumn || addLabels?.length) {
2115
- try {
2116
- const { card } = await client.getCard(cardId);
2117
- const projectId = card.project_id;
2118
- if (projectId) {
2119
- const board = await client.getBoard(projectId, {
2120
- summary: true,
2121
- });
2122
- const columns = board.columns;
2123
- const labels = board.labels;
2124
- if (moveToColumn) {
2125
- const col = columns.find((c) => c.name.toLowerCase().includes(moveToColumn.toLowerCase()));
2126
- if (col) {
2127
- await client.moveCard(cardId, col.id);
2128
- movedTo = col.name;
2129
- }
2130
- }
2131
- if (addLabels?.length) {
2132
- for (const labelName of addLabels) {
2133
- let label = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
2134
- if (!label && projectId) {
2135
- const created = await client.createLabel(projectId, {
2136
- name: labelName,
2137
- color: "#57b8a5",
2138
- });
2139
- label = created
2140
- .label;
2141
- }
2142
- if (label) {
2143
- await client.addLabelToCard(cardId, label.id);
2144
- labelsAdded.push(label.name ?? labelName);
2145
- }
2146
- }
2147
- }
2148
- }
2149
- }
2150
- catch {
2151
- // Setup failed, continue with session start
2152
- }
2153
- }
2154
- // Auto-assign to configured user if userEmail is set
2155
- let assignedTo = null;
2156
- const userEmail = deps.getUserEmail();
2157
- if (userEmail) {
2158
- try {
2159
- const workspaceId = deps.getActiveWorkspaceId();
2160
- if (workspaceId) {
2161
- const { members } = await client.getWorkspaceMembers(workspaceId);
2162
- const user = members.find((m) => m.email === userEmail);
2163
- if (user) {
2164
- await client.updateCard(cardId, { assigneeId: user.id });
2165
- assignedTo = user.email;
2166
- }
2167
- }
2168
- }
2169
- catch {
2170
- // Auto-assign failed, continue without assignment
2171
- }
2172
- }
2173
- const result = await client.startAgentSession(cardId, {
2174
- agentIdentifier,
2175
- agentName,
2176
- status: "working",
2177
- currentTask: args.currentTask,
2178
- estimatedMinutesRemaining: args.estimatedMinutesRemaining,
2179
- });
2180
- // Mark as explicit so auto-session won't interfere
2181
- markExplicit(cardId, { agentIdentifier, agentName });
2182
- // Initialize memory session tracking for action visibility
2183
- initMemorySession(cardId, agentIdentifier, agentName);
2184
- // Prefetch relevant context (non-blocking, best-effort)
2185
- let prefetchedMemoryIds = [];
2186
- try {
2187
- const workspaceId = deps.getActiveWorkspaceId();
2188
- if (workspaceId) {
2189
- // Get card details for context
2190
- const { card } = await client.getCard(cardId);
2191
- const typedCard = card;
2192
- const cardLabels = (typedCard.labels || []).map((l) => l.name);
2193
- const taskContext = [
2194
- typedCard.title || "",
2195
- typedCard.description || "",
2196
- ]
2197
- .filter(Boolean)
2198
- .join(" ");
2199
- // Assemble context to find seed entities
2200
- const assembled = await assembleContext({
2201
- workspaceId,
2202
- projectId: getActiveProjectId() || undefined,
2203
- taskContext,
2204
- cardLabels,
2205
- cardId,
2206
- tokenBudget: 2000, // Smaller budget for prefetch
2207
- client,
2208
- });
2209
- prefetchedMemoryIds = assembled.memories.map((m) => m.id);
2210
- // Track assembly for feedback loop
2211
- if (assembled.manifest.assemblyId) {
2212
- cacheManifest(assembled.manifest);
2213
- trackSessionAssembly(cardId, assembled.manifest.assemblyId);
2214
- }
2215
- // Walk graph from seed entities to discover more context
2216
- if (prefetchedMemoryIds.length > 0) {
2217
- const graphResult = await discoverRelatedContext(client, prefetchedMemoryIds.slice(0, 5), // Limit seeds
2218
- 2, // 2 hops
2219
- 10);
2220
- const additionalIds = graphResult.entities
2221
- .map((e) => e.id)
2222
- .filter((id) => !prefetchedMemoryIds.includes(id));
2223
- prefetchedMemoryIds.push(...additionalIds);
2224
- }
2225
- }
2226
- }
2227
- catch {
2228
- // Prefetch failed, non-fatal
2229
- }
2230
- return {
2231
- success: true,
2232
- assignedTo,
2233
- movedTo,
2234
- labelsAdded,
2235
- prefetchedMemoryCount: prefetchedMemoryIds.length,
2236
- ...result,
2237
- };
2238
- }
2239
- case "harmony_update_agent_progress": {
2240
- const cardId = z.string().uuid().parse(args.cardId);
2241
- const agentIdentifier = z
2242
- .string()
2243
- .min(1)
2244
- .max(100)
2245
- .parse(args.agentIdentifier);
2246
- const agentName = z.string().min(1).max(100).parse(args.agentName);
2247
- const progressPercent = args.progressPercent !== undefined
2248
- ? z.number().min(0).max(100).parse(args.progressPercent)
2249
- : undefined;
2250
- // Merge any pending memory actions into the progress update
2251
- const callerRecentActions = args.recentActions;
2252
- const memSession = getMemorySession(cardId);
2253
- let mergedRecentActions;
2254
- if (memSession?.dirty) {
2255
- mergedRecentActions = mergeMemoryActionsInto(cardId, callerRecentActions || []);
2256
- }
2257
- else if (callerRecentActions) {
2258
- mergedRecentActions = callerRecentActions;
2259
- }
2260
- const result = await client.updateAgentProgress(cardId, {
2261
- agentIdentifier,
2262
- agentName,
2263
- status: args.status,
2264
- progressPercent,
2265
- currentTask: args.currentTask,
2266
- blockers: args.blockers,
2267
- estimatedMinutesRemaining: args.estimatedMinutesRemaining,
2268
- ...(mergedRecentActions && { recentActions: mergedRecentActions }),
2269
- });
2270
- // Mid-session learning extraction (fire-and-forget)
2271
- let midSessionLearnings = 0;
2272
- try {
2273
- const { card } = await client.getCard(cardId);
2274
- const typedCard = card;
2275
- const midResult = await extractMidSessionLearnings(client, {
2276
- cardId,
2277
- cardTitle: typedCard.title || "",
2278
- agentIdentifier,
2279
- agentName,
2280
- currentTask: args.currentTask,
2281
- status: args.status,
2282
- blockers: args.blockers,
2283
- progressPercent,
2284
- });
2285
- midSessionLearnings = midResult.count;
2286
- }
2287
- catch {
2288
- // Non-fatal: mid-session learning extraction failure
2289
- }
2290
- return { success: true, midSessionLearnings, ...result };
2291
- }
2292
- case "harmony_end_agent_session": {
2293
- const cardId = z.string().uuid().parse(args.cardId);
2294
- const moveToColumn = args.moveToColumn;
2295
- const sessionStatus = args.status || "completed";
2296
- const endProgressPercent = args.progressPercent !== undefined
2297
- ? z.number().min(0).max(100).parse(args.progressPercent)
2298
- : undefined;
2299
- // Final flush of any pending memory actions before ending the session
2300
- await flushMemoryActions(client, cardId);
2301
- cleanupMemorySession(cardId);
2302
- // End the session — tolerate failure (e.g., session already ended or not found)
2303
- let result = { session: null };
2304
- let sessionEndError = null;
2305
- try {
2306
- result = await client.endAgentSession(cardId, {
2307
- status: sessionStatus,
2308
- progressPercent: endProgressPercent,
2309
- });
2310
- }
2311
- catch (err) {
2312
- sessionEndError =
2313
- err instanceof Error ? err.message : "Failed to end session";
2314
- }
2315
- // Remove from auto-session tracking regardless
2316
- untrack(cardId);
2317
- let movedTo = null;
2318
- try {
2319
- const { card } = await client.getCard(cardId);
2320
- const typedCard = card;
2321
- const projectId = typedCard.project_id;
2322
- // Remove "agent" label when session is completed (not paused)
2323
- if (sessionStatus === "completed" && typedCard.labels?.length) {
2324
- const agentLabel = typedCard.labels.find((l) => l.name.toLowerCase() === "agent");
2325
- if (agentLabel) {
2326
- await client.removeLabelFromCard(cardId, agentLabel.id);
2327
- }
2328
- }
2329
- if (moveToColumn && projectId) {
2330
- const col = await resolveColumnByName(client, projectId, moveToColumn);
2331
- await client.moveCard(cardId, col.id);
2332
- movedTo = col.name;
2333
- }
2334
- }
2335
- catch {
2336
- // Card fetch/move failed, continue
2337
- }
2338
- // Run shared end-session pipeline (learning, feedback, maintenance)
2339
- const sessionObj = result.session;
2340
- const pipelineResult = await runEndSessionPipeline(client, deps, cardId, sessionStatus, endProgressPercent, sessionObj);
2341
- return {
2342
- success: true,
2343
- ...(sessionEndError && { sessionEndError }),
2344
- movedTo,
2345
- learningsExtracted: pipelineResult.learningsExtracted,
2346
- feedbackAdjusted: pipelineResult.feedbackAdjusted,
2347
- ...(pipelineResult.maintenance &&
2348
- (pipelineResult.maintenance.archived > 0 ||
2349
- pipelineResult.maintenance.pruned > 0 ||
2350
- pipelineResult.maintenance.promoted > 0) && {
2351
- maintenance: pipelineResult.maintenance,
2352
- }),
2353
- ...result,
2354
- };
2355
- }
2356
- case "harmony_get_agent_session": {
2357
- const cardId = z.string().uuid().parse(args.cardId);
2358
- const result = await client.getAgentSession(cardId, {
2359
- includeEnded: args.includeEnded === true || args.includeEnded === "true",
2360
- });
2361
- return { success: true, ...result };
2362
- }
2363
- case "harmony_get_agent_profile": {
2364
- const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
2365
- if (!workspaceId) {
2366
- return {
2367
- success: false,
2368
- error: "No workspace context. Provide workspaceId or set context.",
2369
- };
2370
- }
2371
- const agentIdentifier = args.agentIdentifier || "claude-code";
2372
- const result = await client.getAgentProfile(workspaceId, agentIdentifier);
2373
- return { success: true, ...result };
2374
- }
2375
- // Prompt generation
2376
- case "harmony_generate_prompt": {
2377
- // Resolve card ID — either directly or via short ID
2378
- let cardId;
2379
- if (args.cardId) {
2380
- cardId = z.string().uuid().parse(args.cardId);
2381
- }
2382
- else if (args.shortId !== undefined) {
2383
- const shortId = z.number().int().positive().parse(args.shortId);
2384
- const projectId = args.projectId || deps.getActiveProjectId();
2385
- if (!projectId) {
2386
- throw new Error("Project ID required when using shortId. Use harmony_set_project_context or provide projectId.");
2387
- }
2388
- const cardResult = await client.getCardByShortId(projectId, shortId);
2389
- cardId = cardResult.card.id;
2390
- }
2391
- else {
2392
- throw new Error("Either cardId or shortId must be provided");
2393
- }
2394
- // Parse MCP-specific context options
2395
- const contextOptions = {};
2396
- if (args.includeSubtasks !== undefined) {
2397
- contextOptions.includeSubtasks =
2398
- args.includeSubtasks === true || args.includeSubtasks === "true";
2399
- }
2400
- if (args.includeLinks !== undefined) {
2401
- contextOptions.includeLinks =
2402
- args.includeLinks === true || args.includeLinks === "true";
2403
- }
2404
- if (args.includeDescription !== undefined) {
2405
- contextOptions.includeDescription =
2406
- args.includeDescription === true ||
2407
- args.includeDescription === "true";
2408
- }
2409
- // Delegate to the shared prompt generation pipeline
2410
- const result = await client.generateCardPrompt({
2411
- cardId,
2412
- workspaceId: deps.getActiveWorkspaceId() || "",
2413
- projectId: args.projectId || getActiveProjectId() || undefined,
2414
- variant: args.variant || "execute",
2415
- customConstraints: args.customConstraints,
2416
- contextOptions,
2417
- });
2418
- // MCP-specific: cache the assembly manifest for the feedback loop
2419
- if (result.assemblyId) {
2420
- trackSessionAssembly(cardId, result.assemblyId);
2421
- }
2422
- return {
2423
- success: true,
2424
- ...result,
2425
- };
2426
- }
2427
- // Memory / Knowledge Graph operations
2428
- case "harmony_remember": {
2429
- const title = z.string().min(1).max(300).parse(args.title);
2430
- const content = z.string().min(1).max(50000).parse(args.content);
2431
- const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
2432
- if (!workspaceId) {
2433
- throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
2434
- }
2435
- const entityType = args.type || "context";
2436
- const entityTags = args.tags || [];
2437
- // Use session's agent identifier if available, otherwise null
2438
- const activeMemSession = getActiveMemorySession();
2439
- const result = await client.createMemoryEntity({
2440
- workspace_id: workspaceId,
2441
- project_id: args.projectId || deps.getActiveProjectId() || undefined,
2442
- type: entityType,
2443
- scope: args.scope || "project",
2444
- memory_tier: args.tier || undefined,
2445
- title,
2446
- content,
2447
- metadata: args.metadata,
2448
- confidence: args.confidence !== undefined
2449
- ? z.number().min(0).max(1).parse(args.confidence)
2450
- : undefined,
2451
- tags: entityTags.length > 0 ? entityTags : undefined,
2452
- agent_identifier: activeMemSession?.agentIdentifier || undefined,
2453
- });
2454
- // Fire-and-forget graph expansion: link new entity to semantically similar ones
2455
- const newEntityIdForGraph = result.entity?.id;
2456
- if (newEntityIdForGraph) {
2457
- autoExpandGraph(client, newEntityIdForGraph, title, content, entityTags, workspaceId, args.projectId || deps.getActiveProjectId() || undefined).catch(() => { });
2458
- }
2459
- // Semantic contradiction detection (uses hybrid search instead of naive type+tag matching)
2460
- let potentialContradictions = [];
2461
- const newEntityId = result.entity?.id;
2462
- if (newEntityId) {
2463
- try {
2464
- potentialContradictions = await detectContradictions(client, newEntityId, entityType, title, content, entityTags, workspaceId, args.projectId ||
2465
- deps.getActiveProjectId() ||
2466
- undefined);
2467
- }
2468
- catch {
2469
- // Don't block creation if contradiction detection fails
2470
- }
2471
- }
2472
- // Track memory write action and flush (fire-and-forget)
2473
- if (activeMemSession) {
2474
- appendMemoryAction(activeMemSession.cardId, `Stored memory: ${title}`);
2475
- flushMemoryActions(client, activeMemSession.cardId).catch(() => { });
2476
- }
2477
- return {
2478
- success: true,
2479
- ...result,
2480
- ...(potentialContradictions.length > 0 && {
2481
- potentialContradictions: potentialContradictions.map((c) => ({
2482
- id: c.entityId,
2483
- title: c.title,
2484
- tags: c.tags,
2485
- })),
2486
- contradictionNote: `Found ${potentialContradictions.length} semantically similar memor${potentialContradictions.length === 1 ? "y" : "ies"} of same type that may contradict. 'contradicts' relations created automatically. Review these to resolve or confirm.`,
2487
- }),
2488
- };
2489
- }
2490
- case "harmony_recall": {
2491
- const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
2492
- if (!workspaceId) {
2493
- throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
2494
- }
2495
- const queryOpts = {
2496
- workspace_id: workspaceId,
2497
- project_id: args.projectId || deps.getActiveProjectId() || undefined,
2498
- type: args.type,
2499
- scope: args.scope,
2500
- tags: args.tags,
2501
- min_confidence: args.minConfidence,
2502
- q: args.query,
2503
- limit: args.limit,
2504
- };
2505
- // Fetch both markdown (for display) and JSON (for lifecycle evaluation) in parallel
2506
- const [markdown, { entities }] = await Promise.all([
2507
- client.listMemoryEntitiesMarkdown(queryOpts),
2508
- client.listMemoryEntities(queryOpts),
2509
- ]);
2510
- // Evaluate lifecycle and auto-promote eligible entities (fire-and-forget)
2511
- if (entities.length > 0) {
2512
- Promise.all(entities.map(async (entity) => {
2513
- if (!entity.memory_tier || !entity.created_at)
2514
- return;
2515
- try {
2516
- // Touch access count via API (triggers server-side touch_knowledge_entity)
2517
- await client.updateMemoryEntity(entity.id, {
2518
- metadata: {
2519
- ...(entity.metadata || {}),
2520
- _last_recall: new Date().toISOString(),
2521
- },
2522
- });
2523
- const lifecycle = evaluateLifecycle({
2524
- memory_tier: entity.memory_tier,
2525
- confidence: entity.confidence ?? 1.0,
2526
- access_count: entity.access_count ?? 0,
2527
- last_accessed_at: entity.last_accessed_at,
2528
- created_at: entity.created_at,
2529
- });
2530
- if (lifecycle.promotion.eligible &&
2531
- lifecycle.promotion.targetTier) {
2532
- await client.updateMemoryEntity(entity.id, {
2533
- memory_tier: lifecycle.promotion.targetTier,
2534
- metadata: {
2535
- ...(entity.metadata || {}),
2536
- promoted_at: new Date().toISOString(),
2537
- promotion_reason: lifecycle.promotion.reason,
2538
- },
2539
- });
2540
- }
2541
- }
2542
- catch (_) {
2543
- // Non-critical: don't fail recall if promotion check fails
2544
- }
2545
- })).catch(() => { });
2546
- }
2547
- // Track memory read (batched on flush)
2548
- const recallMemSession = getActiveMemorySession();
2549
- if (recallMemSession) {
2550
- incrementMemoryReads(recallMemSession.cardId);
2551
- }
2552
- return markdown || "No memories found.";
2553
- }
2554
- case "harmony_update_memory": {
2555
- const entityId = z.string().uuid().parse(args.entityId);
2556
- const updates = {};
2557
- if (args.title !== undefined)
2558
- updates.title = z.string().min(1).max(300).parse(args.title);
2559
- if (args.content !== undefined)
2560
- updates.content = z.string().max(50000).parse(args.content);
2561
- if (args.type !== undefined)
2562
- updates.type = args.type;
2563
- if (args.scope !== undefined)
2564
- updates.scope = args.scope;
2565
- if (args.tags !== undefined)
2566
- updates.tags = args.tags;
2567
- if (args.confidence !== undefined)
2568
- updates.confidence = z.number().min(0).max(1).parse(args.confidence);
2569
- if (args.metadata !== undefined)
2570
- updates.metadata = args.metadata;
2571
- const result = await client.updateMemoryEntity(entityId, updates);
2572
- // Track memory write action and flush (fire-and-forget)
2573
- const updateMemSession = getActiveMemorySession();
2574
- if (updateMemSession) {
2575
- const updateTitle = updates.title || entityId.slice(0, 8);
2576
- appendMemoryAction(updateMemSession.cardId, `Updated memory: ${updateTitle}`);
2577
- flushMemoryActions(client, updateMemSession.cardId).catch(() => { });
2578
- }
2579
- return { success: true, ...result };
2580
- }
2581
- case "harmony_forget": {
2582
- const entityId = z.string().uuid().parse(args.entityId);
2583
- // Fetch title before deletion for action tracking
2584
- let forgetTitle = null;
2585
- const forgetMemSession = getActiveMemorySession();
2586
- if (forgetMemSession) {
2587
- try {
2588
- const { entity } = await client.getMemoryEntity(entityId);
2589
- forgetTitle = entity?.title || null;
2590
- }
2591
- catch {
2592
- // Non-fatal: use truncated ID if fetch fails
2593
- }
2594
- }
2595
- await client.deleteMemoryEntity(entityId);
2596
- // Track memory write action and flush (fire-and-forget)
2597
- if (forgetMemSession) {
2598
- appendMemoryAction(forgetMemSession.cardId, `Removed memory: ${forgetTitle || entityId.slice(0, 8)}`);
2599
- flushMemoryActions(client, forgetMemSession.cardId).catch(() => { });
2600
- }
2601
- return { success: true };
2602
- }
2603
- case "harmony_relate": {
2604
- const sourceId = z.string().uuid().parse(args.sourceId);
2605
- const targetId = z.string().uuid().parse(args.targetId);
2606
- const relationType = z
2607
- .enum([
2608
- "learned_from",
2609
- "resolved_by",
2610
- "contradicts",
2611
- "supports",
2612
- "depends_on",
2613
- "part_of",
2614
- "caused_by",
2615
- "relates_to",
2616
- ])
2617
- .parse(args.relationType);
2618
- const result = await client.createMemoryRelation({
2619
- source_id: sourceId,
2620
- target_id: targetId,
2621
- relation_type: relationType,
2622
- confidence: args.confidence !== undefined
2623
- ? z.number().min(0).max(1).parse(args.confidence)
2624
- : undefined,
2625
- });
2626
- // Track memory write action and flush (fire-and-forget)
2627
- const relateMemSession = getActiveMemorySession();
2628
- if (relateMemSession) {
2629
- appendMemoryAction(relateMemSession.cardId, `Linked memories: ${relationType}`);
2630
- flushMemoryActions(client, relateMemSession.cardId).catch(() => { });
2631
- }
2632
- return { success: true, ...result };
2633
- }
2634
- case "harmony_memory_search": {
2635
- const query = z.string().min(1).max(500).parse(args.query);
2636
- const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
2637
- if (!workspaceId) {
2638
- throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
2639
- }
2640
- const markdown = await client.searchMemoryEntitiesMarkdown(workspaceId, query, {
2641
- project_id: args.projectId ||
2642
- deps.getActiveProjectId() ||
2643
- undefined,
2644
- type: args.type,
2645
- limit: args.limit,
2646
- });
2647
- // Track memory read (batched on flush)
2648
- const searchMemSession = getActiveMemorySession();
2649
- if (searchMemSession) {
2650
- incrementMemoryReads(searchMemSession.cardId);
2651
- }
2652
- return markdown || "No memories found.";
2653
- }
2654
- // Vault index
2655
- case "harmony_vault_index": {
2656
- const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
2657
- if (!workspaceId) {
2658
- throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
2659
- }
2660
- const markdown = await client.getVaultIndexMarkdown({
2661
- workspace_id: workspaceId,
2662
- project_id: args.projectId || deps.getActiveProjectId() || undefined,
2663
- type: args.type,
2664
- limit: args.limit,
2665
- });
2666
- return markdown || "No entities found.";
2667
- }
2668
- // Wiki-link resolution
2669
- case "harmony_resolve_links": {
2670
- const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
2671
- if (!workspaceId) {
2672
- throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
2673
- }
2674
- const result = await client.resolveLinks({
2675
- workspace_id: workspaceId,
2676
- project_id: args.projectId || deps.getActiveProjectId() || undefined,
2677
- });
2678
- return {
2679
- success: true,
2680
- ...result,
2681
- message: `Resolved ${result.resolved} wiki-links, ${result.unresolved} remain unresolved.`,
2682
- };
2683
- }
2684
- // Memory sync
2685
- case "harmony_sync": {
2686
- const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
2687
- if (!workspaceId) {
2688
- throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
2689
- }
2690
- const direction = args.direction || "full";
2691
- const syncProjectId = args.projectId || deps.getActiveProjectId() || undefined;
2692
- const memoryDir = deps.getMemoryDir();
2693
- if (!memoryDir) {
2694
- return {
2695
- success: true,
2696
- message: "Memory sync requires local filesystem access. Not available in remote mode.",
2697
- };
2698
- }
2699
- const syncConfig = { memoryDir };
2700
- let syncResult;
2701
- switch (direction) {
2702
- case "pull":
2703
- syncResult = await syncPull(client, syncConfig, workspaceId, syncProjectId);
2704
- break;
2705
- case "push":
2706
- syncResult = await syncPush(client, syncConfig, workspaceId);
2707
- break;
2708
- default:
2709
- syncResult = await syncFull(client, syncConfig, workspaceId, syncProjectId);
2710
- }
2711
- const parts = [];
2712
- if (syncResult.pulled > 0)
2713
- parts.push(`${syncResult.pulled} pulled`);
2714
- if (syncResult.pushed > 0)
2715
- parts.push(`${syncResult.pushed} pushed`);
2716
- if (syncResult.deleted > 0)
2717
- parts.push(`${syncResult.deleted} deleted`);
2718
- if (syncResult.conflicts > 0)
2719
- parts.push(`${syncResult.conflicts} conflicts (server wins)`);
2720
- const summary = parts.length > 0 ? parts.join(", ") : "Already in sync";
2721
- return {
2722
- success: syncResult.errors.length === 0,
2723
- ...syncResult,
2724
- memoryDir: syncConfig.memoryDir,
2725
- message: summary,
2726
- };
2727
- }
2728
- // Plan operations
2729
- case "harmony_list_plans": {
2730
- const projectId = args.projectId || getProjectId();
2731
- const result = await client.listPlans(projectId, {
2732
- search: args.search,
2733
- status: args.status,
2734
- });
2735
- return {
2736
- success: true,
2737
- plans: result.plans,
2738
- count: result.plans.length,
2739
- };
2740
- }
2741
- case "harmony_create_plan": {
2742
- const title = z.string().min(1).max(200).parse(args.title);
2743
- const projectId = args.projectId || getProjectId();
2744
- const result = await client.createPlan(projectId, {
2745
- title,
2746
- content: args.content,
2747
- source: args.source || "agent",
2748
- tasks: args.tasks,
2749
- });
2750
- // Build URL for viewing the plan
2751
- const planUrl = `https://app.gethmy.com/plans/${result.plan.id}`;
2752
- return {
2753
- success: true,
2754
- planId: result.plan.id,
2755
- planUrl,
2756
- plan: result.plan,
2757
- tasks: result.tasks,
2758
- };
2759
- }
2760
- case "harmony_get_plan": {
2761
- let result = null;
2762
- if (args.planId) {
2763
- const planId = z.string().uuid().parse(args.planId);
2764
- result = await client.getPlan(planId);
2765
- }
2766
- else if (args.cardId) {
2767
- const cardId = z.string().uuid().parse(args.cardId);
2768
- result = await client.getPlanByCardId(cardId);
2769
- if (!result) {
2770
- return {
2771
- success: true,
2772
- plan: null,
2773
- tasks: [],
2774
- message: "No plan linked to this card",
2775
- };
2776
- }
2777
- }
2778
- else {
2779
- throw new Error("Either planId or cardId must be provided");
2780
- }
2781
- return {
2782
- success: true,
2783
- plan: result.plan,
2784
- tasks: result.tasks,
2785
- };
2786
- }
2787
- case "harmony_update_plan": {
2788
- const planId = z.string().uuid().parse(args.planId);
2789
- const updates = {};
2790
- if (args.title !== undefined)
2791
- updates.title = z.string().min(1).max(200).parse(args.title);
2792
- if (args.content !== undefined)
2793
- updates.content = args.content;
2794
- if (args.status !== undefined) {
2795
- updates.status = z
2796
- .enum(["draft", "active", "archived"])
2797
- .parse(args.status);
2798
- }
2799
- const result = await client.updatePlan(planId, updates);
2800
- return { success: true, plan: result.plan };
2801
- }
2802
- case "harmony_advance_plan": {
2803
- // Simplified: just archive the plan
2804
- const planId = z.string().uuid().parse(args.planId);
2805
- const summary = args.summary;
2806
- const planResult = await client.getPlan(planId);
2807
- const plan = planResult.plan;
2808
- await client.updatePlan(planId, { status: "archived" });
2809
- const results = {};
2810
- const workspaceId = deps.getActiveWorkspaceId() || "";
2811
- if (summary && workspaceId) {
2812
- try {
2813
- const memResult = await client.createMemoryEntity({
2814
- title: `Lesson: ${plan.title}`,
2815
- content: summary,
2816
- type: "lesson",
2817
- scope: "project",
2818
- workspaceId,
2819
- projectId: plan.project_id,
2820
- tags: ["plan", "archived"],
2821
- confidence: 0.8,
2822
- });
2823
- results.memoryEntityId = memResult.entity?.id;
2824
- if (results.memoryEntityId) {
2825
- const title = `Lesson: ${plan.title}`;
2826
- const tags = ["plan", "archived"];
2827
- autoExpandGraph(client, results.memoryEntityId, title, summary, tags, workspaceId, plan.project_id).catch(() => { });
2828
- detectContradictions(client, results.memoryEntityId, "lesson", title, summary, tags, workspaceId, plan.project_id).catch(() => { });
2829
- }
2830
- }
2831
- catch (_) {
2832
- /* best-effort */
2833
- }
2834
- }
2835
- return {
2836
- success: true,
2837
- newStatus: "archived",
2838
- ...results,
2839
- };
2840
- }
2841
- case "harmony_debug_context": {
2842
- const entityId = z.string().uuid().parse(args.entityId);
2843
- const taskContext = z.string().min(1).max(2000).parse(args.taskContext);
2844
- const cardLabels = args.cardLabels || [];
2845
- // Fetch the entity
2846
- const entityResult = await client.getMemoryEntity(entityId);
2847
- const entity = mapToContextEntity(entityResult.entity);
2848
- // Compute relevance score
2849
- const { score, reasons } = computeRelevanceScore(entity, taskContext, cardLabels);
2850
- // Compute lifecycle status
2851
- const lifecycle = evaluateLifecycle({
2852
- memory_tier: entity.memory_tier,
2853
- confidence: entity.confidence,
2854
- access_count: entity.access_count,
2855
- last_accessed_at: entity.last_accessed_at,
2856
- created_at: entity.updated_at, // using updated_at as proxy
2857
- });
2858
- return {
2859
- success: true,
2860
- entity: {
2861
- id: entity.id,
2862
- title: entity.title,
2863
- type: entity.type,
2864
- tier: entity.memory_tier,
2865
- confidence: entity.confidence,
2866
- accessCount: entity.access_count,
2867
- },
2868
- relevance: {
2869
- score: Math.round(score * 1000) / 1000,
2870
- reasons,
2871
- wouldBeIncluded: score >= 0.1,
2872
- threshold: 0.1,
2873
- },
2874
- lifecycle: {
2875
- decayScore: Math.round(lifecycle.decay.score * 1000) / 1000,
2876
- daysSinceAccess: Math.round(lifecycle.decay.daysSinceAccess),
2877
- promotionEligible: lifecycle.promotion.eligible,
2878
- promotionTarget: lifecycle.promotion.targetTier,
2879
- shouldArchive: lifecycle.shouldArchive,
2880
- shouldFlagForReview: lifecycle.shouldFlagForReview,
2881
- },
2882
- suggestions: [
2883
- ...(lifecycle.promotion.eligible
2884
- ? [
2885
- `Eligible for promotion to ${lifecycle.promotion.targetTier}: ${lifecycle.promotion.reason}`,
2886
- ]
2887
- : []),
2888
- ...(lifecycle.shouldArchive
2889
- ? [`Consider archiving: ${lifecycle.archiveReason}`]
2890
- : []),
2891
- ...(lifecycle.shouldFlagForReview
2892
- ? [`Flagged for review: ${lifecycle.reviewReason}`]
2893
- : []),
2894
- ...(score < 0.1
2895
- ? [
2896
- "Below relevance threshold - consider adding more specific tags or updating content",
2897
- ]
2898
- : []),
2899
- ],
2900
- };
2901
- }
2902
- case "harmony_export_memory_graph": {
2903
- const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
2904
- if (!workspaceId) {
2905
- throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
2906
- }
2907
- const projectId = args.projectId || deps.getActiveProjectId() || undefined;
2908
- const limit = args.limit || 50;
2909
- // Fetch entities
2910
- const result = await client.listMemoryEntities({
2911
- workspace_id: workspaceId,
2912
- project_id: projectId,
2913
- limit,
2914
- });
2915
- const entities = (result.entities || []);
2916
- // Build DOT graph
2917
- const tierColors = {
2918
- draft: "#FFEB3B", // yellow
2919
- episode: "#2196F3", // blue
2920
- reference: "#4CAF50", // green
2921
- };
2922
- const typeShapes = {
2923
- error: "octagon",
2924
- solution: "diamond",
2925
- pattern: "hexagon",
2926
- decision: "house",
2927
- lesson: "ellipse",
2928
- preference: "parallelogram",
2929
- };
2930
- const lines = [
2931
- "digraph KnowledgeGraph {",
2932
- " rankdir=LR;",
2933
- " node [style=filled, fontsize=10];",
2934
- "",
2935
- ];
2936
- // Add nodes
2937
- for (const entity of entities) {
2938
- const tier = entity.memory_tier || "reference";
2939
- const color = tierColors[tier] || "#9E9E9E";
2940
- const shape = typeShapes[entity.type] || "box";
2941
- const label = entity.title.length > 40
2942
- ? entity.title.slice(0, 37) + "..."
2943
- : entity.title;
2944
- const safeLabel = label.replace(/"/g, '\\"');
2945
- lines.push(` "${entity.id.slice(0, 8)}" [label="${safeLabel}\\n(${entity.type}, ${tier})", fillcolor="${color}", shape=${shape}];`);
2946
- }
2947
- // Fetch relations for each entity and add edges
2948
- const entityIds = new Set(entities.map((e) => e.id));
2949
- const addedEdges = new Set();
2950
- for (const entity of entities.slice(0, 20)) {
2951
- // Limit graph API calls
2952
- try {
2953
- const related = await client.getRelatedEntities(entity.id);
2954
- // Outgoing relations: { id, relation_type, target: { id, ... } }
2955
- for (const raw of related.outgoing || []) {
2956
- const rel = raw;
2957
- const sourceId = rel.source_id || entity.id;
2958
- const targetId = rel.target_id || rel.target?.id;
2959
- if (targetId &&
2960
- entityIds.has(sourceId) &&
2961
- entityIds.has(targetId)) {
2962
- const edgeKey = `${sourceId}-${targetId}-${rel.relation_type}`;
2963
- if (!addedEdges.has(edgeKey)) {
2964
- addedEdges.add(edgeKey);
2965
- lines.push(` "${sourceId.slice(0, 8)}" -> "${targetId.slice(0, 8)}" [label="${rel.relation_type}"];`);
2966
- }
2967
- }
2968
- }
2969
- // Incoming relations: { id, relation_type, source: { id, ... } }
2970
- for (const raw of related.incoming || []) {
2971
- const rel = raw;
2972
- const sourceId = rel.source_id || rel.source?.id;
2973
- const targetId = rel.target_id || entity.id;
2974
- if (sourceId &&
2975
- entityIds.has(sourceId) &&
2976
- entityIds.has(targetId)) {
2977
- const edgeKey = `${sourceId}-${targetId}-${rel.relation_type}`;
2978
- if (!addedEdges.has(edgeKey)) {
2979
- addedEdges.add(edgeKey);
2980
- lines.push(` "${sourceId.slice(0, 8)}" -> "${targetId.slice(0, 8)}" [label="${rel.relation_type}"];`);
2981
- }
2982
- }
2983
- }
2984
- }
2985
- catch {
2986
- // Relation fetch failed, continue
2987
- }
2988
- }
2989
- lines.push("}");
2990
- const dotGraph = lines.join("\n");
2991
- return {
2992
- success: true,
2993
- format: "dot",
2994
- entityCount: entities.length,
2995
- edgeCount: addedEdges.size,
2996
- graph: dotGraph,
2997
- message: `Exported ${entities.length} entities and ${addedEdges.size} relations as DOT graph. Use Graphviz to render: echo '<graph>' | dot -Tpng -o graph.png`,
2998
- };
2999
- }
3000
- case "harmony_prune_draft": {
3001
- const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
3002
- if (!workspaceId) {
3003
- throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
3004
- }
3005
- const projectId = args.projectId || deps.getActiveProjectId() || undefined;
3006
- const dryRun = args.dryRun !== false; // default true
3007
- const maxAgeDays = args.maxAgeDays || 30;
3008
- // Fetch draft entities
3009
- const result = await client.listMemoryEntities({
3010
- workspace_id: workspaceId,
3011
- project_id: projectId,
3012
- limit: 100,
3013
- });
3014
- const drafts = (result.entities || []).filter((e) => {
3015
- const entity = e;
3016
- return entity.memory_tier === "draft";
3017
- });
3018
- const now = Date.now();
3019
- const stale = [];
3020
- for (const raw of drafts) {
3021
- const entity = raw;
3022
- const ageDays = (now - new Date(entity.created_at).getTime()) / (1000 * 60 * 60 * 24);
3023
- if (ageDays < maxAgeDays)
3024
- continue;
3025
- const lifecycle = evaluateLifecycle(entity);
3026
- stale.push({
3027
- id: entity.id,
3028
- title: entity.title,
3029
- ageDays: Math.round(ageDays),
3030
- decayScore: Math.round(lifecycle.decay.score * 100) / 100,
3031
- });
3032
- }
3033
- if (!dryRun) {
3034
- for (const item of stale) {
3035
- try {
3036
- await client.deleteMemoryEntity(item.id);
3037
- }
3038
- catch {
3039
- // Non-fatal
3040
- }
3041
- }
3042
- }
3043
- return {
3044
- success: true,
3045
- dryRun,
3046
- totalDrafts: drafts.length,
3047
- staleDrafts: stale.length,
3048
- pruned: dryRun ? 0 : stale.length,
3049
- items: stale,
3050
- message: dryRun
3051
- ? `Found ${stale.length} stale drafts (>${maxAgeDays} days old). Run with dryRun=false to delete.`
3052
- : `Pruned ${stale.length} stale draft memories.`,
3053
- };
3054
- }
3055
- case "harmony_consolidate_memories": {
3056
- const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
3057
- if (!workspaceId) {
3058
- throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
3059
- }
3060
- const projectId = args.projectId || deps.getActiveProjectId() || undefined;
3061
- const dryRun = args.dryRun !== false; // default true
3062
- const minClusterSize = args.minClusterSize || 2;
3063
- const consolidationResult = await consolidateMemories(client, workspaceId, projectId, { dryRun, minClusterSize });
3064
- return {
3065
- success: true,
3066
- dryRun,
3067
- ...consolidationResult,
3068
- message: dryRun
3069
- ? `Found ${consolidationResult.clustersFound} clusters across ${consolidationResult.entitiesProcessed} entities. Run with dryRun=false to consolidate.`
3070
- : `Consolidated ${consolidationResult.consolidated} clusters from ${consolidationResult.entitiesProcessed} entities.`,
3071
- };
3072
- }
3073
- case "harmony_get_context_manifest": {
3074
- const assemblyId = z.string().min(1).max(100).parse(args.assemblyId);
3075
- const manifest = getCachedManifest(assemblyId);
3076
- if (!manifest) {
3077
- throw new Error(`Manifest not found for assembly '${assemblyId}'. Manifests are cached in-memory and expire after server restart.`);
3078
- }
3079
- return {
3080
- success: true,
3081
- manifest,
3082
- summary: {
3083
- totalIncluded: manifest.included.length,
3084
- totalExcluded: manifest.excluded.length,
3085
- budgetUsed: `${manifest.budgetUsed}/${manifest.budgetTotal} tokens`,
3086
- tierBreakdown: manifest.tierBreakdown,
3087
- procedureBreakdown: manifest.procedureBreakdown,
3088
- },
3089
- };
3090
- }
3091
- case "harmony_promote_memory": {
3092
- const entityId = z.string().uuid().parse(args.entityId);
3093
- const targetTier = z
3094
- .enum(["episode", "reference"])
3095
- .parse(args.targetTier);
3096
- const reason = z.string().min(1).max(500).parse(args.reason);
3097
- // Fetch current entity to validate promotion path
3098
- const entityResult = await client.getMemoryEntity(entityId);
3099
- const entity = entityResult.entity;
3100
- const currentTier = entity.memory_tier || "reference";
3101
- // Validate promotion path: draft→episode→reference
3102
- const tierOrder = { draft: 0, episode: 1, reference: 2 };
3103
- if ((tierOrder[targetTier] || 0) <=
3104
- (tierOrder[currentTier] || 0)) {
3105
- throw new Error(`Cannot promote from '${currentTier}' to '${targetTier}'. Must promote to a higher tier.`);
3106
- }
3107
- const result = await client.updateMemoryEntity(entityId, {
3108
- memory_tier: targetTier,
3109
- metadata: {
3110
- promoted_from_tier: currentTier,
3111
- promotion_reason: reason,
3112
- promoted_at: new Date().toISOString(),
3113
- },
3114
- });
3115
- return {
3116
- success: true,
3117
- promoted: {
3118
- from: currentTier,
3119
- to: targetTier,
3120
- reason,
3121
- },
3122
- ...result,
3123
- };
3124
- }
3125
- // ============ ONBOARDING TOOLS ============
3126
- case "harmony_signup": {
3127
- const email = z.string().email().max(254).parse(args.email);
3128
- const password = z.string().min(8).max(128).parse(args.password);
3129
- const fullName = z.string().min(1).max(100).parse(args.fullName);
3130
- const apiUrl = deps.getApiUrl();
3131
- const result = await signupUser(apiUrl, {
3132
- email,
3133
- password,
3134
- full_name: fullName,
3135
- });
3136
- return {
3137
- success: true,
3138
- ...result,
3139
- message: "Account created. Use the session tokens for authenticated requests, or call harmony_onboard for full setup.",
3140
- };
3141
- }
3142
- case "harmony_create_workspace": {
3143
- const name = z.string().min(1).max(100).parse(args.name);
3144
- const result = await client.createWorkspace({
3145
- name,
3146
- description: args.description,
3147
- });
3148
- return { success: true, ...result };
3149
- }
3150
- case "harmony_create_project": {
3151
- const workspaceId = z.string().uuid().parse(args.workspaceId);
3152
- const name = z.string().min(1).max(100).parse(args.name);
3153
- const result = await client.createProject({
3154
- workspaceId,
3155
- name,
3156
- description: args.description,
3157
- color: args.color,
3158
- template: args.template,
3159
- });
3160
- return { success: true, ...result };
3161
- }
3162
- case "harmony_send_invitations": {
3163
- const workspaceId = z.string().uuid().parse(args.workspaceId);
3164
- const emails = z
3165
- .array(z.string().email().max(254))
3166
- .min(1)
3167
- .parse(args.emails);
3168
- const result = await client.sendInvitations({
3169
- workspaceId,
3170
- emails,
3171
- role: args.role,
3172
- sendEmail: args.sendEmail,
3173
- });
3174
- return { success: true, ...result };
3175
- }
3176
- case "harmony_generate_api_key": {
3177
- const name = z.string().min(1).max(100).parse(args.name);
3178
- const result = await client.generateApiKey(name);
3179
- return { success: true, ...result };
3180
- }
3181
- case "harmony_onboard": {
3182
- const email = z.string().email().max(254).parse(args.email);
3183
- const password = z.string().min(8).max(128).parse(args.password);
3184
- const fullName = z.string().min(1).max(100).parse(args.fullName);
3185
- const workspaceName = args.workspaceName || `${fullName}'s Workspace`;
3186
- const projectName = args.projectName || "My First Project";
3187
- const template = args.template || "kanban";
3188
- const keyName = args.keyName || "mcp-agent";
3189
- const result = await onboardNewUser({
3190
- email,
3191
- password,
3192
- fullName,
3193
- workspaceName,
3194
- projectName,
3195
- template,
3196
- keyName,
3197
- apiUrl: deps.getApiUrl(),
3198
- });
3199
- // Save config and reset client
3200
- deps.saveConfig({ apiKey: result.apiKey.rawKey });
3201
- deps.setActiveWorkspace(result.workspace.id);
3202
- deps.setActiveProject(result.project.id);
3203
- deps.resetClient();
3204
- return {
3205
- success: true,
3206
- user: result.user,
3207
- workspace: result.workspace,
3208
- project: result.project,
3209
- columns: result.columns,
3210
- apiKey: result.apiKey,
3211
- message: `Onboarding complete! Account created for ${email}. Workspace "${workspaceName}" and project "${projectName}" are ready. API key saved to config.`,
3212
- };
3213
- }
3214
- case "harmony_backfill_embeddings": {
3215
- const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
3216
- if (!workspaceId) {
3217
- throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
3218
- }
3219
- const result = await client.backfillEmbeddings(workspaceId, args.batchSize);
3220
- return {
3221
- success: true,
3222
- ...result,
3223
- message: result.remaining > 0
3224
- ? `Processed ${result.processed} entities. ${result.remaining} still need embeddings — run again to continue.`
3225
- : `All embeddings up to date. Processed ${result.processed} entities.`,
3226
- };
3227
- }
3228
- case "harmony_backfill_relations": {
3229
- const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
3230
- if (!workspaceId) {
3231
- throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
3232
- }
3233
- const projectId = args.projectId || deps.getActiveProjectId() || undefined;
3234
- const maxRelationsPerEntity = args.maxRelationsPerEntity || 3;
3235
- // Fetch all entities
3236
- const { entities: allEntities } = await client.listMemoryEntities({
3237
- workspace_id: workspaceId,
3238
- project_id: projectId,
3239
- limit: 200,
3240
- });
3241
- let entitiesProcessed = 0;
3242
- let relationsCreated = 0;
3243
- for (const raw of allEntities) {
3244
- try {
3245
- const result = await autoExpandGraph(client, raw.id, raw.title, raw.content || "", raw.tags || [], workspaceId, projectId, maxRelationsPerEntity);
3246
- entitiesProcessed++;
3247
- relationsCreated += result.relationsCreated;
3248
- }
3249
- catch {
3250
- // Non-fatal: continue with remaining entities
3251
- }
3252
- }
3253
- return {
3254
- success: true,
3255
- entitiesProcessed,
3256
- relationsCreated,
3257
- message: `Processed ${entitiesProcessed} entities, created ${relationsCreated} new relations.`,
3258
- };
3259
- }
3260
- case "harmony_cleanup_memories": {
3261
- const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
3262
- if (!workspaceId) {
3263
- throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
3264
- }
3265
- const projectId = args.projectId || deps.getActiveProjectId() || undefined;
3266
- const validSteps = [
3267
- "prune",
3268
- "consolidate",
3269
- "orphans",
3270
- "duplicates",
3271
- "backfill",
3272
- ];
3273
- const rawSteps = args.steps;
3274
- const steps = rawSteps?.filter((s) => validSteps.includes(s));
3275
- if (rawSteps && steps && steps.length < rawSteps.length) {
3276
- const invalid = rawSteps.filter((s) => !validSteps.includes(s));
3277
- // Will appear in report.errors via the healthReport
3278
- console.warn(`Unknown cleanup steps ignored: ${invalid.join(", ")}`);
3279
- }
3280
- const report = await runMemoryCleanup(client, workspaceId, projectId, {
3281
- dryRun: args.dryRun,
3282
- steps,
3283
- maxAgeDays: args.maxAgeDays,
3284
- minClusterSize: args.minClusterSize,
3285
- orphanAgeDays: args.orphanAgeDays,
3286
- });
3287
- return {
3288
- success: report.success,
3289
- dryRun: report.dryRun,
3290
- summary: report.summary,
3291
- errors: report.errors,
3292
- healthReport: report.healthReport,
3293
- };
3294
- }
3295
- default:
3296
- throw new Error(`Unknown tool: ${name}`);
3297
- }
3298
- }
3299
- /** Default deps that read from the local config file (for stdio mode). */
3300
- function createConfigDeps() {
3301
- return {
3302
- getClient,
3303
- isConfigured,
3304
- getActiveProjectId: () => getActiveProjectId(),
3305
- getActiveWorkspaceId: () => getActiveWorkspaceId(),
3306
- setActiveProject: (id) => setActiveProject(id),
3307
- setActiveWorkspace: (id) => setActiveWorkspace(id),
3308
- getApiUrl,
3309
- getMemoryDir: () => getMemoryDir(),
3310
- getUserEmail,
3311
- saveConfig: (cfg) => saveConfig(cfg),
3312
- resetClient,
3313
- };
3314
- }
3315
- export class HarmonyMCPServer {
3316
- server;
3317
- constructor() {
3318
- this.server = new Server({ name: "@gethmy/mcp", version: "2.0.0" }, { capabilities: { tools: {}, resources: {} } });
3319
- registerHandlers(this.server, createConfigDeps());
3320
- }
3321
- async run() {
3322
- const transport = new StdioServerTransport();
3323
- await this.server.connect(transport);
3324
- console.error("Harmony MCP server running on stdio");
3325
- // Initialize auto-session tracking with MCP client identity detection
3326
- const configDeps = createConfigDeps();
3327
- initAutoSession(async (client, cardId, status) => {
3328
- await runEndSessionPipeline(client, configDeps, cardId, status);
3329
- }, () => getClient(), () => {
3330
- const cv = this.server.getClientVersion();
3331
- return cv ? { name: cv.name, version: cv.version } : null;
3332
- });
3333
- // Graceful shutdown: end all auto-sessions
3334
- const handleShutdown = async () => {
3335
- try {
3336
- await shutdownAllSessions();
3337
- }
3338
- catch {
3339
- // Best-effort
3340
- }
3341
- destroyAutoSession();
3342
- process.exit(0);
3343
- };
3344
- process.on("SIGINT", handleShutdown);
3345
- process.on("SIGTERM", handleShutdown);
3346
- // Attempt startup sync (non-blocking, best-effort)
3347
- try {
3348
- if (isConfigured()) {
3349
- const workspaceId = getActiveWorkspaceId();
3350
- if (workspaceId) {
3351
- const client = getClient();
3352
- const memoryDir = getMemoryDir();
3353
- if (memoryDir) {
3354
- const syncConfig = { memoryDir };
3355
- const result = await syncPull(client, syncConfig, workspaceId);
3356
- console.error(`Startup sync: ${result.pulled} pulled, ${result.deleted} deleted, ${result.errors.length} errors`);
3357
- }
3358
- }
3359
- }
3360
- }
3361
- catch (err) {
3362
- console.error(`Startup sync failed (non-fatal): ${err}`);
3363
- }
3364
- }
3365
- }