@gethmy/mcp 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +6 -1
  2. package/dist/cli.js +711 -59
  3. package/dist/index.js +5 -3
  4. package/dist/lib/__tests__/active-learning.test.js +386 -0
  5. package/dist/lib/__tests__/agent-performance-profiles.test.js +325 -0
  6. package/dist/lib/__tests__/auto-session.test.js +661 -0
  7. package/dist/lib/__tests__/context-assembly.test.js +362 -0
  8. package/dist/lib/__tests__/graph-expansion.test.js +150 -0
  9. package/dist/lib/__tests__/integration-memory-crud.test.js +797 -0
  10. package/dist/lib/__tests__/integration-memory-system.test.js +281 -0
  11. package/dist/lib/__tests__/lifecycle-maintenance.test.js +207 -0
  12. package/dist/lib/__tests__/pattern-detection.test.js +295 -0
  13. package/dist/lib/__tests__/prompt-builder.test.js +418 -0
  14. package/dist/lib/active-learning.js +878 -0
  15. package/dist/lib/api-client.js +550 -0
  16. package/dist/lib/auto-session.js +173 -0
  17. package/dist/lib/cli.js +127 -0
  18. package/dist/lib/config.js +205 -0
  19. package/dist/lib/consolidation.js +243 -0
  20. package/dist/lib/context-assembly.js +606 -0
  21. package/dist/lib/graph-expansion.js +163 -0
  22. package/dist/lib/http.js +174 -0
  23. package/dist/lib/index.js +7 -0
  24. package/dist/lib/lifecycle-maintenance.js +88 -0
  25. package/dist/lib/prompt-builder.js +483 -0
  26. package/dist/lib/remote.js +166 -0
  27. package/dist/lib/server.js +3132 -0
  28. package/dist/lib/tui/agents.js +116 -0
  29. package/dist/lib/tui/docs.js +744 -0
  30. package/dist/lib/tui/setup.js +1068 -0
  31. package/dist/lib/tui/theme.js +95 -0
  32. package/dist/lib/tui/writer.js +200 -0
  33. package/package.json +15 -6
  34. package/src/__tests__/active-learning.test.ts +483 -0
  35. package/src/__tests__/agent-performance-profiles.test.ts +468 -0
  36. package/src/__tests__/auto-session.test.ts +912 -0
  37. package/src/__tests__/context-assembly.test.ts +506 -0
  38. package/src/__tests__/graph-expansion.test.ts +285 -0
  39. package/src/__tests__/integration-memory-crud.test.ts +948 -0
  40. package/src/__tests__/integration-memory-system.test.ts +321 -0
  41. package/src/__tests__/lifecycle-maintenance.test.ts +238 -0
  42. package/src/__tests__/pattern-detection.test.ts +438 -0
  43. package/src/__tests__/prompt-builder.test.ts +505 -0
  44. package/src/active-learning.ts +1227 -0
  45. package/src/api-client.ts +969 -0
  46. package/src/auto-session.ts +218 -0
  47. package/src/cli.ts +166 -0
  48. package/src/config.ts +285 -0
  49. package/src/consolidation.ts +314 -0
  50. package/src/context-assembly.ts +842 -0
  51. package/src/graph-expansion.ts +234 -0
  52. package/src/http.ts +265 -0
  53. package/src/index.ts +8 -0
  54. package/src/lifecycle-maintenance.ts +120 -0
  55. package/src/prompt-builder.ts +681 -0
  56. package/src/remote.ts +227 -0
  57. package/src/server.ts +3858 -0
  58. package/src/tui/agents.ts +154 -0
  59. package/src/tui/docs.ts +863 -0
  60. package/src/tui/setup.ts +1281 -0
  61. package/src/tui/theme.ts +114 -0
  62. package/src/tui/writer.ts +260 -0
package/dist/index.js CHANGED
@@ -22942,7 +22942,7 @@ function hasLocalConfig(cwd) {
22942
22942
  function getApiKey() {
22943
22943
  const config2 = loadConfig();
22944
22944
  if (!config2.apiKey) {
22945
- throw new Error(`Not configured. Run "harmony-mcp configure" to set your API key.
22945
+ throw new Error(`Not configured. Run "npx @gethmy/mcp setup" to set your API key.
22946
22946
  ` + "You can generate an API key at https://gethmy.com → Settings → API Keys.");
22947
22947
  }
22948
22948
  return config2.apiKey;
@@ -24061,6 +24061,8 @@ class HarmonyApiClient {
24061
24061
  params.set("summary", "true");
24062
24062
  if (options?.includeArchived)
24063
24063
  params.set("include_archived", "true");
24064
+ if (options?.labelName)
24065
+ params.set("label_name", options.labelName);
24064
24066
  const query = params.toString() ? `?${params.toString()}` : "";
24065
24067
  return this.request("GET", `/board/${projectId}${query}`);
24066
24068
  }
@@ -27169,7 +27171,7 @@ function registerHandlers(server, deps) {
27169
27171
  async function handleToolCall(name, args, deps) {
27170
27172
  const unauthenticatedTools = ["harmony_signup", "harmony_onboard"];
27171
27173
  if (!unauthenticatedTools.includes(name) && !deps.isConfigured()) {
27172
- throw new Error(`Not configured. Run "harmony-mcp configure" to set your API key.
27174
+ throw new Error(`Not configured. Run "npx @gethmy/mcp setup" to set your API key.
27173
27175
  ` + `You can generate an API key at https://gethmy.com → Settings → API Keys.
27174
27176
  ` + 'Or use "harmony_onboard" to create an account and configure automatically.');
27175
27177
  }
@@ -28464,7 +28466,7 @@ function createConfigDeps() {
28464
28466
  class HarmonyMCPServer {
28465
28467
  server;
28466
28468
  constructor() {
28467
- this.server = new Server({ name: "harmony-mcp", version: "1.0.0" }, { capabilities: { tools: {}, resources: {} } });
28469
+ this.server = new Server({ name: "@gethmy/mcp", version: "2.0.0" }, { capabilities: { tools: {}, resources: {} } });
28468
28470
  registerHandlers(this.server, createConfigDeps());
28469
28471
  }
28470
28472
  async run() {
@@ -0,0 +1,386 @@
1
+ /**
2
+ * Unit tests for the Active Learning system.
3
+ *
4
+ * These tests use a mock API client to verify extraction rules
5
+ * and confidence-based routing - no real API calls.
6
+ *
7
+ * Run with: bun test packages/mcp-server/src/__tests__/active-learning.test.ts
8
+ */
9
+ import { describe, expect, mock, test } from "bun:test";
10
+ // Mock the config module before importing active-learning
11
+ let mockWorkspaceId = "ws-test-123";
12
+ const mockProjectId = "proj-test-456";
13
+ mock.module("../config.js", () => ({
14
+ getActiveWorkspaceId: () => mockWorkspaceId,
15
+ getActiveProjectId: () => mockProjectId,
16
+ isConfigured: () => true,
17
+ loadConfig: () => { },
18
+ }));
19
+ // Import after mocking
20
+ const { extractLearnings } = await import("../active-learning.js");
21
+ // Extended mock client that satisfies all methods called by fire-and-forget paths
22
+ // (autoExpandGraph, detectAndCreatePatterns). Unused by most tests but prevents crashes.
23
+ function makeFullMockClient() {
24
+ const createdEntities = [];
25
+ let nextId = 1;
26
+ return {
27
+ createdEntities,
28
+ createMemoryEntity: mock(async (data) => {
29
+ const entity = { id: `entity-${nextId++}`, ...data };
30
+ createdEntities.push(entity);
31
+ return { entity };
32
+ }),
33
+ updateMemoryEntity: mock(async (entityId, updates) => ({
34
+ entity: { id: entityId, ...updates },
35
+ })),
36
+ deleteMemoryEntity: mock(async () => ({ success: true })),
37
+ listMemoryEntities: mock(async () => ({
38
+ entities: [],
39
+ count: 0,
40
+ })),
41
+ getMemoryEntity: mock(async (id) => ({
42
+ entity: { id, type: "lesson" },
43
+ })),
44
+ searchMemoryEntities: mock(async () => ({
45
+ entities: [],
46
+ count: 0,
47
+ })),
48
+ createMemoryRelation: mock(async () => ({ relation: {} })),
49
+ };
50
+ }
51
+ // ---- Mock helpers ----
52
+ function makeMockClient() {
53
+ const createdEntities = [];
54
+ let nextId = 1;
55
+ return {
56
+ createdEntities,
57
+ createMemoryEntity: mock(async (data) => {
58
+ const entity = { id: `entity-${nextId++}`, ...data };
59
+ createdEntities.push(entity);
60
+ return { entity };
61
+ }),
62
+ updateMemoryEntity: mock(async (entityId, updates) => {
63
+ return { entity: { id: entityId, ...updates } };
64
+ }),
65
+ deleteMemoryEntity: mock(async () => ({ success: true })),
66
+ listMemoryEntities: mock(async () => ({
67
+ entities: [],
68
+ count: 0,
69
+ })),
70
+ };
71
+ }
72
+ function makeSession(overrides = {}) {
73
+ return {
74
+ cardId: "card-1",
75
+ cardTitle: "Fix login bug",
76
+ cardLabels: ["feature"],
77
+ agentIdentifier: "claude-code",
78
+ agentName: "Claude Code",
79
+ status: "completed",
80
+ progressPercent: 100,
81
+ ...overrides,
82
+ };
83
+ }
84
+ // ---- Tests ----
85
+ describe("extractLearnings", () => {
86
+ describe("Rule 1: Blockers create error entities", () => {
87
+ test("session with blockers creates error entities", async () => {
88
+ const client = makeMockClient();
89
+ const session = makeSession({
90
+ blockers: ["API rate limit exceeded", "Database timeout"],
91
+ status: "completed",
92
+ });
93
+ const result = await extractLearnings(client, session);
94
+ // 2 blockers + 1 session lesson = 3 entities
95
+ expect(result.count).toBe(3);
96
+ expect(result.entityIds).toHaveLength(3);
97
+ // Check error entities were created for each blocker
98
+ const errorEntities = client.createdEntities.filter((e) => e.type === "error");
99
+ expect(errorEntities).toHaveLength(2);
100
+ expect(errorEntities[0].title).toContain("Blocker:");
101
+ expect(errorEntities[0].title).toContain("API rate limit");
102
+ expect(errorEntities[0].memory_tier).toBe("reference");
103
+ expect(errorEntities[0].confidence).toBe(0.7);
104
+ expect(errorEntities[0].tags ?? []).toContain("auto-extracted");
105
+ expect(errorEntities[0].tags ?? []).toContain("blocker");
106
+ });
107
+ test("blocker title is truncated to 100 chars", async () => {
108
+ const client = makeMockClient();
109
+ const longBlocker = "A".repeat(200);
110
+ const session = makeSession({
111
+ blockers: [longBlocker],
112
+ status: "paused",
113
+ });
114
+ await extractLearnings(client, session);
115
+ const errorEntities = client.createdEntities.filter((e) => e.type === "error");
116
+ expect(errorEntities).toHaveLength(1);
117
+ // Title: "Blocker: " + 100 chars
118
+ expect(errorEntities[0].title.length).toBeLessThanOrEqual(110);
119
+ });
120
+ });
121
+ describe("Rule 2: Completed sessions create lesson entities", () => {
122
+ test("clean completed session does NOT create lesson entity", async () => {
123
+ const client = makeMockClient();
124
+ const session = makeSession({
125
+ status: "completed",
126
+ currentTask: "Deployed to production",
127
+ progressPercent: 100,
128
+ sessionDurationMs: 300000, // 5 minutes
129
+ });
130
+ const result = await extractLearnings(client, session);
131
+ expect(result.count).toBe(0);
132
+ const lessons = client.createdEntities.filter((e) => e.type === "lesson");
133
+ expect(lessons).toHaveLength(0);
134
+ });
135
+ test("completed session with blockers creates lesson entity", async () => {
136
+ const client = makeMockClient();
137
+ const session = makeSession({
138
+ status: "completed",
139
+ currentTask: "Deployed to production",
140
+ progressPercent: 100,
141
+ sessionDurationMs: 300000, // 5 minutes
142
+ blockers: ["API rate limit hit"],
143
+ });
144
+ const _result = await extractLearnings(client, session);
145
+ // 1 error entity (Rule 1) + 1 lesson entity (Rule 2)
146
+ const lesson = client.createdEntities.find((e) => e.type === "lesson");
147
+ expect(lesson).toBeTruthy();
148
+ expect(lesson.title).toContain("Session:");
149
+ expect(lesson.memory_tier).toBe("episode");
150
+ expect(lesson.confidence).toBe(0.7);
151
+ expect(lesson.content).toContain("Deployed to production");
152
+ expect(lesson.content).toContain("5 minutes");
153
+ });
154
+ test("completed session with incomplete subtasks creates lesson entity", async () => {
155
+ const client = makeMockClient();
156
+ const session = makeSession({
157
+ status: "completed",
158
+ currentTask: "Finished main work",
159
+ progressPercent: 100,
160
+ cardSubtasks: [
161
+ { title: "Step 1", done: true },
162
+ { title: "Step 2", done: false },
163
+ ],
164
+ });
165
+ const _result = await extractLearnings(client, session);
166
+ const lesson = client.createdEntities.find((e) => e.type === "lesson");
167
+ expect(lesson).toBeTruthy();
168
+ expect(lesson.title).toContain("Session:");
169
+ });
170
+ test("paused session does NOT create lesson entity", async () => {
171
+ const client = makeMockClient();
172
+ const session = makeSession({ status: "paused", blockers: undefined });
173
+ const result = await extractLearnings(client, session);
174
+ expect(result.count).toBe(0);
175
+ const lessons = client.createdEntities.filter((e) => e.type === "lesson");
176
+ expect(lessons).toHaveLength(0);
177
+ });
178
+ });
179
+ describe("Rule 3: Bug label + completed creates solution entity", () => {
180
+ test('card with "bug" label creates solution entity', async () => {
181
+ const client = makeMockClient();
182
+ const session = makeSession({
183
+ cardLabels: ["bug", "priority-high"],
184
+ status: "completed",
185
+ });
186
+ const _result = await extractLearnings(client, session);
187
+ const solutions = client.createdEntities.filter((e) => e.type === "solution");
188
+ expect(solutions).toHaveLength(1);
189
+ expect(solutions[0].title).toContain("Solution:");
190
+ expect(solutions[0].memory_tier).toBe("reference");
191
+ expect(solutions[0].confidence).toBe(0.8);
192
+ expect(solutions[0].tags ?? []).toContain("bug-fix");
193
+ });
194
+ test('case-insensitive "Bug" label works', async () => {
195
+ const client = makeMockClient();
196
+ const session = makeSession({
197
+ cardLabels: ["Bug"],
198
+ status: "completed",
199
+ });
200
+ await extractLearnings(client, session);
201
+ const solutions = client.createdEntities.filter((e) => e.type === "solution");
202
+ // The check is .toLowerCase(), so "Bug" matches
203
+ expect(solutions).toHaveLength(1);
204
+ });
205
+ test("bug label + paused does NOT create solution", async () => {
206
+ const client = makeMockClient();
207
+ const session = makeSession({
208
+ cardLabels: ["bug"],
209
+ status: "paused",
210
+ });
211
+ await extractLearnings(client, session);
212
+ const solutions = client.createdEntities.filter((e) => e.type === "solution");
213
+ expect(solutions).toHaveLength(0);
214
+ });
215
+ for (const label of ["fix", "hotfix", "defect", "error"]) {
216
+ test(`"${label}" label also triggers solution creation`, async () => {
217
+ const client = makeMockClient();
218
+ const session = makeSession({
219
+ cardLabels: [label],
220
+ status: "completed",
221
+ });
222
+ await extractLearnings(client, session);
223
+ const solutions = client.createdEntities.filter((e) => e.type === "solution");
224
+ expect(solutions).toHaveLength(1);
225
+ });
226
+ }
227
+ });
228
+ describe("Card labels are included in tags", () => {
229
+ test("first 3 card labels are appended to tags", async () => {
230
+ const client = makeMockClient();
231
+ const session = makeSession({
232
+ cardLabels: ["feature", "frontend", "urgent", "extra"],
233
+ status: "completed",
234
+ blockers: ["test blocker"],
235
+ });
236
+ await extractLearnings(client, session);
237
+ const lesson = client.createdEntities.find((e) => e.type === "lesson");
238
+ const tags = lesson.tags;
239
+ expect(tags).toContain("feature");
240
+ expect(tags).toContain("frontend");
241
+ expect(tags).toContain("urgent");
242
+ expect(tags).not.toContain("extra"); // Only first 3
243
+ });
244
+ });
245
+ describe("Error handling", () => {
246
+ test("individual entity creation failure does not break others", async () => {
247
+ let callCount = 0;
248
+ const client = {
249
+ createMemoryEntity: mock(async (_data) => {
250
+ callCount++;
251
+ if (callCount === 1)
252
+ throw new Error("Network error");
253
+ return { entity: { id: `entity-${callCount}` } };
254
+ }),
255
+ };
256
+ const session = makeSession({
257
+ blockers: ["blocker-1"],
258
+ status: "completed",
259
+ });
260
+ // blocker creates error (fails), completed creates lesson (succeeds)
261
+ const result = await extractLearnings(client, session);
262
+ // Only 1 succeeded out of 2
263
+ expect(result.entityIds).toHaveLength(1);
264
+ expect(result.count).toBe(1);
265
+ });
266
+ });
267
+ describe("No workspace returns empty", () => {
268
+ test("returns empty when no workspace configured", async () => {
269
+ // Temporarily override
270
+ const origWs = mockWorkspaceId;
271
+ mockWorkspaceId = null;
272
+ const client = makeMockClient();
273
+ const session = makeSession();
274
+ const result = await extractLearnings(client, session);
275
+ expect(result.count).toBe(0);
276
+ expect(result.entityIds).toHaveLength(0);
277
+ // Restore
278
+ mockWorkspaceId = origWs;
279
+ });
280
+ });
281
+ });
282
+ describe("extractLearnings - entity metadata", () => {
283
+ test("stores card_id and source in metadata", async () => {
284
+ const client = makeMockClient();
285
+ const session = makeSession({
286
+ cardId: "card-abc",
287
+ status: "completed",
288
+ blockers: ["test blocker"],
289
+ });
290
+ await extractLearnings(client, session);
291
+ const lesson = client.createdEntities.find((e) => e.type === "lesson");
292
+ const meta = lesson.metadata;
293
+ expect(meta.source).toBe("active_learning");
294
+ expect(meta.card_id).toBe("card-abc");
295
+ });
296
+ test("lesson entity carries agent_identifier", async () => {
297
+ const client = makeMockClient();
298
+ const session = makeSession({
299
+ agentIdentifier: "my-agent-v2",
300
+ status: "completed",
301
+ blockers: ["test blocker"],
302
+ });
303
+ await extractLearnings(client, session);
304
+ const lesson = client.createdEntities.find((e) => e.type === "lesson");
305
+ expect(lesson.agent_identifier).toBe("my-agent-v2");
306
+ });
307
+ test("session lesson content includes agent name", async () => {
308
+ const client = makeMockClient();
309
+ const session = makeSession({
310
+ agentName: "TestBot",
311
+ status: "completed",
312
+ blockers: ["test blocker"],
313
+ });
314
+ await extractLearnings(client, session);
315
+ const lesson = client.createdEntities.find((e) => e.type === "lesson");
316
+ expect(lesson.content).toContain("TestBot");
317
+ });
318
+ test("clean completed session without meaningful content skips lesson", async () => {
319
+ const client = makeMockClient();
320
+ const session = makeSession({
321
+ status: "completed",
322
+ currentTask: undefined,
323
+ progressPercent: undefined,
324
+ sessionDurationMs: undefined,
325
+ blockers: undefined,
326
+ });
327
+ const result = await extractLearnings(client, session);
328
+ expect(result.count).toBe(0);
329
+ const lessons = client.createdEntities.filter((e) => e.type === "lesson");
330
+ expect(lessons).toHaveLength(0);
331
+ });
332
+ });
333
+ describe("extractLearnings - returns correct entityIds", () => {
334
+ test("entityIds array matches returned count", async () => {
335
+ const client = makeMockClient();
336
+ const session = makeSession({
337
+ cardLabels: ["bug"],
338
+ blockers: ["error occurred"],
339
+ status: "completed",
340
+ });
341
+ const result = await extractLearnings(client, session);
342
+ // error + lesson + solution = 3
343
+ expect(result.count).toBe(result.entityIds.length);
344
+ expect(result.entityIds).toHaveLength(3);
345
+ // All IDs should be truthy strings
346
+ for (const id of result.entityIds) {
347
+ expect(typeof id).toBe("string");
348
+ expect(id.length).toBeGreaterThan(0);
349
+ }
350
+ });
351
+ test("entityIds are unique", async () => {
352
+ const client = makeMockClient();
353
+ const session = makeSession({
354
+ cardLabels: ["bug"],
355
+ blockers: ["err-1", "err-2"],
356
+ status: "completed",
357
+ });
358
+ const result = await extractLearnings(client, session);
359
+ const unique = new Set(result.entityIds);
360
+ expect(unique.size).toBe(result.entityIds.length);
361
+ });
362
+ });
363
+ describe("extractLearnings - fire-and-forget calls don't crash", () => {
364
+ test("works with full client (autoExpandGraph and detectAndCreatePatterns paths)", async () => {
365
+ const client = makeFullMockClient();
366
+ const session = makeSession({ status: "completed", blockers: ["test"] });
367
+ // Should not throw even when graph expansion / pattern detection run
368
+ const result = await extractLearnings(client, session);
369
+ expect(result.count).toBeGreaterThan(0);
370
+ // Flush microtasks so fire-and-forget paths complete
371
+ await new Promise((r) => setTimeout(r, 10));
372
+ // searchMemoryEntities was called by autoExpandGraph
373
+ expect(client.searchMemoryEntities).toHaveBeenCalled();
374
+ });
375
+ test("client errors in fire-and-forget paths do not surface to caller", async () => {
376
+ const client = makeFullMockClient();
377
+ // Make the search throw - should be swallowed
378
+ client.searchMemoryEntities = mock(async () => {
379
+ throw new Error("Search service unavailable");
380
+ });
381
+ const session = makeSession({ status: "completed", blockers: ["test"] });
382
+ const result = await extractLearnings(client, session);
383
+ // Main extraction still succeeded (error + lesson)
384
+ expect(result.count).toBe(2);
385
+ });
386
+ });