@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
@@ -0,0 +1,505 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ type CardData,
4
+ generatePrompt,
5
+ getAvailableCategories,
6
+ getAvailableVariants,
7
+ getRoleFraming,
8
+ inferCategoryFromLabels,
9
+ type MemoryData,
10
+ } from "../prompt-builder.js";
11
+
12
+ function makeCard(overrides: Partial<CardData> = {}): CardData {
13
+ return {
14
+ id: "card-1",
15
+ short_id: 42,
16
+ title: "Fix login button",
17
+ description: "The login button is not responding on mobile devices.",
18
+ priority: "high",
19
+ done: false,
20
+ labels: [{ name: "bug", color: "#ff0000" }],
21
+ subtasks: [
22
+ { title: "Reproduce on iOS", completed: true },
23
+ { title: "Fix click handler", completed: false },
24
+ ],
25
+ links: [],
26
+ assignee: { full_name: "Alice", email: "alice@test.com" },
27
+ ...overrides,
28
+ };
29
+ }
30
+
31
+ function makeMemory(overrides: Partial<MemoryData> = {}): MemoryData {
32
+ return {
33
+ id: "mem-1",
34
+ type: "pattern",
35
+ title: "Mobile Click Handler Pattern",
36
+ content:
37
+ "Use touchstart event for mobile devices to avoid 300ms delay on click events.",
38
+ confidence: 0.95,
39
+ tags: ["mobile", "performance"],
40
+ ...overrides,
41
+ };
42
+ }
43
+
44
+ // ─── Memory inclusion in generated prompts ──────────────────────────
45
+
46
+ describe("generatePrompt — memory inclusion", () => {
47
+ test("includes legacy memories when no assembledContext", () => {
48
+ const memories = [
49
+ makeMemory({ title: "Pattern A", content: "Content A" }),
50
+ makeMemory({ title: "Pattern B", content: "Content B" }),
51
+ ];
52
+
53
+ const result = generatePrompt({
54
+ card: makeCard(),
55
+ variant: "analysis",
56
+ memories,
57
+ });
58
+
59
+ expect(result.prompt).toContain("## Relevant Memories");
60
+ expect(result.prompt).toContain("Pattern A");
61
+ expect(result.prompt).toContain("Pattern B");
62
+ expect(result.prompt).toContain("Content A");
63
+ expect(result.prompt).toContain("2 memories recalled");
64
+ expect(result.contextSummary.memoryCount).toBe(2);
65
+ });
66
+
67
+ test("includes memory type and confidence in legacy format", () => {
68
+ const memories = [
69
+ makeMemory({
70
+ type: "decision",
71
+ confidence: 0.85,
72
+ title: "Architecture Decision",
73
+ }),
74
+ ];
75
+
76
+ const result = generatePrompt({
77
+ card: makeCard(),
78
+ variant: "analysis",
79
+ memories,
80
+ });
81
+
82
+ expect(result.prompt).toContain("(decision, confidence: 0.85)");
83
+ });
84
+
85
+ test("includes memory tags in legacy format", () => {
86
+ const memories = [
87
+ makeMemory({ tags: ["auth", "security"], title: "Auth Pattern" }),
88
+ ];
89
+
90
+ const result = generatePrompt({
91
+ card: makeCard(),
92
+ variant: "analysis",
93
+ memories,
94
+ });
95
+
96
+ expect(result.prompt).toContain("[auth, security]");
97
+ });
98
+
99
+ test("prefers assembledContext over legacy memories", () => {
100
+ const assembledContext =
101
+ "## Procedures (1 loaded)\n### Deploy Checklist\nStep 1: Run tests";
102
+ const memories = [makeMemory({ title: "Should Not Appear" })];
103
+
104
+ const result = generatePrompt({
105
+ card: makeCard(),
106
+ variant: "analysis",
107
+ memories,
108
+ assembledContext,
109
+ });
110
+
111
+ expect(result.prompt).toContain("Deploy Checklist");
112
+ expect(result.prompt).not.toContain("Should Not Appear");
113
+ expect(result.prompt).not.toContain("## Relevant Memories");
114
+ });
115
+
116
+ test("counts ### headings in assembledContext for memoryCount", () => {
117
+ const assembledContext = [
118
+ "## Relevant Memories (3 loaded)",
119
+ "### Memory One",
120
+ "Content one",
121
+ "### Memory Two",
122
+ "Content two",
123
+ "### Memory Three",
124
+ "Content three",
125
+ ].join("\n");
126
+
127
+ const result = generatePrompt({
128
+ card: makeCard(),
129
+ variant: "analysis",
130
+ assembledContext,
131
+ });
132
+
133
+ expect(result.contextSummary.memoryCount).toBe(3);
134
+ });
135
+
136
+ test("memoryCount is 0 when no memories and no assembledContext", () => {
137
+ const result = generatePrompt({
138
+ card: makeCard(),
139
+ variant: "analysis",
140
+ });
141
+
142
+ expect(result.contextSummary.memoryCount).toBe(0);
143
+ expect(result.prompt).not.toContain("## Relevant Memories");
144
+ });
145
+
146
+ test("assemblyId is passed through to result", () => {
147
+ const result = generatePrompt({
148
+ card: makeCard(),
149
+ variant: "execute",
150
+ assembledContext: "### Test Memory\nContent",
151
+ assemblyId: "ctx_abc123",
152
+ });
153
+
154
+ expect(result.assemblyId).toBe("ctx_abc123");
155
+ });
156
+
157
+ test("assemblyId is undefined when not provided", () => {
158
+ const result = generatePrompt({
159
+ card: makeCard(),
160
+ variant: "analysis",
161
+ });
162
+
163
+ expect(result.assemblyId).toBeUndefined();
164
+ });
165
+ });
166
+
167
+ // ─── Role framing based on labels ───────────────────────────────────
168
+
169
+ describe("inferCategoryFromLabels", () => {
170
+ test("maps bug-related labels", () => {
171
+ expect(inferCategoryFromLabels([{ name: "bug" }])).toBe("bug");
172
+ expect(inferCategoryFromLabels([{ name: "fix" }])).toBe("bug");
173
+ expect(inferCategoryFromLabels([{ name: "hotfix" }])).toBe("bug");
174
+ expect(inferCategoryFromLabels([{ name: "defect" }])).toBe("bug");
175
+ });
176
+
177
+ test("maps feature-related labels", () => {
178
+ expect(inferCategoryFromLabels([{ name: "feature" }])).toBe("feature");
179
+ expect(inferCategoryFromLabels([{ name: "enhancement" }])).toBe("feature");
180
+ });
181
+
182
+ test("maps design-related labels", () => {
183
+ expect(inferCategoryFromLabels([{ name: "design" }])).toBe("design");
184
+ expect(inferCategoryFromLabels([{ name: "ui" }])).toBe("design");
185
+ expect(inferCategoryFromLabels([{ name: "ux" }])).toBe("design");
186
+ });
187
+
188
+ test("returns custom for unknown labels", () => {
189
+ expect(inferCategoryFromLabels([{ name: "unknown-label" }])).toBe("custom");
190
+ });
191
+
192
+ test("returns custom for empty labels", () => {
193
+ expect(inferCategoryFromLabels([])).toBe("custom");
194
+ });
195
+
196
+ test("is case-insensitive", () => {
197
+ expect(inferCategoryFromLabels([{ name: "BUG" }])).toBe("bug");
198
+ expect(inferCategoryFromLabels([{ name: "Feature" }])).toBe("feature");
199
+ });
200
+
201
+ test("uses first matching label when multiple present", () => {
202
+ const result = inferCategoryFromLabels([
203
+ { name: "bug" },
204
+ { name: "feature" },
205
+ ]);
206
+ expect(result).toBe("bug"); // First match wins
207
+ });
208
+ });
209
+
210
+ // ─── Prompt structure ───────────────────────────────────────────────
211
+
212
+ describe("generatePrompt — structure", () => {
213
+ test("includes role framing for bug category", () => {
214
+ const result = generatePrompt({
215
+ card: makeCard({ labels: [{ name: "bug", color: "#f00" }] }),
216
+ variant: "analysis",
217
+ });
218
+
219
+ expect(result.role).toBe("Senior QA Engineer and Software Developer");
220
+ expect(result.category).toBe("bug");
221
+ expect(result.prompt).toContain("# Role: Senior QA Engineer");
222
+ });
223
+
224
+ test("includes variant instruction", () => {
225
+ const analysis = generatePrompt({
226
+ card: makeCard(),
227
+ variant: "analysis",
228
+ });
229
+ expect(analysis.prompt).toContain("ANALYSIS MODE");
230
+ expect(analysis.variant).toBe("analysis");
231
+
232
+ const execute = generatePrompt({
233
+ card: makeCard(),
234
+ variant: "execute",
235
+ });
236
+ expect(execute.prompt).toContain("EXECUTE MODE");
237
+ });
238
+
239
+ test("includes card metadata", () => {
240
+ const result = generatePrompt({
241
+ card: makeCard(),
242
+ column: { name: "In Progress" },
243
+ variant: "analysis",
244
+ });
245
+
246
+ expect(result.prompt).toContain("# Task: Fix login button");
247
+ expect(result.prompt).toContain("**Status:** In Progress");
248
+ expect(result.prompt).toContain("**Priority:** high");
249
+ expect(result.prompt).toContain("**Assignee:** Alice");
250
+ });
251
+
252
+ test("includes subtasks with completion status", () => {
253
+ const result = generatePrompt({
254
+ card: makeCard(),
255
+ variant: "analysis",
256
+ });
257
+
258
+ expect(result.prompt).toContain("[x] Reproduce on iOS");
259
+ expect(result.prompt).toContain("[ ] Fix click handler");
260
+ expect(result.prompt).toContain("1/2 completed");
261
+ expect(result.contextSummary.subtaskCount).toBe(2);
262
+ expect(result.contextSummary.completedSubtasks).toBe(1);
263
+ });
264
+
265
+ test("includes description", () => {
266
+ const result = generatePrompt({
267
+ card: makeCard(),
268
+ variant: "analysis",
269
+ });
270
+
271
+ expect(result.prompt).toContain("## Description");
272
+ expect(result.prompt).toContain("login button is not responding on mobile");
273
+ expect(result.contextSummary.hasDescription).toBe(true);
274
+ });
275
+
276
+ test("omits description when null", () => {
277
+ const result = generatePrompt({
278
+ card: makeCard({ description: null }),
279
+ variant: "analysis",
280
+ });
281
+
282
+ expect(result.prompt).not.toContain("## Description");
283
+ expect(result.contextSummary.hasDescription).toBe(false);
284
+ });
285
+
286
+ test("includes card reference footer", () => {
287
+ const result = generatePrompt({
288
+ card: makeCard({ short_id: 42 }),
289
+ variant: "draft",
290
+ });
291
+
292
+ expect(result.prompt).toContain("Card #42");
293
+ expect(result.prompt).toContain("draft mode");
294
+ });
295
+
296
+ test("execute variant includes progress tracking instructions", () => {
297
+ const result = generatePrompt({
298
+ card: makeCard(),
299
+ variant: "execute",
300
+ });
301
+
302
+ expect(result.prompt).toContain("## Progress Tracking");
303
+ expect(result.prompt).toContain("harmony_update_agent_progress");
304
+ });
305
+
306
+ test("non-execute variants omit progress tracking", () => {
307
+ const analysis = generatePrompt({
308
+ card: makeCard(),
309
+ variant: "analysis",
310
+ });
311
+ expect(analysis.prompt).not.toContain("## Progress Tracking");
312
+
313
+ const draft = generatePrompt({
314
+ card: makeCard(),
315
+ variant: "draft",
316
+ });
317
+ expect(draft.prompt).not.toContain("## Progress Tracking");
318
+ });
319
+
320
+ test("includes custom constraints when provided", () => {
321
+ const result = generatePrompt({
322
+ card: makeCard(),
323
+ variant: "analysis",
324
+ customConstraints: "Do not modify the database schema.",
325
+ });
326
+
327
+ expect(result.prompt).toContain("## Additional Instructions");
328
+ expect(result.prompt).toContain("Do not modify the database schema.");
329
+ });
330
+ });
331
+
332
+ // ─── Linked cards ───────────────────────────────────────────────────
333
+
334
+ describe("generatePrompt — linked cards", () => {
335
+ test("includes related cards in prompt", () => {
336
+ const result = generatePrompt({
337
+ card: makeCard({
338
+ links: [
339
+ {
340
+ target_card: { short_id: 10, title: "Auth refactor" },
341
+ display_type: "relates_to",
342
+ direction: "outgoing",
343
+ },
344
+ {
345
+ target_card: { short_id: 11, title: "API update" },
346
+ display_type: "is_blocked_by",
347
+ direction: "incoming",
348
+ },
349
+ ],
350
+ }),
351
+ variant: "analysis",
352
+ });
353
+
354
+ expect(result.prompt).toContain("## Related Cards");
355
+ expect(result.prompt).toContain("#10: Auth refactor");
356
+ expect(result.prompt).toContain("#11: API update");
357
+ expect(result.contextSummary.linkedCardCount).toBe(2);
358
+ });
359
+
360
+ test("synthesizes blocker as recommended next step", () => {
361
+ const result = generatePrompt({
362
+ card: makeCard({
363
+ links: [
364
+ {
365
+ target_card: { short_id: 5, title: "Dependency task" },
366
+ display_type: "is_blocked_by",
367
+ direction: "incoming",
368
+ },
369
+ ],
370
+ }),
371
+ variant: "execute",
372
+ });
373
+
374
+ expect(result.prompt).toContain("## Recommended Next Step");
375
+ expect(result.prompt).toContain("Unblock first");
376
+ expect(result.prompt).toContain("#5");
377
+ });
378
+ });
379
+
380
+ // ─── One Thing synthesis ────────────────────────────────────────────
381
+
382
+ describe("generatePrompt — recommended next step", () => {
383
+ test("suggests next incomplete subtask", () => {
384
+ const result = generatePrompt({
385
+ card: makeCard({
386
+ links: [],
387
+ subtasks: [
388
+ { title: "Step 1", completed: true },
389
+ { title: "Step 2", completed: false },
390
+ { title: "Step 3", completed: false },
391
+ ],
392
+ }),
393
+ variant: "analysis",
394
+ });
395
+
396
+ expect(result.prompt).toContain("## Recommended Next Step");
397
+ expect(result.prompt).toContain("Step 2");
398
+ });
399
+
400
+ test("suggests review when all subtasks done", () => {
401
+ const result = generatePrompt({
402
+ card: makeCard({
403
+ links: [],
404
+ subtasks: [
405
+ { title: "Step 1", completed: true },
406
+ { title: "Step 2", completed: true },
407
+ ],
408
+ }),
409
+ variant: "analysis",
410
+ });
411
+
412
+ expect(result.prompt).toContain("All subtasks completed");
413
+ expect(result.prompt).toContain("mark the card as done");
414
+ });
415
+
416
+ test("no recommended step when card is done", () => {
417
+ const result = generatePrompt({
418
+ card: makeCard({ done: true, links: [], subtasks: [] }),
419
+ variant: "analysis",
420
+ });
421
+
422
+ expect(result.prompt).not.toContain("## Recommended Next Step");
423
+ });
424
+
425
+ test("resumes paused session from assembledContext", () => {
426
+ const assembledContext = [
427
+ "### Session: 2025-06-01",
428
+ "Paused work on refactoring auth middleware",
429
+ "Final task: Refactoring auth middleware",
430
+ "Progress: 60%",
431
+ ].join("\n");
432
+
433
+ const result = generatePrompt({
434
+ card: makeCard({ links: [], subtasks: [] }),
435
+ variant: "execute",
436
+ assembledContext,
437
+ });
438
+
439
+ expect(result.prompt).toContain("Resume previous session");
440
+ expect(result.prompt).toContain("60% complete");
441
+ expect(result.prompt).toContain("Refactoring auth middleware");
442
+ });
443
+ });
444
+
445
+ // ─── Token estimation ───────────────────────────────────────────────
446
+
447
+ describe("generatePrompt — token estimation", () => {
448
+ test("tokenEstimate is positive and proportional to content", () => {
449
+ const short = generatePrompt({
450
+ card: makeCard({ description: "Short" }),
451
+ variant: "analysis",
452
+ });
453
+ const long = generatePrompt({
454
+ card: makeCard({ description: "A".repeat(10000) }),
455
+ variant: "analysis",
456
+ });
457
+
458
+ expect(short.tokenEstimate).toBeGreaterThan(0);
459
+ expect(long.tokenEstimate).toBeGreaterThan(short.tokenEstimate);
460
+ });
461
+
462
+ test("memories increase token estimate", () => {
463
+ const without = generatePrompt({
464
+ card: makeCard(),
465
+ variant: "analysis",
466
+ });
467
+ const with_ = generatePrompt({
468
+ card: makeCard(),
469
+ variant: "analysis",
470
+ memories: [
471
+ makeMemory({ content: "A".repeat(2000) }),
472
+ makeMemory({ content: "B".repeat(2000) }),
473
+ ],
474
+ });
475
+
476
+ expect(with_.tokenEstimate).toBeGreaterThan(without.tokenEstimate);
477
+ });
478
+ });
479
+
480
+ // ─── Utility exports ────────────────────────────────────────────────
481
+
482
+ describe("utility exports", () => {
483
+ test("getAvailableCategories returns all categories", () => {
484
+ const categories = getAvailableCategories();
485
+ expect(categories).toContain("bug");
486
+ expect(categories).toContain("feature");
487
+ expect(categories).toContain("custom");
488
+ expect(categories.length).toBe(7);
489
+ });
490
+
491
+ test("getAvailableVariants returns all variants", () => {
492
+ const variants = getAvailableVariants();
493
+ expect(variants).toEqual(["analysis", "draft", "execute"]);
494
+ });
495
+
496
+ test("getRoleFraming returns framing for each category", () => {
497
+ for (const category of getAvailableCategories()) {
498
+ const framing = getRoleFraming(category);
499
+ expect(framing.role).toBeTruthy();
500
+ expect(framing.perspective).toBeTruthy();
501
+ expect(framing.focus.length).toBeGreaterThan(0);
502
+ expect(framing.outputSuggestions.length).toBeGreaterThan(0);
503
+ }
504
+ });
505
+ });