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