@slorenzot/memento-core 0.7.0 → 2.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.
@@ -1,131 +0,0 @@
1
- import { readFileSync, existsSync } from 'fs';
2
- import { join, dirname } from 'path';
3
- import { homedir } from 'os';
4
-
5
- export interface MementoConfig {
6
- storageMethod?: 'database' | 'storage';
7
- dbPath?: string;
8
- storagePath?: string;
9
- projectId?: string;
10
- }
11
-
12
- const DEFAULT_CONFIG: MementoConfig = {
13
- storageMethod: 'database',
14
- dbPath: '.memento/db/memento.db',
15
- storagePath: 'database/storage',
16
- };
17
-
18
- const GLOBAL_CONFIG_PATH = join(homedir(), '.memento', 'config');
19
- const LOCAL_CONFIG_FILE = '.mementorc';
20
-
21
- function loadJSONFile<T>(path: string): T | null {
22
- if (!existsSync(path)) {
23
- return null;
24
- }
25
-
26
- try {
27
- const content = readFileSync(path, 'utf-8');
28
- return JSON.parse(content);
29
- } catch {
30
- return null;
31
- }
32
- }
33
-
34
- export function findProjectConfig(startDir: string = process.cwd()): MementoConfig | null {
35
- let currentDir = startDir;
36
- const maxDepth = 10;
37
- let depth = 0;
38
-
39
- while (depth < maxDepth) {
40
- const configPath = join(currentDir, LOCAL_CONFIG_FILE);
41
-
42
- if (existsSync(configPath)) {
43
- const config = loadJSONFile<MementoConfig>(configPath);
44
- if (config) {
45
- return config;
46
- }
47
- }
48
-
49
- const parentDir = dirname(currentDir);
50
-
51
- if (parentDir === currentDir) {
52
- break;
53
- }
54
-
55
- currentDir = parentDir;
56
- depth++;
57
- }
58
-
59
- return null;
60
- }
61
-
62
- export function loadConfig(): MementoConfig {
63
- let config: MementoConfig = { ...DEFAULT_CONFIG };
64
-
65
- const projectConfig = findProjectConfig();
66
- if (projectConfig) {
67
- config = { ...config, ...projectConfig };
68
- }
69
-
70
- const globalConfig = loadJSONFile<MementoConfig>(GLOBAL_CONFIG_PATH);
71
- if (globalConfig) {
72
- config = { ...config, ...globalConfig };
73
- }
74
-
75
- if (process.env.MEMENTO_STORAGE_METHOD) {
76
- config.storageMethod = process.env.MEMENTO_STORAGE_METHOD as 'database' | 'storage';
77
- }
78
-
79
- if (process.env.MEMENTO_DB_PATH) {
80
- config.dbPath = process.env.MEMENTO_DB_PATH;
81
- }
82
-
83
- if (process.env.MEMENTO_STORAGE_PATH) {
84
- config.storagePath = process.env.MEMENTO_STORAGE_PATH;
85
- }
86
-
87
- if (process.env.MEMENTO_PROJECT_ID) {
88
- config.projectId = process.env.MEMENTO_PROJECT_ID;
89
- }
90
-
91
- return config;
92
- }
93
-
94
- export function resolveStoragePath(config: MementoConfig): string {
95
- const storagePath = config.storagePath || DEFAULT_CONFIG.storagePath!;
96
-
97
- if (storagePath.startsWith('/')) {
98
- return storagePath;
99
- }
100
-
101
- if (storagePath.startsWith('~/')) {
102
- return join(homedir(), storagePath.slice(2));
103
- }
104
-
105
- return join(process.cwd(), storagePath);
106
- }
107
-
108
- export function resolveDbPath(config: MementoConfig): string {
109
- const dbPath = config.dbPath || DEFAULT_CONFIG.dbPath!;
110
-
111
- if (dbPath.startsWith('/')) {
112
- return dbPath;
113
- }
114
-
115
- if (dbPath.startsWith('~/')) {
116
- return join(homedir(), dbPath.slice(2));
117
- }
118
-
119
- return join(process.cwd(), dbPath);
120
- }
121
-
122
- export function getProjectId(config: MementoConfig): string {
123
- if (config.projectId) {
124
- return config.projectId;
125
- }
126
-
127
- const packageJsonPath = join(process.cwd(), 'package.json');
128
- const packageJson = loadJSONFile<{ name?: string }>(packageJsonPath);
129
-
130
- return packageJson?.name || 'default';
131
- }
@@ -1,456 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'bun:test';
2
- import { MemoryEngine } from './MemoryEngine';
3
- import type { Observation, Session, Prompt } from './types';
4
- import { mkdirSync, rmSync, existsSync } from 'fs';
5
- import { join } from 'path';
6
-
7
- describe('MemoryEngine', () => {
8
- let engine: MemoryEngine;
9
- let testDbPath: string;
10
- const testDir = join(process.cwd(), 'test-data');
11
-
12
- beforeAll(() => {
13
- if (existsSync(testDir)) {
14
- rmSync(testDir, { recursive: true, force: true });
15
- }
16
- mkdirSync(testDir, { recursive: true });
17
- });
18
-
19
- afterAll(() => {
20
- if (existsSync(testDir)) {
21
- rmSync(testDir, { recursive: true, force: true });
22
- }
23
- });
24
-
25
- beforeEach(() => {
26
- testDbPath = join(testDir, `test-${Date.now()}-${Math.random().toString(36).slice(7)}.db`);
27
- engine = new MemoryEngine(testDbPath);
28
- });
29
-
30
- afterEach(() => {
31
- engine.close();
32
- });
33
-
34
- describe('Database Path Management', () => {
35
- it('should create database in specified path', () => {
36
- expect(existsSync(testDbPath)).toBe(true);
37
- });
38
-
39
- it('should return the database path', () => {
40
- const path = engine.getDatabasePath();
41
- expect(path).toBe(testDbPath);
42
- });
43
- });
44
-
45
- describe('Session Management', () => {
46
- it('should create a session successfully', async () => {
47
- const session = await engine.createSession({
48
- projectId: 'test-project',
49
- endedAt: null,
50
- metadata: { agent: 'test-agent' },
51
- });
52
-
53
- expect(session).toBeDefined();
54
- expect(session.id).toBeDefined();
55
- expect(session.uuid).toBeDefined();
56
- expect(session.projectId).toBe('test-project');
57
- expect(session.metadata).toEqual({ agent: 'test-agent' });
58
- });
59
-
60
- it('should get a session by id', async () => {
61
- const created = await engine.createSession({
62
- projectId: 'test-project',
63
- endedAt: null,
64
- metadata: {},
65
- });
66
-
67
- const retrieved = await engine.getSession(created.id);
68
-
69
- expect(retrieved).toBeDefined();
70
- expect(retrieved?.id).toBe(created.id);
71
- expect(retrieved?.uuid).toBe(created.uuid);
72
- });
73
-
74
- it('should end a session', async () => {
75
- const session = await engine.createSession({
76
- projectId: 'test-project',
77
- endedAt: null,
78
- metadata: {},
79
- });
80
-
81
- expect(session.endedAt).toBeNull();
82
-
83
- await new Promise((resolve) => setTimeout(resolve, 10));
84
-
85
- const ended = await engine.endSession(session.id);
86
-
87
- expect(ended.endedAt).not.toBeNull();
88
- expect(ended.endedAt?.getTime()).toBeGreaterThan(session.startedAt.getTime());
89
- });
90
-
91
- it('should return null for non-existent session', async () => {
92
- const session = await engine.getSession(99999);
93
- expect(session).toBeNull();
94
- });
95
- });
96
-
97
- describe('Observation Management', () => {
98
- let session: Session;
99
-
100
- beforeEach(async () => {
101
- session = await engine.createSession({
102
- projectId: 'test-project',
103
- endedAt: null,
104
- metadata: {},
105
- });
106
- });
107
-
108
- it('should create an observation successfully', async () => {
109
- const observation = await engine.createObservation({
110
- sessionId: session.id,
111
- title: 'Test Decision',
112
- content: 'This is a test decision',
113
- type: 'decision',
114
- topicKey: 'test-topic',
115
- projectId: 'test-project',
116
- metadata: { source: 'test' },
117
- });
118
-
119
- expect(observation).toBeDefined();
120
- expect(observation.id).toBeDefined();
121
- expect(observation.title).toBe('Test Decision');
122
- expect(observation.type).toBe('decision');
123
- expect(observation.sessionId).toBe(session.id);
124
- });
125
-
126
- it('should get an observation by id', async () => {
127
- const created = await engine.createObservation({
128
- sessionId: session.id,
129
- title: 'Get Test',
130
- content: 'Get this observation',
131
- type: 'note',
132
- topicKey: 'get-topic',
133
- projectId: 'test-project',
134
- metadata: {},
135
- });
136
-
137
- const retrieved = await engine.getObservation(created.id);
138
-
139
- expect(retrieved).toBeDefined();
140
- expect(retrieved?.id).toBe(created.id);
141
- expect(retrieved?.title).toBe('Get Test');
142
- });
143
-
144
- it('should update an observation', async () => {
145
- const created = await engine.createObservation({
146
- sessionId: session.id,
147
- title: 'Original Title',
148
- content: 'Original content',
149
- type: 'decision',
150
- topicKey: 'update-topic',
151
- projectId: 'test-project',
152
- metadata: {},
153
- });
154
-
155
- const updated = await engine.updateObservation(created.id, {
156
- title: 'Updated Title',
157
- content: 'Updated content',
158
- });
159
-
160
- expect(updated.title).toBe('Updated Title');
161
- expect(updated.content).toBe('Updated content');
162
- expect(updated.type).toBe('decision');
163
- });
164
-
165
- it('should delete an observation', async () => {
166
- const created = await engine.createObservation({
167
- sessionId: session.id,
168
- title: 'To Delete',
169
- content: 'This will be deleted',
170
- type: 'note',
171
- topicKey: 'delete-topic',
172
- projectId: 'test-project',
173
- metadata: {},
174
- });
175
-
176
- await engine.deleteObservation(created.id);
177
-
178
- const retrieved = await engine.getObservation(created.id);
179
- expect(retrieved).toBeNull();
180
- });
181
-
182
- it('should return null for non-existent observation', async () => {
183
- const observation = await engine.getObservation(99999);
184
- expect(observation).toBeNull();
185
- });
186
-
187
- it('should handle all observation types', async () => {
188
- const types: Array<Observation['type']> = ['decision', 'bug', 'discovery', 'note'];
189
-
190
- for (const type of types) {
191
- const observation = await engine.createObservation({
192
- sessionId: session.id,
193
- title: `${type} Observation`,
194
- content: `Content for ${type}`,
195
- type,
196
- topicKey: `${type}-topic`,
197
- projectId: 'test-project',
198
- metadata: {},
199
- });
200
-
201
- expect(observation.type).toBe(type);
202
- }
203
- });
204
-
205
- it('should handle null topic key', async () => {
206
- const observation = await engine.createObservation({
207
- sessionId: session.id,
208
- title: 'No Topic',
209
- content: 'This has no topic',
210
- type: 'note',
211
- topicKey: null,
212
- projectId: 'test-project',
213
- metadata: {},
214
- });
215
-
216
- expect(observation.topicKey).toBeNull();
217
- });
218
- });
219
-
220
- describe('Prompt Management', () => {
221
- let session: Session;
222
-
223
- beforeEach(async () => {
224
- session = await engine.createSession({
225
- projectId: 'test-project',
226
- endedAt: null,
227
- metadata: {},
228
- });
229
- });
230
-
231
- it('should save a prompt successfully', async () => {
232
- const prompt = await engine.savePrompt({
233
- sessionId: session.id,
234
- content: 'This is a test prompt',
235
- projectId: 'test-project',
236
- metadata: { length: 21 },
237
- });
238
-
239
- expect(prompt).toBeDefined();
240
- expect(prompt.id).toBeDefined();
241
- expect(prompt.content).toBe('This is a test prompt');
242
- expect(prompt.sessionId).toBe(session.id);
243
- expect(prompt.metadata).toEqual({ length: 21 });
244
- });
245
- });
246
-
247
- describe('Search Functionality', () => {
248
- let session: Session;
249
-
250
- beforeEach(async () => {
251
- session = await engine.createSession({
252
- projectId: 'search-project',
253
- endedAt: null,
254
- metadata: {},
255
- });
256
-
257
- await engine.createObservation({
258
- sessionId: session.id,
259
- title: 'Authentication Fix',
260
- content: 'Fixed the JWT authentication bug',
261
- type: 'bug',
262
- topicKey: 'auth',
263
- projectId: 'search-project',
264
- metadata: {},
265
- });
266
-
267
- await engine.createObservation({
268
- sessionId: session.id,
269
- title: 'Performance Decision',
270
- content: 'Decided to use Redis for caching',
271
- type: 'decision',
272
- topicKey: 'performance',
273
- projectId: 'search-project',
274
- metadata: {},
275
- });
276
-
277
- await engine.createObservation({
278
- sessionId: session.id,
279
- title: 'Database Discovery',
280
- content: 'Found a better way to query the database',
281
- type: 'discovery',
282
- topicKey: 'database',
283
- projectId: 'search-project',
284
- metadata: {},
285
- });
286
- });
287
-
288
- it('should search all observations', async () => {
289
- const result = await engine.search({});
290
-
291
- expect(result.observations.length).toBeGreaterThanOrEqual(3);
292
- expect(result.total).toBeGreaterThanOrEqual(3);
293
- });
294
-
295
- it('should search by type', async () => {
296
- const result = await engine.search({ type: 'bug' });
297
-
298
- expect(result.observations).toHaveLength(1);
299
- expect(result.observations[0].type).toBe('bug');
300
- expect(result.observations[0].title).toContain('Authentication');
301
- });
302
-
303
- it('should search by project id', async () => {
304
- const result = await engine.search({ projectId: 'search-project' });
305
-
306
- expect(result.observations.length).toBeGreaterThanOrEqual(3);
307
- result.observations.forEach((obs: any) => {
308
- expect(obs.projectId).toBe('search-project');
309
- });
310
- });
311
-
312
- it('should search by topic key', async () => {
313
- const result = await engine.search({ topicKey: 'auth' });
314
-
315
- expect(result.observations).toHaveLength(1);
316
- expect(result.observations[0].topicKey).toBe('auth');
317
- });
318
-
319
- it('should limit results', async () => {
320
- const result = await engine.search({ limit: 2 });
321
-
322
- expect(result.observations.length).toBeLessThanOrEqual(2);
323
- });
324
-
325
- it('should offset results', async () => {
326
- const firstPage = await engine.search({ limit: 2, offset: 0 });
327
- const secondPage = await engine.search({ limit: 2, offset: 2 });
328
-
329
- expect(firstPage.observations.length).toBe(2);
330
- if (secondPage.observations.length > 0) {
331
- const firstIds = firstPage.observations.map((o: any) => o.id);
332
- const secondIds = secondPage.observations.map((o: any) => o.id);
333
- expect(firstIds).not.toEqual(secondIds);
334
- }
335
- });
336
-
337
- it('should return correct total count', async () => {
338
- const result = await engine.search({ projectId: 'search-project' });
339
-
340
- expect(result.total).toBe(result.observations.length);
341
- });
342
-
343
- it('should handle empty search results', async () => {
344
- const result = await engine.search({
345
- projectId: 'non-existent-project',
346
- });
347
-
348
- expect(result.observations).toHaveLength(0);
349
- expect(result.total).toBe(0);
350
- });
351
- });
352
-
353
- describe('Metadata Handling', () => {
354
- let session: Session;
355
-
356
- beforeEach(async () => {
357
- session = await engine.createSession({
358
- projectId: 'metadata-project',
359
- endedAt: null,
360
- metadata: {},
361
- });
362
- });
363
-
364
- it('should serialize and deserialize complex metadata', async () => {
365
- const complexMetadata = {
366
- nested: {
367
- array: [1, 2, 3],
368
- object: { key: 'value' },
369
- },
370
- string: 'test',
371
- number: 42,
372
- boolean: true,
373
- nullValue: null,
374
- };
375
-
376
- const observation = await engine.createObservation({
377
- sessionId: session.id,
378
- title: 'Complex Metadata',
379
- content: 'Testing metadata serialization',
380
- type: 'note',
381
- topicKey: 'metadata',
382
- projectId: 'metadata-project',
383
- metadata: complexMetadata,
384
- });
385
-
386
- expect(observation.metadata).toEqual(complexMetadata);
387
- });
388
-
389
- it('should handle empty metadata', async () => {
390
- const observation = await engine.createObservation({
391
- sessionId: session.id,
392
- title: 'Empty Metadata',
393
- content: 'No metadata here',
394
- type: 'note',
395
- topicKey: 'empty',
396
- projectId: 'metadata-project',
397
- metadata: {},
398
- });
399
-
400
- expect(observation.metadata).toEqual({});
401
- });
402
-
403
- it('should update metadata', async () => {
404
- const created = await engine.createObservation({
405
- sessionId: session.id,
406
- title: 'Update Metadata Test',
407
- content: 'Will update metadata',
408
- type: 'note',
409
- topicKey: 'update',
410
- projectId: 'metadata-project',
411
- metadata: { version: 1 },
412
- });
413
-
414
- const updated = await engine.updateObservation(created.id, {
415
- metadata: { version: 2, changed: true },
416
- });
417
-
418
- expect(updated.metadata).toEqual({ version: 2, changed: true });
419
- });
420
- });
421
-
422
- describe('Cross-Session Persistence', () => {
423
- it('should persist observations across sessions', async () => {
424
- const session1 = await engine.createSession({
425
- projectId: 'persist-test',
426
- endedAt: null,
427
- metadata: {},
428
- });
429
-
430
- const obs = await engine.createObservation({
431
- sessionId: session1.id,
432
- title: 'Persistent Note',
433
- content: 'This should persist',
434
- type: 'note',
435
- topicKey: 'test',
436
- projectId: 'persist-test',
437
- metadata: {},
438
- });
439
-
440
- await engine.endSession(session1.id);
441
-
442
- const session2 = await engine.createSession({
443
- projectId: 'persist-test',
444
- endedAt: null,
445
- metadata: {},
446
- });
447
-
448
- const searchResult = await engine.search({
449
- projectId: 'persist-test',
450
- });
451
-
452
- expect(searchResult.total).toBe(1);
453
- expect(searchResult.observations[0].id).toBe(obs.id);
454
- });
455
- });
456
- });