@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.
Files changed (65) hide show
  1. package/README.md +201 -36
  2. package/dist/cli.js +20938 -20249
  3. package/dist/http.js +1957 -0
  4. package/dist/index.js +17833 -17888
  5. package/dist/lib/__tests__/active-learning.test.js +386 -0
  6. package/dist/lib/__tests__/agent-performance-profiles.test.js +325 -0
  7. package/dist/lib/__tests__/auto-session.test.js +661 -0
  8. package/dist/lib/__tests__/context-assembly.test.js +362 -0
  9. package/dist/lib/__tests__/graph-expansion.test.js +150 -0
  10. package/dist/lib/__tests__/integration-memory-crud.test.js +797 -0
  11. package/dist/lib/__tests__/integration-memory-system.test.js +281 -0
  12. package/dist/lib/__tests__/lifecycle-maintenance.test.js +207 -0
  13. package/dist/lib/__tests__/pattern-detection.test.js +295 -0
  14. package/dist/lib/__tests__/prompt-builder.test.js +418 -0
  15. package/dist/lib/active-learning.js +878 -0
  16. package/dist/lib/api-client.js +548 -0
  17. package/dist/lib/auto-session.js +173 -0
  18. package/dist/lib/cli.js +127 -0
  19. package/dist/lib/config.js +205 -0
  20. package/dist/lib/consolidation.js +243 -0
  21. package/dist/lib/context-assembly.js +606 -0
  22. package/dist/lib/graph-expansion.js +163 -0
  23. package/dist/lib/http.js +174 -0
  24. package/dist/lib/index.js +7 -0
  25. package/dist/lib/lifecycle-maintenance.js +88 -0
  26. package/dist/lib/prompt-builder.js +483 -0
  27. package/dist/lib/remote.js +166 -0
  28. package/dist/lib/server.js +3132 -0
  29. package/dist/lib/tui/agents.js +116 -0
  30. package/dist/lib/tui/docs.js +558 -0
  31. package/dist/lib/tui/setup.js +1068 -0
  32. package/dist/lib/tui/theme.js +95 -0
  33. package/dist/lib/tui/writer.js +200 -0
  34. package/dist/remote.js +34534 -0
  35. package/dist/server.js +31967 -0
  36. package/package.json +20 -7
  37. package/src/__tests__/active-learning.test.ts +483 -0
  38. package/src/__tests__/agent-performance-profiles.test.ts +468 -0
  39. package/src/__tests__/auto-session.test.ts +912 -0
  40. package/src/__tests__/context-assembly.test.ts +506 -0
  41. package/src/__tests__/graph-expansion.test.ts +285 -0
  42. package/src/__tests__/integration-memory-crud.test.ts +948 -0
  43. package/src/__tests__/integration-memory-system.test.ts +321 -0
  44. package/src/__tests__/lifecycle-maintenance.test.ts +238 -0
  45. package/src/__tests__/pattern-detection.test.ts +438 -0
  46. package/src/__tests__/prompt-builder.test.ts +505 -0
  47. package/src/active-learning.ts +1227 -0
  48. package/src/api-client.ts +963 -0
  49. package/src/auto-session.ts +218 -0
  50. package/src/cli.ts +166 -0
  51. package/src/config.ts +285 -0
  52. package/src/consolidation.ts +314 -0
  53. package/src/context-assembly.ts +842 -0
  54. package/src/graph-expansion.ts +234 -0
  55. package/src/http.ts +265 -0
  56. package/src/index.ts +8 -0
  57. package/src/lifecycle-maintenance.ts +120 -0
  58. package/src/prompt-builder.ts +681 -0
  59. package/src/remote.ts +227 -0
  60. package/src/server.ts +3858 -0
  61. package/src/tui/agents.ts +154 -0
  62. package/src/tui/docs.ts +650 -0
  63. package/src/tui/setup.ts +1281 -0
  64. package/src/tui/theme.ts +114 -0
  65. package/src/tui/writer.ts +260 -0
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Unit + integration tests for Agent Performance Profiles.
3
+ *
4
+ * Unit tests verify that runEndSessionPipeline correctly refreshes
5
+ * the materialized view and upserts an "agent" knowledge entity.
6
+ *
7
+ * Integration tests (skipped when not configured) call the real API.
8
+ *
9
+ * Run with: bun test packages/mcp-server/src/__tests__/agent-performance-profiles.test.ts
10
+ */
11
+ import { describe, expect, mock, test } from "bun:test";
12
+ // ============================================================
13
+ // Module mocks (must be before imports)
14
+ // ============================================================
15
+ const mockWorkspaceId = "ws-test-123";
16
+ const mockProjectId = "proj-test-456";
17
+ mock.module("../config.js", () => ({
18
+ getActiveWorkspaceId: () => mockWorkspaceId,
19
+ getActiveProjectId: () => mockProjectId,
20
+ isConfigured: () => true,
21
+ loadConfig: () => { },
22
+ getApiUrl: () => "http://localhost:54321",
23
+ getApiKey: () => "test-key",
24
+ getMemoryDir: () => null,
25
+ getUserEmail: () => null,
26
+ saveConfig: () => { },
27
+ setActiveProject: () => { },
28
+ setActiveWorkspace: () => { },
29
+ resetClient: () => { },
30
+ }));
31
+ mock.module("../active-learning.js", () => ({
32
+ extractLearnings: mock(async () => ({ count: 0, entityIds: [] })),
33
+ extractMidSessionLearnings: mock(async () => ({ count: 0, entityIds: [] })),
34
+ detectContradictions: mock(async () => ({ contradictions: [] })),
35
+ }));
36
+ mock.module("../context-assembly.js", () => ({
37
+ recordContextFeedback: mock(async () => ({ adjusted: 0 })),
38
+ assembleContext: mock(async () => ({ entities: [], markdown: "" })),
39
+ cacheManifest: mock(() => { }),
40
+ computeRelevanceScore: mock(() => 0),
41
+ getCachedManifest: mock(() => null),
42
+ mapToContextEntity: mock(() => ({})),
43
+ trackSessionAssembly: mock(async () => ({})),
44
+ }));
45
+ mock.module("../lifecycle-maintenance.js", () => ({
46
+ runLifecycleMaintenance: mock(async () => ({
47
+ archived: 0,
48
+ pruned: 0,
49
+ promoted: 0,
50
+ reviewed: 0,
51
+ })),
52
+ }));
53
+ mock.module("../consolidation.js", () => ({
54
+ consolidateMemories: mock(async () => ({ consolidated: 0 })),
55
+ }));
56
+ mock.module("../graph-expansion.js", () => ({
57
+ autoExpandGraph: mock(async () => { }),
58
+ }));
59
+ // Import after mocking
60
+ const { runEndSessionPipeline } = await import("../server.js");
61
+ // ============================================================
62
+ // Helpers
63
+ // ============================================================
64
+ const flush = () => new Promise((r) => setTimeout(r, 100));
65
+ function makeMockClient() {
66
+ const createdEntities = [];
67
+ let nextId = 1;
68
+ return {
69
+ createdEntities,
70
+ getCard: mock(async () => ({
71
+ card: {
72
+ title: "Test card",
73
+ description: "desc",
74
+ labels: [],
75
+ subtasks: [],
76
+ },
77
+ })),
78
+ removeLabelFromCard: mock(async () => ({ success: true })),
79
+ createMemoryEntity: mock(async (data) => {
80
+ const entity = { id: `entity-${nextId++}`, ...data };
81
+ createdEntities.push(entity);
82
+ return { entity };
83
+ }),
84
+ updateMemoryEntity: mock(async (entityId, updates) => ({
85
+ entity: { id: entityId, ...updates },
86
+ })),
87
+ listMemoryEntities: mock(async () => ({
88
+ entities: [],
89
+ count: 0,
90
+ })),
91
+ searchMemoryEntities: mock(async () => ({
92
+ entities: [],
93
+ count: 0,
94
+ })),
95
+ getMemoryEntity: mock(async (id) => ({
96
+ entity: { id, type: "agent" },
97
+ })),
98
+ deleteMemoryEntity: mock(async () => ({ success: true })),
99
+ createMemoryRelation: mock(async () => ({ relation: {} })),
100
+ refreshAgentProfiles: mock(async () => ({ refreshed: true })),
101
+ getAgentProfile: mock(async () => ({
102
+ profile: {
103
+ agent_identifier: "claude-code",
104
+ total_sessions: 10,
105
+ completed_sessions: 8,
106
+ paused_sessions: 1,
107
+ blocked_sessions: 1,
108
+ completion_rate_pct: 80,
109
+ avg_active_duration_ms: 120000,
110
+ avg_completion_progress: 95,
111
+ first_session_at: "2026-01-01T00:00:00Z",
112
+ last_session_at: "2026-03-05T00:00:00Z",
113
+ },
114
+ })),
115
+ };
116
+ }
117
+ function makeDeps(overrides = {}) {
118
+ return {
119
+ getClient: () => null,
120
+ isConfigured: () => true,
121
+ getActiveProjectId: () => mockProjectId,
122
+ getActiveWorkspaceId: () => overrides.workspaceId !== undefined
123
+ ? overrides.workspaceId
124
+ : mockWorkspaceId,
125
+ setActiveProject: () => { },
126
+ setActiveWorkspace: () => { },
127
+ getApiUrl: () => "http://localhost",
128
+ getMemoryDir: () => null,
129
+ getUserEmail: () => null,
130
+ saveConfig: () => { },
131
+ resetClient: () => { },
132
+ };
133
+ }
134
+ // ============================================================
135
+ // Unit Tests
136
+ // ============================================================
137
+ describe("Agent profile refresh in runEndSessionPipeline", () => {
138
+ test("creates new entity when none exists", async () => {
139
+ const client = makeMockClient();
140
+ const deps = makeDeps();
141
+ await runEndSessionPipeline(client, deps, "card-1", "completed", 100, {
142
+ agent_identifier: "claude-code",
143
+ agent_name: "Claude Code",
144
+ });
145
+ await flush();
146
+ expect(client.refreshAgentProfiles).toHaveBeenCalledWith(mockWorkspaceId);
147
+ expect(client.getAgentProfile).toHaveBeenCalled();
148
+ expect(client.createMemoryEntity).toHaveBeenCalled();
149
+ const agentEntity = client.createdEntities.find((e) => e.type === "agent");
150
+ expect(agentEntity).toBeTruthy();
151
+ expect(agentEntity.memory_tier).toBe("reference");
152
+ expect(agentEntity.confidence).toBe(1.0);
153
+ expect(agentEntity.title).toBe("Agent Profile: claude-code");
154
+ expect(agentEntity.tags).toContain("agent-profile");
155
+ expect(agentEntity.tags).toContain("claude-code");
156
+ expect(agentEntity.content).toContain("## claude-code Performance Profile");
157
+ });
158
+ test("updates existing entity by title match", async () => {
159
+ const client = makeMockClient();
160
+ client.listMemoryEntities = mock(async () => ({
161
+ entities: [
162
+ {
163
+ id: "existing-1",
164
+ title: "Agent Profile: claude-code",
165
+ agent_identifier: "claude-code",
166
+ },
167
+ ],
168
+ count: 1,
169
+ }));
170
+ const deps = makeDeps();
171
+ await runEndSessionPipeline(client, deps, "card-1", "completed", 100, {
172
+ agent_identifier: "claude-code",
173
+ agent_name: "Claude Code",
174
+ });
175
+ await flush();
176
+ expect(client.updateMemoryEntity).toHaveBeenCalledWith("existing-1", expect.objectContaining({
177
+ content: expect.stringContaining("## claude-code Performance Profile"),
178
+ confidence: 1.0,
179
+ }));
180
+ // Should NOT create a new entity
181
+ const agentEntities = client.createdEntities.filter((e) => e.type === "agent");
182
+ expect(agentEntities).toHaveLength(0);
183
+ });
184
+ test("updates existing entity by agent_identifier match", async () => {
185
+ const client = makeMockClient();
186
+ client.listMemoryEntities = mock(async () => ({
187
+ entities: [
188
+ {
189
+ id: "existing-2",
190
+ title: "Old Title",
191
+ agent_identifier: "claude-code",
192
+ },
193
+ ],
194
+ count: 1,
195
+ }));
196
+ const deps = makeDeps();
197
+ await runEndSessionPipeline(client, deps, "card-1", "completed", 100, {
198
+ agent_identifier: "claude-code",
199
+ agent_name: "Claude Code",
200
+ });
201
+ await flush();
202
+ expect(client.updateMemoryEntity).toHaveBeenCalledWith("existing-2", expect.objectContaining({
203
+ confidence: 1.0,
204
+ }));
205
+ });
206
+ test("skips when workspace is null", async () => {
207
+ const client = makeMockClient();
208
+ const deps = makeDeps({ workspaceId: null });
209
+ await runEndSessionPipeline(client, deps, "card-1", "completed", 100, {
210
+ agent_identifier: "claude-code",
211
+ agent_name: "Claude Code",
212
+ });
213
+ await flush();
214
+ expect(client.refreshAgentProfiles).not.toHaveBeenCalled();
215
+ });
216
+ test('skips when agent is "unknown"', async () => {
217
+ const client = makeMockClient();
218
+ const deps = makeDeps();
219
+ await runEndSessionPipeline(client, deps, "card-1", "completed", 100, {
220
+ agent_identifier: "unknown",
221
+ agent_name: "Unknown",
222
+ });
223
+ await flush();
224
+ expect(client.refreshAgentProfiles).not.toHaveBeenCalled();
225
+ });
226
+ test("skips when sessionData is undefined", async () => {
227
+ const client = makeMockClient();
228
+ const deps = makeDeps();
229
+ await runEndSessionPipeline(client, deps, "card-1", "completed", 100);
230
+ await flush();
231
+ // agent_identifier defaults to "unknown" when sessionData is undefined
232
+ expect(client.refreshAgentProfiles).not.toHaveBeenCalled();
233
+ });
234
+ test("non-fatal on refresh failure", async () => {
235
+ const client = makeMockClient();
236
+ client.refreshAgentProfiles = mock(async () => {
237
+ throw new Error("Refresh service down");
238
+ });
239
+ const deps = makeDeps();
240
+ // Should not throw
241
+ const result = await runEndSessionPipeline(client, deps, "card-1", "completed", 100, {
242
+ agent_identifier: "claude-code",
243
+ agent_name: "Claude Code",
244
+ });
245
+ await flush();
246
+ // Pipeline still returns normally
247
+ expect(result).toBeTruthy();
248
+ expect(typeof result.learningsExtracted).toBe("number");
249
+ });
250
+ test("null profile skips entity upsert", async () => {
251
+ const client = makeMockClient();
252
+ client.getAgentProfile = mock(async () => ({ profile: null }));
253
+ const deps = makeDeps();
254
+ await runEndSessionPipeline(client, deps, "card-1", "completed", 100, {
255
+ agent_identifier: "claude-code",
256
+ agent_name: "Claude Code",
257
+ });
258
+ await flush();
259
+ expect(client.refreshAgentProfiles).toHaveBeenCalled();
260
+ // No agent entity should be created since profile is null
261
+ const agentEntities = client.createdEntities.filter((e) => e.type === "agent");
262
+ expect(agentEntities).toHaveLength(0);
263
+ });
264
+ test("content has correct markdown", async () => {
265
+ const client = makeMockClient();
266
+ const deps = makeDeps();
267
+ await runEndSessionPipeline(client, deps, "card-1", "completed", 100, {
268
+ agent_identifier: "claude-code",
269
+ agent_name: "Claude Code",
270
+ });
271
+ await flush();
272
+ const agentEntity = client.createdEntities.find((e) => e.type === "agent");
273
+ expect(agentEntity).toBeTruthy();
274
+ const content = agentEntity.content;
275
+ expect(content).toContain("## claude-code Performance Profile");
276
+ expect(content).toContain("**Total sessions:**");
277
+ expect(content).toContain("**Completed:**");
278
+ expect(content).toContain("**Paused:**");
279
+ expect(content).toContain("**Blocked:**");
280
+ expect(content).toContain("**Avg duration:**");
281
+ expect(content).toContain("**First session:**");
282
+ expect(content).toContain("**Last session:**");
283
+ });
284
+ test("works with paused sessions", async () => {
285
+ const client = makeMockClient();
286
+ const deps = makeDeps();
287
+ await runEndSessionPipeline(client, deps, "card-1", "paused", 50, {
288
+ agent_identifier: "claude-code",
289
+ agent_name: "Claude Code",
290
+ });
291
+ await flush();
292
+ expect(client.refreshAgentProfiles).toHaveBeenCalled();
293
+ expect(client.getAgentProfile).toHaveBeenCalled();
294
+ });
295
+ test("metadata contains profile data", async () => {
296
+ const client = makeMockClient();
297
+ const deps = makeDeps();
298
+ await runEndSessionPipeline(client, deps, "card-1", "completed", 100, {
299
+ agent_identifier: "claude-code",
300
+ agent_name: "Claude Code",
301
+ });
302
+ await flush();
303
+ const agentEntity = client.createdEntities.find((e) => e.type === "agent");
304
+ expect(agentEntity).toBeTruthy();
305
+ const metadata = agentEntity.metadata;
306
+ expect(metadata.total_sessions).toBe(10);
307
+ expect(metadata.completion_rate_pct).toBe(80);
308
+ expect(metadata.completed_sessions).toBe(8);
309
+ expect(metadata.avg_active_duration_ms).toBe(120000);
310
+ });
311
+ });
312
+ // ============================================================
313
+ // Integration Tests (skipped when not configured)
314
+ // ============================================================
315
+ // Integration tests are skipped in this file because mock.module() overrides
316
+ // the real config. Run integration-memory-crud.test.ts for live API tests,
317
+ // or use MCP tools directly for agent profile integration verification.
318
+ describe.skip("Agent Performance Profiles - Integration (run via MCP tools)", () => {
319
+ test("refreshAgentProfiles returns { refreshed: true }", () => { });
320
+ test('getAgentProfile("claude-code") returns profile or null', () => { });
321
+ test('getAgentProfile("non-existent-xyz") returns null profile', () => { });
322
+ test("listAgentProfiles returns array", () => { });
323
+ test("profile shape has expected fields", () => { });
324
+ test("entity round-trip: create agent entity → recall by type → cleanup", () => { });
325
+ });