@auxiora/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.
- package/LICENSE +191 -0
- package/dist/extractor.d.ts +40 -0
- package/dist/extractor.d.ts.map +1 -0
- package/dist/extractor.js +130 -0
- package/dist/extractor.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/partition.d.ts +25 -0
- package/dist/partition.d.ts.map +1 -0
- package/dist/partition.js +104 -0
- package/dist/partition.js.map +1 -0
- package/dist/pattern-detector.d.ts +27 -0
- package/dist/pattern-detector.d.ts.map +1 -0
- package/dist/pattern-detector.js +260 -0
- package/dist/pattern-detector.js.map +1 -0
- package/dist/personality-adapter.d.ts +10 -0
- package/dist/personality-adapter.d.ts.map +1 -0
- package/dist/personality-adapter.js +72 -0
- package/dist/personality-adapter.js.map +1 -0
- package/dist/retriever.d.ts +15 -0
- package/dist/retriever.d.ts.map +1 -0
- package/dist/retriever.js +179 -0
- package/dist/retriever.js.map +1 -0
- package/dist/sentiment.d.ts +7 -0
- package/dist/sentiment.d.ts.map +1 -0
- package/dist/sentiment.js +118 -0
- package/dist/sentiment.js.map +1 -0
- package/dist/store.d.ts +41 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +352 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +26 -0
- package/src/extractor.ts +212 -0
- package/src/index.ts +22 -0
- package/src/partition.ts +123 -0
- package/src/pattern-detector.ts +304 -0
- package/src/personality-adapter.ts +82 -0
- package/src/retriever.ts +213 -0
- package/src/sentiment.ts +129 -0
- package/src/store.ts +384 -0
- package/src/types.ts +92 -0
- package/tests/extractor.test.ts +247 -0
- package/tests/partition.test.ts +168 -0
- package/tests/pattern-detector.test.ts +150 -0
- package/tests/personality-adapter.test.ts +155 -0
- package/tests/retriever.test.ts +240 -0
- package/tests/sentiment.test.ts +207 -0
- package/tests/store.test.ts +390 -0
- package/tsconfig.json +13 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/store.ts
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as crypto from 'node:crypto';
|
|
4
|
+
import { getLogger } from '@auxiora/logger';
|
|
5
|
+
import { audit } from '@auxiora/audit';
|
|
6
|
+
import { getMemoryDir } from '@auxiora/core';
|
|
7
|
+
import type { MemoryEntry, MemoryCategory, LivingMemoryState, MemoryPartition } from './types.js';
|
|
8
|
+
|
|
9
|
+
const logger = getLogger('memory:store');
|
|
10
|
+
|
|
11
|
+
export class MemoryStore {
|
|
12
|
+
private filePath: string;
|
|
13
|
+
private maxEntries: number;
|
|
14
|
+
|
|
15
|
+
constructor(options?: { dir?: string; maxEntries?: number }) {
|
|
16
|
+
const dir = options?.dir ?? getMemoryDir();
|
|
17
|
+
this.filePath = path.join(dir, 'memories.json');
|
|
18
|
+
this.maxEntries = options?.maxEntries ?? 1000;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async add(
|
|
22
|
+
content: string,
|
|
23
|
+
category: MemoryCategory,
|
|
24
|
+
source: MemoryEntry['source'],
|
|
25
|
+
extra?: Partial<Pick<MemoryEntry, 'importance' | 'confidence' | 'sentiment' | 'expiresAt' | 'encrypted' | 'relatedMemories' | 'partitionId' | 'sourceUserId'>>,
|
|
26
|
+
): Promise<MemoryEntry> {
|
|
27
|
+
const memories = await this.readFile();
|
|
28
|
+
const tags = this.extractTags(content);
|
|
29
|
+
|
|
30
|
+
// Dedup: check for >50% tag overlap with existing entries
|
|
31
|
+
const existing = this.findOverlap(memories, tags);
|
|
32
|
+
if (existing) {
|
|
33
|
+
existing.content = content;
|
|
34
|
+
existing.updatedAt = Date.now();
|
|
35
|
+
existing.tags = tags;
|
|
36
|
+
if (extra?.importance !== undefined) existing.importance = extra.importance;
|
|
37
|
+
if (extra?.confidence !== undefined) existing.confidence = extra.confidence;
|
|
38
|
+
if (extra?.sentiment !== undefined) existing.sentiment = extra.sentiment;
|
|
39
|
+
await this.writeFile(memories);
|
|
40
|
+
logger.debug('Updated existing memory (dedup)', { id: existing.id });
|
|
41
|
+
return existing;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const entry: MemoryEntry = {
|
|
45
|
+
id: `mem-${crypto.randomUUID().slice(0, 8)}`,
|
|
46
|
+
content,
|
|
47
|
+
category,
|
|
48
|
+
source,
|
|
49
|
+
createdAt: Date.now(),
|
|
50
|
+
updatedAt: Date.now(),
|
|
51
|
+
accessCount: 0,
|
|
52
|
+
tags,
|
|
53
|
+
importance: extra?.importance ?? 0.5,
|
|
54
|
+
confidence: extra?.confidence ?? 0.8,
|
|
55
|
+
sentiment: extra?.sentiment ?? 'neutral',
|
|
56
|
+
encrypted: extra?.encrypted ?? false,
|
|
57
|
+
partitionId: extra?.partitionId ?? 'global',
|
|
58
|
+
...(extra?.sourceUserId !== undefined ? { sourceUserId: extra.sourceUserId } : {}),
|
|
59
|
+
...(extra?.expiresAt !== undefined ? { expiresAt: extra.expiresAt } : {}),
|
|
60
|
+
...(extra?.relatedMemories !== undefined ? { relatedMemories: extra.relatedMemories } : {}),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
memories.push(entry);
|
|
64
|
+
|
|
65
|
+
// Enforce max entries: remove oldest by updatedAt
|
|
66
|
+
if (memories.length > this.maxEntries) {
|
|
67
|
+
memories.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
68
|
+
memories.length = this.maxEntries;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await this.writeFile(memories);
|
|
72
|
+
void audit('memory.saved', { id: entry.id, category, source });
|
|
73
|
+
logger.debug('Saved memory', { id: entry.id, category });
|
|
74
|
+
return entry;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async remove(id: string): Promise<boolean> {
|
|
78
|
+
const memories = await this.readFile();
|
|
79
|
+
const filtered = memories.filter(m => m.id !== id);
|
|
80
|
+
if (filtered.length === memories.length) return false;
|
|
81
|
+
await this.writeFile(filtered);
|
|
82
|
+
void audit('memory.deleted', { id });
|
|
83
|
+
logger.debug('Removed memory', { id });
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async update(id: string, updates: Partial<Pick<MemoryEntry, 'content' | 'category' | 'importance' | 'confidence' | 'sentiment' | 'expiresAt' | 'encrypted'>>): Promise<MemoryEntry | undefined> {
|
|
88
|
+
const memories = await this.readFile();
|
|
89
|
+
const entry = memories.find(m => m.id === id);
|
|
90
|
+
if (!entry) return undefined;
|
|
91
|
+
|
|
92
|
+
if (updates.content !== undefined) {
|
|
93
|
+
entry.content = updates.content;
|
|
94
|
+
entry.tags = this.extractTags(updates.content);
|
|
95
|
+
}
|
|
96
|
+
if (updates.category !== undefined) entry.category = updates.category;
|
|
97
|
+
if (updates.importance !== undefined) entry.importance = updates.importance;
|
|
98
|
+
if (updates.confidence !== undefined) entry.confidence = updates.confidence;
|
|
99
|
+
if (updates.sentiment !== undefined) entry.sentiment = updates.sentiment;
|
|
100
|
+
if (updates.expiresAt !== undefined) entry.expiresAt = updates.expiresAt;
|
|
101
|
+
if (updates.encrypted !== undefined) entry.encrypted = updates.encrypted;
|
|
102
|
+
entry.updatedAt = Date.now();
|
|
103
|
+
|
|
104
|
+
await this.writeFile(memories);
|
|
105
|
+
return entry;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async getAll(): Promise<MemoryEntry[]> {
|
|
109
|
+
return this.readFile();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async search(query: string): Promise<MemoryEntry[]> {
|
|
113
|
+
const memories = await this.readFile();
|
|
114
|
+
const queryTags = this.extractTags(query);
|
|
115
|
+
if (queryTags.length === 0) return memories;
|
|
116
|
+
|
|
117
|
+
// Score by tag overlap
|
|
118
|
+
const scored = memories.map(m => {
|
|
119
|
+
const overlap = m.tags.filter(t => queryTags.includes(t)).length;
|
|
120
|
+
return { entry: m, score: overlap };
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const results = scored
|
|
124
|
+
.filter(s => s.score > 0)
|
|
125
|
+
.sort((a, b) => b.score - a.score)
|
|
126
|
+
.map(s => {
|
|
127
|
+
s.entry.accessCount++;
|
|
128
|
+
return s.entry;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Fix: persist accessCount increments
|
|
132
|
+
if (results.length > 0) {
|
|
133
|
+
await this.writeFile(memories);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return results;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async getByCategory(category: MemoryCategory, partitionId?: string): Promise<MemoryEntry[]> {
|
|
140
|
+
const memories = await this.readFile();
|
|
141
|
+
return memories.filter(m =>
|
|
142
|
+
m.category === category &&
|
|
143
|
+
(partitionId === undefined || (m.partitionId ?? 'global') === partitionId),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async getByPartition(partitionId: string): Promise<MemoryEntry[]> {
|
|
148
|
+
const memories = await this.readFile();
|
|
149
|
+
return memories.filter(m => (m.partitionId ?? 'global') === partitionId);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async getByPartitions(partitionIds: string[]): Promise<MemoryEntry[]> {
|
|
153
|
+
const memories = await this.readFile();
|
|
154
|
+
const idSet = new Set(partitionIds);
|
|
155
|
+
return memories.filter(m => idSet.has(m.partitionId ?? 'global'));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async getExpired(): Promise<MemoryEntry[]> {
|
|
159
|
+
const memories = await this.readFile();
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
return memories.filter(m => m.expiresAt !== undefined && m.expiresAt <= now);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async cleanExpired(): Promise<number> {
|
|
165
|
+
const memories = await this.readFile();
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
const before = memories.length;
|
|
168
|
+
const kept = memories.filter(m => m.expiresAt === undefined || m.expiresAt > now);
|
|
169
|
+
const removed = before - kept.length;
|
|
170
|
+
if (removed > 0) {
|
|
171
|
+
await this.writeFile(kept);
|
|
172
|
+
logger.debug('Cleaned expired memories', { removed });
|
|
173
|
+
}
|
|
174
|
+
return removed;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async merge(id1: string, id2: string, mergedContent: string): Promise<MemoryEntry> {
|
|
178
|
+
const memories = await this.readFile();
|
|
179
|
+
const entry1 = memories.find(m => m.id === id1);
|
|
180
|
+
const entry2 = memories.find(m => m.id === id2);
|
|
181
|
+
if (!entry1) throw new Error(`Memory not found: ${id1}`);
|
|
182
|
+
if (!entry2) throw new Error(`Memory not found: ${id2}`);
|
|
183
|
+
|
|
184
|
+
// Keep the older entry's id, merge fields
|
|
185
|
+
const merged: MemoryEntry = {
|
|
186
|
+
id: entry1.id,
|
|
187
|
+
content: mergedContent,
|
|
188
|
+
category: entry1.category,
|
|
189
|
+
source: entry1.source,
|
|
190
|
+
createdAt: Math.min(entry1.createdAt, entry2.createdAt),
|
|
191
|
+
updatedAt: Date.now(),
|
|
192
|
+
accessCount: entry1.accessCount + entry2.accessCount,
|
|
193
|
+
tags: this.extractTags(mergedContent),
|
|
194
|
+
importance: Math.max(entry1.importance, entry2.importance),
|
|
195
|
+
confidence: Math.max(entry1.confidence, entry2.confidence),
|
|
196
|
+
sentiment: entry1.sentiment,
|
|
197
|
+
encrypted: entry1.encrypted || entry2.encrypted,
|
|
198
|
+
relatedMemories: [
|
|
199
|
+
...new Set([
|
|
200
|
+
...(entry1.relatedMemories ?? []),
|
|
201
|
+
...(entry2.relatedMemories ?? []),
|
|
202
|
+
]),
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Remove both originals and add merged
|
|
207
|
+
const filtered = memories.filter(m => m.id !== id1 && m.id !== id2);
|
|
208
|
+
filtered.push(merged);
|
|
209
|
+
await this.writeFile(filtered);
|
|
210
|
+
|
|
211
|
+
logger.debug('Merged memories', { id1, id2, mergedId: merged.id });
|
|
212
|
+
return merged;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async getStats(): Promise<LivingMemoryState['stats']> {
|
|
216
|
+
const memories = await this.readFile();
|
|
217
|
+
|
|
218
|
+
if (memories.length === 0) {
|
|
219
|
+
return {
|
|
220
|
+
totalMemories: 0,
|
|
221
|
+
oldestMemory: 0,
|
|
222
|
+
newestMemory: 0,
|
|
223
|
+
averageImportance: 0,
|
|
224
|
+
topTags: [],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const tagCounts = new Map<string, number>();
|
|
229
|
+
let totalImportance = 0;
|
|
230
|
+
let oldest = Infinity;
|
|
231
|
+
let newest = 0;
|
|
232
|
+
|
|
233
|
+
for (const m of memories) {
|
|
234
|
+
totalImportance += m.importance;
|
|
235
|
+
if (m.createdAt < oldest) oldest = m.createdAt;
|
|
236
|
+
if (m.createdAt > newest) newest = m.createdAt;
|
|
237
|
+
for (const tag of m.tags) {
|
|
238
|
+
tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const topTags = Array.from(tagCounts.entries())
|
|
243
|
+
.sort((a, b) => b[1] - a[1])
|
|
244
|
+
.slice(0, 10)
|
|
245
|
+
.map(([tag, count]) => ({ tag, count }));
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
totalMemories: memories.length,
|
|
249
|
+
oldestMemory: oldest,
|
|
250
|
+
newestMemory: newest,
|
|
251
|
+
averageImportance: totalImportance / memories.length,
|
|
252
|
+
topTags,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async exportAll(): Promise<{ version: string; memories: MemoryEntry[]; exportedAt: number }> {
|
|
257
|
+
const memories = await this.readFile();
|
|
258
|
+
return {
|
|
259
|
+
version: '1.0',
|
|
260
|
+
memories,
|
|
261
|
+
exportedAt: Date.now(),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async importAll(data: { memories: MemoryEntry[] }): Promise<{ imported: number; skipped: number }> {
|
|
266
|
+
const existing = await this.readFile();
|
|
267
|
+
const existingIds = new Set(existing.map(m => m.id));
|
|
268
|
+
let imported = 0;
|
|
269
|
+
let skipped = 0;
|
|
270
|
+
|
|
271
|
+
for (const entry of data.memories) {
|
|
272
|
+
if (existingIds.has(entry.id)) {
|
|
273
|
+
skipped++;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
// Ensure new fields have defaults for legacy imports
|
|
277
|
+
existing.push(this.applyDefaults(entry));
|
|
278
|
+
existingIds.add(entry.id);
|
|
279
|
+
imported++;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Enforce max entries
|
|
283
|
+
if (existing.length > this.maxEntries) {
|
|
284
|
+
existing.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
285
|
+
existing.length = this.maxEntries;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
await this.writeFile(existing);
|
|
289
|
+
logger.debug('Imported memories', { imported, skipped });
|
|
290
|
+
return { imported, skipped };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async setImportance(id: string, importance: number): Promise<void> {
|
|
294
|
+
if (importance < 0 || importance > 1) {
|
|
295
|
+
throw new Error('Importance must be between 0 and 1');
|
|
296
|
+
}
|
|
297
|
+
const result = await this.update(id, { importance });
|
|
298
|
+
if (!result) throw new Error(`Memory not found: ${id}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async linkMemories(id1: string, id2: string): Promise<void> {
|
|
302
|
+
const memories = await this.readFile();
|
|
303
|
+
const entry1 = memories.find(m => m.id === id1);
|
|
304
|
+
const entry2 = memories.find(m => m.id === id2);
|
|
305
|
+
if (!entry1) throw new Error(`Memory not found: ${id1}`);
|
|
306
|
+
if (!entry2) throw new Error(`Memory not found: ${id2}`);
|
|
307
|
+
|
|
308
|
+
if (!entry1.relatedMemories) entry1.relatedMemories = [];
|
|
309
|
+
if (!entry2.relatedMemories) entry2.relatedMemories = [];
|
|
310
|
+
|
|
311
|
+
if (!entry1.relatedMemories.includes(id2)) entry1.relatedMemories.push(id2);
|
|
312
|
+
if (!entry2.relatedMemories.includes(id1)) entry2.relatedMemories.push(id1);
|
|
313
|
+
|
|
314
|
+
await this.writeFile(memories);
|
|
315
|
+
logger.debug('Linked memories', { id1, id2 });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
extractTags(text: string): string[] {
|
|
319
|
+
const stopWords = new Set([
|
|
320
|
+
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
321
|
+
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
322
|
+
'should', 'may', 'might', 'can', 'shall', 'to', 'of', 'in', 'for',
|
|
323
|
+
'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during',
|
|
324
|
+
'before', 'after', 'above', 'below', 'between', 'and', 'but', 'or',
|
|
325
|
+
'nor', 'not', 'so', 'yet', 'both', 'either', 'neither', 'each',
|
|
326
|
+
'every', 'all', 'any', 'few', 'more', 'most', 'other', 'some',
|
|
327
|
+
'such', 'no', 'only', 'own', 'same', 'than', 'too', 'very',
|
|
328
|
+
'just', 'about', 'also', 'that', 'this', 'it', 'its', 'i', 'my',
|
|
329
|
+
'me', 'we', 'our', 'you', 'your', 'he', 'she', 'they', 'them',
|
|
330
|
+
'their', 'what', 'which', 'who', 'when', 'where', 'how', 'like',
|
|
331
|
+
'user', 'prefers', 'uses', 'wants', 'likes',
|
|
332
|
+
]);
|
|
333
|
+
|
|
334
|
+
return text
|
|
335
|
+
.toLowerCase()
|
|
336
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
337
|
+
.split(/\s+/)
|
|
338
|
+
.filter(w => w.length > 2 && !stopWords.has(w))
|
|
339
|
+
.filter((w, i, arr) => arr.indexOf(w) === i); // unique
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private findOverlap(memories: MemoryEntry[], tags: string[]): MemoryEntry | undefined {
|
|
343
|
+
if (tags.length === 0) return undefined;
|
|
344
|
+
|
|
345
|
+
for (const m of memories) {
|
|
346
|
+
const overlap = m.tags.filter(t => tags.includes(t)).length;
|
|
347
|
+
const ratio = overlap / Math.max(m.tags.length, tags.length);
|
|
348
|
+
if (ratio > 0.5) return m;
|
|
349
|
+
}
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Apply defaults for new fields when loading legacy entries */
|
|
354
|
+
private applyDefaults(entry: MemoryEntry): MemoryEntry {
|
|
355
|
+
return {
|
|
356
|
+
...entry,
|
|
357
|
+
importance: entry.importance ?? 0.5,
|
|
358
|
+
confidence: entry.confidence ?? 0.8,
|
|
359
|
+
sentiment: entry.sentiment ?? 'neutral',
|
|
360
|
+
encrypted: entry.encrypted ?? false,
|
|
361
|
+
partitionId: entry.partitionId ?? 'global',
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private async readFile(): Promise<MemoryEntry[]> {
|
|
366
|
+
try {
|
|
367
|
+
const content = await fs.readFile(this.filePath, 'utf-8');
|
|
368
|
+
const raw = JSON.parse(content) as MemoryEntry[];
|
|
369
|
+
// Apply defaults for legacy entries that lack new fields
|
|
370
|
+
return raw.map(e => this.applyDefaults(e));
|
|
371
|
+
} catch (error) {
|
|
372
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private async writeFile(memories: MemoryEntry[]): Promise<void> {
|
|
380
|
+
const dir = path.dirname(this.filePath);
|
|
381
|
+
await fs.mkdir(dir, { recursive: true });
|
|
382
|
+
await fs.writeFile(this.filePath, JSON.stringify(memories, null, 2), 'utf-8');
|
|
383
|
+
}
|
|
384
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export type MemoryCategory =
|
|
2
|
+
| 'preference' // user preferences
|
|
3
|
+
| 'fact' // factual knowledge about user
|
|
4
|
+
| 'context' // contextual information
|
|
5
|
+
| 'relationship' // shared history, inside jokes
|
|
6
|
+
| 'pattern' // observed communication patterns
|
|
7
|
+
| 'personality'; // personality adaptation signals
|
|
8
|
+
|
|
9
|
+
export type MemorySource = 'extracted' | 'explicit' | 'observed';
|
|
10
|
+
|
|
11
|
+
export interface MemoryEntry {
|
|
12
|
+
id: string;
|
|
13
|
+
content: string;
|
|
14
|
+
category: MemoryCategory;
|
|
15
|
+
source: MemorySource;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
updatedAt: number;
|
|
18
|
+
accessCount: number;
|
|
19
|
+
tags: string[];
|
|
20
|
+
importance: number;
|
|
21
|
+
confidence: number;
|
|
22
|
+
expiresAt?: number;
|
|
23
|
+
relatedMemories?: string[];
|
|
24
|
+
sentiment?: 'positive' | 'negative' | 'neutral';
|
|
25
|
+
encrypted?: boolean;
|
|
26
|
+
/** Partition this memory belongs to. Defaults to 'global'. */
|
|
27
|
+
partitionId?: string;
|
|
28
|
+
/** User ID that created this memory. */
|
|
29
|
+
sourceUserId?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A memory partition for per-user or shared storage. */
|
|
33
|
+
export interface MemoryPartition {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
type: 'private' | 'shared' | 'global';
|
|
37
|
+
ownerId?: string;
|
|
38
|
+
memberIds?: string[];
|
|
39
|
+
createdAt: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RelationshipMemory {
|
|
43
|
+
type: 'inside_joke' | 'shared_experience' | 'milestone' | 'callback';
|
|
44
|
+
originalContext?: string;
|
|
45
|
+
useCount: number;
|
|
46
|
+
lastUsed: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PatternMemory {
|
|
50
|
+
type: 'communication' | 'schedule' | 'topic' | 'mood';
|
|
51
|
+
pattern: string;
|
|
52
|
+
observations: number;
|
|
53
|
+
confidence: number;
|
|
54
|
+
examples?: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface PersonalityAdaptation {
|
|
58
|
+
trait: string;
|
|
59
|
+
adjustment: number;
|
|
60
|
+
reason: string;
|
|
61
|
+
signalCount: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type SentimentLabel = 'positive' | 'negative' | 'neutral';
|
|
65
|
+
|
|
66
|
+
export interface SentimentResult {
|
|
67
|
+
sentiment: SentimentLabel;
|
|
68
|
+
confidence: number;
|
|
69
|
+
keywords: string[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface SentimentSnapshot {
|
|
73
|
+
sentiment: SentimentLabel;
|
|
74
|
+
confidence: number;
|
|
75
|
+
timestamp: number;
|
|
76
|
+
hour: number;
|
|
77
|
+
dayOfWeek: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface LivingMemoryState {
|
|
81
|
+
facts: MemoryEntry[];
|
|
82
|
+
relationships: MemoryEntry[];
|
|
83
|
+
patterns: MemoryEntry[];
|
|
84
|
+
adaptations: PersonalityAdaptation[];
|
|
85
|
+
stats: {
|
|
86
|
+
totalMemories: number;
|
|
87
|
+
oldestMemory: number;
|
|
88
|
+
newestMemory: number;
|
|
89
|
+
averageImportance: number;
|
|
90
|
+
topTags: Array<{ tag: string; count: number }>;
|
|
91
|
+
};
|
|
92
|
+
}
|