@aitytech/agentkits-memory 1.0.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 (116) hide show
  1. package/README.md +250 -0
  2. package/dist/cache-manager.d.ts +134 -0
  3. package/dist/cache-manager.d.ts.map +1 -0
  4. package/dist/cache-manager.js +407 -0
  5. package/dist/cache-manager.js.map +1 -0
  6. package/dist/cli/save.d.ts +20 -0
  7. package/dist/cli/save.d.ts.map +1 -0
  8. package/dist/cli/save.js +94 -0
  9. package/dist/cli/save.js.map +1 -0
  10. package/dist/cli/setup.d.ts +18 -0
  11. package/dist/cli/setup.d.ts.map +1 -0
  12. package/dist/cli/setup.js +163 -0
  13. package/dist/cli/setup.js.map +1 -0
  14. package/dist/cli/viewer.d.ts +21 -0
  15. package/dist/cli/viewer.d.ts.map +1 -0
  16. package/dist/cli/viewer.js +182 -0
  17. package/dist/cli/viewer.js.map +1 -0
  18. package/dist/hnsw-index.d.ts +111 -0
  19. package/dist/hnsw-index.d.ts.map +1 -0
  20. package/dist/hnsw-index.js +781 -0
  21. package/dist/hnsw-index.js.map +1 -0
  22. package/dist/hooks/cli.d.ts +20 -0
  23. package/dist/hooks/cli.d.ts.map +1 -0
  24. package/dist/hooks/cli.js +102 -0
  25. package/dist/hooks/cli.js.map +1 -0
  26. package/dist/hooks/context.d.ts +31 -0
  27. package/dist/hooks/context.d.ts.map +1 -0
  28. package/dist/hooks/context.js +64 -0
  29. package/dist/hooks/context.js.map +1 -0
  30. package/dist/hooks/index.d.ts +16 -0
  31. package/dist/hooks/index.d.ts.map +1 -0
  32. package/dist/hooks/index.js +20 -0
  33. package/dist/hooks/index.js.map +1 -0
  34. package/dist/hooks/observation.d.ts +30 -0
  35. package/dist/hooks/observation.d.ts.map +1 -0
  36. package/dist/hooks/observation.js +79 -0
  37. package/dist/hooks/observation.js.map +1 -0
  38. package/dist/hooks/service.d.ts +102 -0
  39. package/dist/hooks/service.d.ts.map +1 -0
  40. package/dist/hooks/service.js +454 -0
  41. package/dist/hooks/service.js.map +1 -0
  42. package/dist/hooks/session-init.d.ts +30 -0
  43. package/dist/hooks/session-init.d.ts.map +1 -0
  44. package/dist/hooks/session-init.js +54 -0
  45. package/dist/hooks/session-init.js.map +1 -0
  46. package/dist/hooks/summarize.d.ts +30 -0
  47. package/dist/hooks/summarize.d.ts.map +1 -0
  48. package/dist/hooks/summarize.js +74 -0
  49. package/dist/hooks/summarize.js.map +1 -0
  50. package/dist/hooks/types.d.ts +193 -0
  51. package/dist/hooks/types.d.ts.map +1 -0
  52. package/dist/hooks/types.js +137 -0
  53. package/dist/hooks/types.js.map +1 -0
  54. package/dist/index.d.ts +173 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +564 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/mcp/index.d.ts +9 -0
  59. package/dist/mcp/index.d.ts.map +1 -0
  60. package/dist/mcp/index.js +9 -0
  61. package/dist/mcp/index.js.map +1 -0
  62. package/dist/mcp/server.d.ts +22 -0
  63. package/dist/mcp/server.d.ts.map +1 -0
  64. package/dist/mcp/server.js +368 -0
  65. package/dist/mcp/server.js.map +1 -0
  66. package/dist/mcp/tools.d.ts +14 -0
  67. package/dist/mcp/tools.d.ts.map +1 -0
  68. package/dist/mcp/tools.js +110 -0
  69. package/dist/mcp/tools.js.map +1 -0
  70. package/dist/mcp/types.d.ts +100 -0
  71. package/dist/mcp/types.d.ts.map +1 -0
  72. package/dist/mcp/types.js +9 -0
  73. package/dist/mcp/types.js.map +1 -0
  74. package/dist/migration.d.ts +77 -0
  75. package/dist/migration.d.ts.map +1 -0
  76. package/dist/migration.js +457 -0
  77. package/dist/migration.js.map +1 -0
  78. package/dist/sqljs-backend.d.ts +128 -0
  79. package/dist/sqljs-backend.d.ts.map +1 -0
  80. package/dist/sqljs-backend.js +623 -0
  81. package/dist/sqljs-backend.js.map +1 -0
  82. package/dist/types.d.ts +481 -0
  83. package/dist/types.d.ts.map +1 -0
  84. package/dist/types.js +73 -0
  85. package/dist/types.js.map +1 -0
  86. package/hooks.json +46 -0
  87. package/package.json +67 -0
  88. package/src/__tests__/index.test.ts +407 -0
  89. package/src/__tests__/sqljs-backend.test.ts +410 -0
  90. package/src/cache-manager.ts +515 -0
  91. package/src/cli/save.ts +109 -0
  92. package/src/cli/setup.ts +203 -0
  93. package/src/cli/viewer.ts +218 -0
  94. package/src/hnsw-index.ts +1013 -0
  95. package/src/hooks/__tests__/handlers.test.ts +298 -0
  96. package/src/hooks/__tests__/integration.test.ts +431 -0
  97. package/src/hooks/__tests__/service.test.ts +487 -0
  98. package/src/hooks/__tests__/types.test.ts +341 -0
  99. package/src/hooks/cli.ts +121 -0
  100. package/src/hooks/context.ts +77 -0
  101. package/src/hooks/index.ts +23 -0
  102. package/src/hooks/observation.ts +102 -0
  103. package/src/hooks/service.ts +582 -0
  104. package/src/hooks/session-init.ts +70 -0
  105. package/src/hooks/summarize.ts +89 -0
  106. package/src/hooks/types.ts +365 -0
  107. package/src/index.ts +755 -0
  108. package/src/mcp/__tests__/server.test.ts +181 -0
  109. package/src/mcp/index.ts +9 -0
  110. package/src/mcp/server.ts +441 -0
  111. package/src/mcp/tools.ts +113 -0
  112. package/src/mcp/types.ts +109 -0
  113. package/src/migration.ts +574 -0
  114. package/src/sql.js.d.ts +70 -0
  115. package/src/sqljs-backend.ts +789 -0
  116. package/src/types.ts +715 -0
@@ -0,0 +1,431 @@
1
+ /**
2
+ * Integration Tests for Hook System
3
+ *
4
+ * Tests the full hook flow from session start to end.
5
+ *
6
+ * @module @agentkits/memory/hooks/__tests__/integration
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
10
+ import { existsSync, rmSync, mkdirSync } from 'node:fs';
11
+ import * as path from 'node:path';
12
+ import { NormalizedHookInput, parseHookInput } from '../types.js';
13
+ import { MemoryHookService } from '../service.js';
14
+ import { createContextHook } from '../context.js';
15
+ import { createSessionInitHook } from '../session-init.js';
16
+ import { createObservationHook } from '../observation.js';
17
+ import { createSummarizeHook } from '../summarize.js';
18
+
19
+ const TEST_DIR = path.join(process.cwd(), '.test-integration-hooks');
20
+
21
+ function createTestInput(overrides: Partial<NormalizedHookInput> = {}): NormalizedHookInput {
22
+ return {
23
+ sessionId: 'integration-session',
24
+ cwd: TEST_DIR,
25
+ project: 'test-project',
26
+ timestamp: Date.now(),
27
+ ...overrides,
28
+ };
29
+ }
30
+
31
+ describe('Hook System Integration', () => {
32
+ beforeEach(() => {
33
+ // Clean up test directory
34
+ if (existsSync(TEST_DIR)) {
35
+ rmSync(TEST_DIR, { recursive: true });
36
+ }
37
+ mkdirSync(TEST_DIR, { recursive: true });
38
+ });
39
+
40
+ afterEach(() => {
41
+ // Clean up test directory
42
+ if (existsSync(TEST_DIR)) {
43
+ rmSync(TEST_DIR, { recursive: true });
44
+ }
45
+ });
46
+
47
+ describe('Full Session Flow', () => {
48
+ it('should complete a full session lifecycle', async () => {
49
+ const sessionId = 'full-flow-session';
50
+ const project = 'test-project';
51
+
52
+ // 1. Session Start - Context Hook (no previous context)
53
+ const contextHook = createContextHook(TEST_DIR);
54
+ const contextResult = await contextHook.execute(
55
+ createTestInput({ sessionId, project })
56
+ );
57
+
58
+ expect(contextResult.continue).toBe(true);
59
+ expect(contextResult.additionalContext).toBeUndefined(); // No previous sessions
60
+
61
+ // 2. User Prompt Submit - Session Init Hook
62
+ const sessionInitHook = createSessionInitHook(TEST_DIR);
63
+ const sessionInitResult = await sessionInitHook.execute(
64
+ createTestInput({ sessionId, project, prompt: 'Help me implement a feature' })
65
+ );
66
+
67
+ expect(sessionInitResult.continue).toBe(true);
68
+
69
+ // 3. Tool Uses - Observation Hooks
70
+ const observationHook = createObservationHook(TEST_DIR);
71
+
72
+ // Simulate reading files
73
+ await observationHook.execute(
74
+ createTestInput({
75
+ sessionId,
76
+ project,
77
+ toolName: 'Read',
78
+ toolInput: { file_path: 'src/index.ts' },
79
+ toolResponse: { content: 'export function main() {}' },
80
+ })
81
+ );
82
+
83
+ // Simulate grep search
84
+ await observationHook.execute(
85
+ createTestInput({
86
+ sessionId,
87
+ project,
88
+ toolName: 'Grep',
89
+ toolInput: { pattern: 'function', path: 'src' },
90
+ toolResponse: { matches: ['src/index.ts:1'] },
91
+ })
92
+ );
93
+
94
+ // Simulate writing file
95
+ await observationHook.execute(
96
+ createTestInput({
97
+ sessionId,
98
+ project,
99
+ toolName: 'Write',
100
+ toolInput: { file_path: 'src/feature.ts' },
101
+ toolResponse: { success: true },
102
+ })
103
+ );
104
+
105
+ // Simulate running tests
106
+ await observationHook.execute(
107
+ createTestInput({
108
+ sessionId,
109
+ project,
110
+ toolName: 'Bash',
111
+ toolInput: { command: 'npm test' },
112
+ toolResponse: { stdout: 'All tests passed' },
113
+ })
114
+ );
115
+
116
+ // 4. Session End - Summarize Hook
117
+ const summarizeHook = createSummarizeHook(TEST_DIR);
118
+ const summarizeResult = await summarizeHook.execute(
119
+ createTestInput({ sessionId, project, stopReason: 'user_exit' })
120
+ );
121
+
122
+ expect(summarizeResult.continue).toBe(true);
123
+
124
+ // Verify final state
125
+ const service = new MemoryHookService(TEST_DIR);
126
+ await service.initialize();
127
+
128
+ const session = service.getSession(sessionId);
129
+ expect(session).not.toBeNull();
130
+ expect(session?.status).toBe('completed');
131
+ expect(session?.observationCount).toBe(4);
132
+ expect(session?.summary).toBeDefined();
133
+ expect(session?.summary).toContain('file(s) modified');
134
+ expect(session?.summary).toContain('file(s) read');
135
+ expect(session?.summary).toContain('command(s) executed');
136
+
137
+ const observations = await service.getSessionObservations(sessionId);
138
+ expect(observations.length).toBe(4);
139
+
140
+ await service.shutdown();
141
+ });
142
+
143
+ it('should provide context from previous sessions', async () => {
144
+ // Session 1: Complete a full session
145
+ const session1Id = 'previous-session';
146
+ const project = 'test-project';
147
+
148
+ // Init session 1
149
+ const initHook1 = createSessionInitHook(TEST_DIR);
150
+ await initHook1.execute(createTestInput({ sessionId: session1Id, project, prompt: 'First task' }));
151
+
152
+ // Add observations to session 1
153
+ const obsHook1 = createObservationHook(TEST_DIR);
154
+ await obsHook1.execute(createTestInput({
155
+ sessionId: session1Id,
156
+ project,
157
+ toolName: 'Write',
158
+ toolInput: { file_path: 'src/auth.ts' },
159
+ toolResponse: {},
160
+ }));
161
+
162
+ // Complete session 1
163
+ const sumHook1 = createSummarizeHook(TEST_DIR);
164
+ await sumHook1.execute(createTestInput({ sessionId: session1Id, project }));
165
+
166
+ // Session 2: Should see context from session 1
167
+ const session2Id = 'current-session';
168
+
169
+ const contextHook2 = createContextHook(TEST_DIR);
170
+ const contextResult = await contextHook2.execute(
171
+ createTestInput({ sessionId: session2Id, project })
172
+ );
173
+
174
+ expect(contextResult.continue).toBe(true);
175
+ expect(contextResult.suppressOutput).toBe(false);
176
+ expect(contextResult.additionalContext).toBeDefined();
177
+ expect(contextResult.additionalContext).toContain('Previous Sessions');
178
+ expect(contextResult.additionalContext).toContain('Recent Activity');
179
+ expect(contextResult.additionalContext).toContain('Write');
180
+ });
181
+
182
+ it('should handle multiple projects independently', async () => {
183
+ // Session for project A
184
+ const initHookA = createSessionInitHook(TEST_DIR);
185
+ await initHookA.execute(createTestInput({
186
+ sessionId: 'session-a',
187
+ project: 'project-a',
188
+ prompt: 'Task for A',
189
+ }));
190
+
191
+ const obsHookA = createObservationHook(TEST_DIR);
192
+ await obsHookA.execute(createTestInput({
193
+ sessionId: 'session-a',
194
+ project: 'project-a',
195
+ toolName: 'Write',
196
+ toolInput: { file_path: 'a.ts' },
197
+ toolResponse: {},
198
+ }));
199
+
200
+ // Session for project B
201
+ const initHookB = createSessionInitHook(TEST_DIR);
202
+ await initHookB.execute(createTestInput({
203
+ sessionId: 'session-b',
204
+ project: 'project-b',
205
+ prompt: 'Task for B',
206
+ }));
207
+
208
+ const obsHookB = createObservationHook(TEST_DIR);
209
+ await obsHookB.execute(createTestInput({
210
+ sessionId: 'session-b',
211
+ project: 'project-b',
212
+ toolName: 'Read',
213
+ toolInput: { file_path: 'b.ts' },
214
+ toolResponse: {},
215
+ }));
216
+
217
+ // Verify isolation
218
+ const service = new MemoryHookService(TEST_DIR);
219
+ await service.initialize();
220
+
221
+ const sessionsA = await service.getRecentSessions('project-a', 10);
222
+ const sessionsB = await service.getRecentSessions('project-b', 10);
223
+
224
+ expect(sessionsA.length).toBe(1);
225
+ expect(sessionsB.length).toBe(1);
226
+ expect(sessionsA[0].prompt).toBe('Task for A');
227
+ expect(sessionsB[0].prompt).toBe('Task for B');
228
+
229
+ const obsA = await service.getRecentObservations('project-a', 10);
230
+ const obsB = await service.getRecentObservations('project-b', 10);
231
+
232
+ expect(obsA.length).toBe(1);
233
+ expect(obsB.length).toBe(1);
234
+ expect(obsA[0].toolName).toBe('Write');
235
+ expect(obsB[0].toolName).toBe('Read');
236
+
237
+ await service.shutdown();
238
+ });
239
+ });
240
+
241
+ describe('CLI Input Parsing Integration', () => {
242
+ it('should parse and process real Claude Code input', async () => {
243
+ // Simulate real Claude Code hook input
244
+ const claudeInput = JSON.stringify({
245
+ session_id: 'abc123',
246
+ cwd: TEST_DIR,
247
+ prompt: 'Help me fix the bug',
248
+ tool_name: 'Read',
249
+ tool_input: { file_path: '/path/to/file.ts' },
250
+ tool_result: { content: 'file contents here' },
251
+ });
252
+
253
+ const parsed = parseHookInput(claudeInput);
254
+
255
+ expect(parsed.sessionId).toBe('abc123');
256
+ expect(parsed.cwd).toBe(TEST_DIR);
257
+ expect(parsed.prompt).toBe('Help me fix the bug');
258
+ expect(parsed.toolName).toBe('Read');
259
+ expect(parsed.toolInput).toEqual({ file_path: '/path/to/file.ts' });
260
+ expect(parsed.toolResponse).toEqual({ content: 'file contents here' });
261
+
262
+ // Process through observation hook
263
+ const observationHook = createObservationHook(TEST_DIR);
264
+ const result = await observationHook.execute(parsed);
265
+
266
+ expect(result.continue).toBe(true);
267
+
268
+ // Verify stored
269
+ const service = new MemoryHookService(TEST_DIR);
270
+ await service.initialize();
271
+ const obs = await service.getSessionObservations('abc123');
272
+ await service.shutdown();
273
+
274
+ expect(obs.length).toBe(1);
275
+ expect(obs[0].title).toBe('Read /path/to/file.ts');
276
+ });
277
+ });
278
+
279
+ describe('Error Recovery', () => {
280
+ it('should continue working after errors', async () => {
281
+ const sessionId = 'error-recovery-session';
282
+ const project = 'test-project';
283
+
284
+ // Init session
285
+ const initHook = createSessionInitHook(TEST_DIR);
286
+ await initHook.execute(createTestInput({ sessionId, project }));
287
+
288
+ // Successful observation
289
+ const obsHook = createObservationHook(TEST_DIR);
290
+ await obsHook.execute(createTestInput({
291
+ sessionId,
292
+ project,
293
+ toolName: 'Read',
294
+ toolInput: {},
295
+ toolResponse: {},
296
+ }));
297
+
298
+ // Another successful observation
299
+ await obsHook.execute(createTestInput({
300
+ sessionId,
301
+ project,
302
+ toolName: 'Write',
303
+ toolInput: {},
304
+ toolResponse: {},
305
+ }));
306
+
307
+ // Verify both observations stored
308
+ const service = new MemoryHookService(TEST_DIR);
309
+ await service.initialize();
310
+ const obs = await service.getSessionObservations(sessionId);
311
+ await service.shutdown();
312
+
313
+ expect(obs.length).toBe(2);
314
+ });
315
+ });
316
+
317
+ describe('Multiple Sessions', () => {
318
+ it('should handle multiple sessions sequentially', async () => {
319
+ const project = 'test-project';
320
+
321
+ // Start two sessions sequentially (SQLite doesn't handle concurrent writes well)
322
+ const initHook1 = createSessionInitHook(TEST_DIR);
323
+ await initHook1.execute(createTestInput({ sessionId: 'multi-1', project }));
324
+
325
+ const initHook2 = createSessionInitHook(TEST_DIR);
326
+ await initHook2.execute(createTestInput({ sessionId: 'multi-2', project }));
327
+
328
+ // Add observations sequentially
329
+ const obsHook1 = createObservationHook(TEST_DIR);
330
+ await obsHook1.execute(createTestInput({
331
+ sessionId: 'multi-1',
332
+ project,
333
+ toolName: 'Read',
334
+ toolInput: {},
335
+ toolResponse: {},
336
+ }));
337
+
338
+ const obsHook2 = createObservationHook(TEST_DIR);
339
+ await obsHook2.execute(createTestInput({
340
+ sessionId: 'multi-2',
341
+ project,
342
+ toolName: 'Write',
343
+ toolInput: {},
344
+ toolResponse: {},
345
+ }));
346
+
347
+ // Verify both sessions have their observations
348
+ const service = new MemoryHookService(TEST_DIR);
349
+ await service.initialize();
350
+
351
+ const obs1 = await service.getSessionObservations('multi-1');
352
+ const obs2 = await service.getSessionObservations('multi-2');
353
+
354
+ expect(obs1.length).toBe(1);
355
+ expect(obs2.length).toBe(1);
356
+ expect(obs1[0].toolName).toBe('Read');
357
+ expect(obs2[0].toolName).toBe('Write');
358
+
359
+ await service.shutdown();
360
+ });
361
+ });
362
+
363
+ describe('Large Data Handling', () => {
364
+ it('should handle many observations efficiently', async () => {
365
+ const sessionId = 'large-data-session';
366
+ const project = 'test-project';
367
+
368
+ // Init session
369
+ const initHook = createSessionInitHook(TEST_DIR);
370
+ await initHook.execute(createTestInput({ sessionId, project }));
371
+
372
+ // Add many observations
373
+ const obsHook = createObservationHook(TEST_DIR);
374
+ const observationCount = 50;
375
+
376
+ for (let i = 0; i < observationCount; i++) {
377
+ await obsHook.execute(createTestInput({
378
+ sessionId,
379
+ project,
380
+ toolName: i % 2 === 0 ? 'Read' : 'Write',
381
+ toolInput: { file_path: `file${i}.ts` },
382
+ toolResponse: { content: `content ${i}` },
383
+ }));
384
+ }
385
+
386
+ // Verify all observations stored
387
+ const service = new MemoryHookService(TEST_DIR);
388
+ await service.initialize();
389
+
390
+ const session = service.getSession(sessionId);
391
+ expect(session?.observationCount).toBe(observationCount);
392
+
393
+ const obs = await service.getSessionObservations(sessionId);
394
+ expect(obs.length).toBe(observationCount);
395
+
396
+ await service.shutdown();
397
+ });
398
+
399
+ it('should truncate large tool responses', async () => {
400
+ const sessionId = 'large-response-session';
401
+ const project = 'test-project';
402
+
403
+ // Init session
404
+ const initHook = createSessionInitHook(TEST_DIR);
405
+ await initHook.execute(createTestInput({ sessionId, project }));
406
+
407
+ // Add observation with large response
408
+ const obsHook = createObservationHook(TEST_DIR);
409
+ const largeContent = 'A'.repeat(100000); // 100KB
410
+
411
+ await obsHook.execute(createTestInput({
412
+ sessionId,
413
+ project,
414
+ toolName: 'Read',
415
+ toolInput: { file_path: 'large.ts' },
416
+ toolResponse: { content: largeContent },
417
+ }));
418
+
419
+ // Verify response was truncated
420
+ const service = new MemoryHookService(TEST_DIR);
421
+ await service.initialize();
422
+
423
+ const obs = await service.getSessionObservations(sessionId);
424
+ expect(obs.length).toBe(1);
425
+ expect(obs[0].toolResponse.length).toBeLessThan(100000);
426
+ expect(obs[0].toolResponse).toContain('[truncated]');
427
+
428
+ await service.shutdown();
429
+ });
430
+ });
431
+ });