kiku-agent 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/README.md +122 -0
- package/package.json +85 -0
- package/server/agent/agentCore.js +782 -0
- package/server/agent/agentState.js +140 -0
- package/server/agent/audioPlayer.js +673 -0
- package/server/agent/cli.js +1111 -0
- package/server/agent/commandRegistry.js +58 -0
- package/server/agent/commands.js +788 -0
- package/server/agent/config.js +90 -0
- package/server/agent/errors.js +9 -0
- package/server/agent/eventBus.js +103 -0
- package/server/agent/llmClient.js +200 -0
- package/server/agent/memory.js +566 -0
- package/server/agent/sanitizer.js +314 -0
- package/server/agent/session.js +239 -0
- package/server/agent/theme.js +124 -0
- package/server/agent/tools.js +1528 -0
- package/server/agent/ui.js +469 -0
- package/server/agent/webSearch.js +719 -0
- package/server/agent/youtubeAuth.js +299 -0
- package/server/mcp/claude-desktop-config.example.json +9 -0
- package/server/mcp/data/awards.js +14 -0
- package/server/mcp/data/brands.js +9 -0
- package/server/mcp/data/discography.js +36 -0
- package/server/mcp/data/gallery.js +25 -0
- package/server/mcp/data/index.js +7 -0
- package/server/mcp/data/profile.js +44 -0
- package/server/mcp/data/timeline.js +16 -0
- package/server/mcp/data/works.js +104 -0
- package/server/mcp/handler.js +159 -0
- package/server/mcp/prompts.js +143 -0
- package/server/mcp/resources.js +54 -0
- package/server/mcp/security.js +138 -0
- package/server/mcp/server.js +100 -0
- package/server/mcp/stdio.js +33 -0
- package/server/mcp/tools.js +126 -0
- package/src/data/awardsData.js +146 -0
- package/src/data/brandsData.js +249 -0
- package/src/data/discographyData.js +874 -0
- package/src/data/galleryData.js +666 -0
- package/src/data/musicVideoData.js +406 -0
- package/src/data/profileData.js +156 -0
- package/src/data/socialUpdatesData.js +381 -0
- package/src/data/timelineData.js +172 -0
- package/src/data/worksData.js +516 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const MEMORY_DIR = path.resolve(process.cwd(), '.kiku_sessions');
|
|
5
|
+
const MEMORY_FILE = path.join(MEMORY_DIR, 'long_term_memory.json');
|
|
6
|
+
const SESSIONS_DIR = path.join(MEMORY_DIR, 'sessions');
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export class AgentMemory {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.facts = [];
|
|
12
|
+
this.preferences = {
|
|
13
|
+
language: null,
|
|
14
|
+
topicsOfInterest: [],
|
|
15
|
+
responseStyle: 'default',
|
|
16
|
+
};
|
|
17
|
+
this.sessions = [];
|
|
18
|
+
this.summaries = [];
|
|
19
|
+
this.stats = {
|
|
20
|
+
totalFacts: 0,
|
|
21
|
+
totalSessions: 0,
|
|
22
|
+
totalConsolidations: 0,
|
|
23
|
+
lastConsolidationAt: null,
|
|
24
|
+
createdAt: new Date()
|
|
25
|
+
};
|
|
26
|
+
this._loaded = false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async init() {
|
|
31
|
+
try {
|
|
32
|
+
await fs.mkdir(MEMORY_DIR, { recursive: true });
|
|
33
|
+
await fs.mkdir(SESSIONS_DIR, { recursive: true });
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async load() {
|
|
39
|
+
try {
|
|
40
|
+
await this.init();
|
|
41
|
+
const data = await fs.readFile(MEMORY_FILE, 'utf-8');
|
|
42
|
+
const parsed = JSON.parse(data);
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
this.facts = (parsed.facts || []).map(f => ({
|
|
46
|
+
...f,
|
|
47
|
+
createdAt: f.createdAt ? new Date(f.createdAt) : new Date(),
|
|
48
|
+
lastAccessedAt: f.lastAccessedAt ? new Date(f.lastAccessedAt) : new Date()
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
const loadedPrefs = parsed.preferences || {};
|
|
52
|
+
this.preferences = {
|
|
53
|
+
...this.preferences,
|
|
54
|
+
...loadedPrefs,
|
|
55
|
+
topicsOfInterest: (loadedPrefs.topicsOfInterest || []).map(t => ({
|
|
56
|
+
...t,
|
|
57
|
+
lastSeen: t.lastSeen ? new Date(t.lastSeen) : new Date()
|
|
58
|
+
}))
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
this.sessions = parsed.sessions || [];
|
|
62
|
+
this.summaries = parsed.summaries || [];
|
|
63
|
+
this.stats = { ...this.stats, ...(parsed.stats || {}) };
|
|
64
|
+
this._loaded = true;
|
|
65
|
+
return true;
|
|
66
|
+
} catch {
|
|
67
|
+
this._loaded = true;
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async save() {
|
|
74
|
+
try {
|
|
75
|
+
await this.init();
|
|
76
|
+
|
|
77
|
+
const serializablePrefs = {
|
|
78
|
+
...this.preferences,
|
|
79
|
+
topicsOfInterest: (this.preferences.topicsOfInterest || []).map(t => ({
|
|
80
|
+
...t,
|
|
81
|
+
lastSeen: t.lastSeen ? t.lastSeen.toISOString() : null
|
|
82
|
+
}))
|
|
83
|
+
};
|
|
84
|
+
const data = {
|
|
85
|
+
facts: this.facts.map(f => ({
|
|
86
|
+
...f,
|
|
87
|
+
createdAt: f.createdAt ? f.createdAt.toISOString() : f.createdAt,
|
|
88
|
+
lastAccessedAt: f.lastAccessedAt ? f.lastAccessedAt.toISOString() : f.lastAccessedAt
|
|
89
|
+
})),
|
|
90
|
+
preferences: serializablePrefs,
|
|
91
|
+
sessions: this.sessions,
|
|
92
|
+
summaries: this.summaries,
|
|
93
|
+
stats: this.stats,
|
|
94
|
+
lastSavedAt: new Date().toISOString()
|
|
95
|
+
};
|
|
96
|
+
await fs.writeFile(MEMORY_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
97
|
+
return true;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('Failed to save long-term memory:', error);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
addFact(fact, meta = {}) {
|
|
105
|
+
const entry = {
|
|
106
|
+
id: `fact_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
107
|
+
content: fact,
|
|
108
|
+
category: meta.category || 'general',
|
|
109
|
+
source: meta.source || 'conversation',
|
|
110
|
+
confidence: meta.confidence || 'high',
|
|
111
|
+
createdAt: new Date(),
|
|
112
|
+
lastAccessedAt: new Date(),
|
|
113
|
+
accessCount: 0,
|
|
114
|
+
tags: meta.tags || []
|
|
115
|
+
};
|
|
116
|
+
this.facts.push(entry);
|
|
117
|
+
this.stats.totalFacts = this.facts.length;
|
|
118
|
+
this.save().catch(() => {});
|
|
119
|
+
return entry;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
searchFacts(query, options = {}) {
|
|
124
|
+
const { category, limit = 10 } = options;
|
|
125
|
+
const q = (query || '').toLowerCase().trim();
|
|
126
|
+
|
|
127
|
+
let results = this.facts;
|
|
128
|
+
|
|
129
|
+
if (category) {
|
|
130
|
+
results = results.filter(f => f.category === category);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (q) {
|
|
134
|
+
results = results.filter(f =>
|
|
135
|
+
f.content.toLowerCase().includes(q) ||
|
|
136
|
+
f.tags.some(t => t.toLowerCase().includes(q))
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
results.sort((a, b) => {
|
|
142
|
+
const scoreA = (a.accessCount || 0) + (new Date(a.createdAt).getTime() / 1e12);
|
|
143
|
+
const scoreB = (b.accessCount || 0) + (new Date(b.createdAt).getTime() / 1e12);
|
|
144
|
+
return scoreB - scoreA;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
for (const fact of results.slice(0, limit)) {
|
|
149
|
+
fact.lastAccessedAt = new Date();
|
|
150
|
+
fact.accessCount = (fact.accessCount || 0) + 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return results.slice(0, limit);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
removeFact(factId) {
|
|
157
|
+
const idx = this.facts.findIndex(f => f.id === factId);
|
|
158
|
+
if (idx === -1) return false;
|
|
159
|
+
this.facts.splice(idx, 1);
|
|
160
|
+
this.stats.totalFacts = this.facts.length;
|
|
161
|
+
this.save().catch(() => {});
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
getFactsByCategory() {
|
|
167
|
+
const grouped = {};
|
|
168
|
+
for (const fact of this.facts) {
|
|
169
|
+
const cat = fact.category || 'general';
|
|
170
|
+
if (!grouped[cat]) grouped[cat] = [];
|
|
171
|
+
grouped[cat].push(fact);
|
|
172
|
+
}
|
|
173
|
+
return grouped;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setPreference(key, value) {
|
|
177
|
+
this.preferences[key] = value;
|
|
178
|
+
this.save().catch(() => {});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
async savePreferences() {
|
|
183
|
+
await this.save();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
trackTopicInterest(topic) {
|
|
188
|
+
const existing = this.preferences.topicsOfInterest.find(
|
|
189
|
+
t => t.topic.toLowerCase() === topic.toLowerCase()
|
|
190
|
+
);
|
|
191
|
+
if (existing) {
|
|
192
|
+
existing.count++;
|
|
193
|
+
existing.lastSeen = new Date();
|
|
194
|
+
} else {
|
|
195
|
+
this.preferences.topicsOfInterest.push({
|
|
196
|
+
topic,
|
|
197
|
+
count: 1,
|
|
198
|
+
lastSeen: new Date()
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.preferences.topicsOfInterest.sort((a, b) => b.count - a.count);
|
|
203
|
+
this.preferences.topicsOfInterest = this.preferences.topicsOfInterest.slice(0, 20);
|
|
204
|
+
this.save().catch(() => {});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
getTopTopics(limit = 5) {
|
|
209
|
+
return this.preferences.topicsOfInterest.slice(0, limit);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
async archiveSession(sessionData) {
|
|
214
|
+
const archive = {
|
|
215
|
+
id: sessionData.id,
|
|
216
|
+
createdAt: sessionData.createdAt,
|
|
217
|
+
endedAt: new Date(),
|
|
218
|
+
messageCount: sessionData.messages?.length || 0,
|
|
219
|
+
stats: { ...sessionData.stats },
|
|
220
|
+
thinkingMode: sessionData.thinkingMode,
|
|
221
|
+
summary: null
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const sessionFile = path.join(SESSIONS_DIR, `${sessionData.id}.json`);
|
|
227
|
+
await fs.writeFile(sessionFile, JSON.stringify(sessionData, null, 2), 'utf-8');
|
|
228
|
+
} catch (err) {
|
|
229
|
+
console.error('Failed to archive session file:', err);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
this.sessions.push(archive);
|
|
233
|
+
this.stats.totalSessions = this.sessions.length;
|
|
234
|
+
await this.save();
|
|
235
|
+
return archive;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
getArchivedSessions(limit = 20) {
|
|
239
|
+
return this.sessions
|
|
240
|
+
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
|
241
|
+
.slice(0, limit);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async loadArchivedSession(sessionId) {
|
|
245
|
+
try {
|
|
246
|
+
const sessionFile = path.join(SESSIONS_DIR, `${sessionId}.json`);
|
|
247
|
+
const data = await fs.readFile(sessionFile, 'utf-8');
|
|
248
|
+
return JSON.parse(data);
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
async consolidateSession(sessionData) {
|
|
256
|
+
if (!sessionData?.messages?.length) return null;
|
|
257
|
+
|
|
258
|
+
const userMessages = sessionData.messages
|
|
259
|
+
.filter(m => m.role === 'user')
|
|
260
|
+
.map(m => {
|
|
261
|
+
const textPart = m.parts?.find(p => p.type === 'text');
|
|
262
|
+
return textPart?.content || '';
|
|
263
|
+
})
|
|
264
|
+
.filter(Boolean);
|
|
265
|
+
|
|
266
|
+
const assistantMessages = sessionData.messages
|
|
267
|
+
.filter(m => m.role === 'assistant')
|
|
268
|
+
.map(m => {
|
|
269
|
+
const textPart = m.parts?.find(p => p.type === 'text');
|
|
270
|
+
return textPart?.content || '';
|
|
271
|
+
})
|
|
272
|
+
.filter(Boolean);
|
|
273
|
+
|
|
274
|
+
const topics = this._extractTopics(userMessages);
|
|
275
|
+
|
|
276
|
+
for (const topic of topics) {
|
|
277
|
+
this.trackTopicInterest(topic);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const summary = {
|
|
281
|
+
id: `sum_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
282
|
+
sessionId: sessionData.id,
|
|
283
|
+
createdAt: new Date(),
|
|
284
|
+
topics,
|
|
285
|
+
questionCount: userMessages.length,
|
|
286
|
+
keyExchanges: userMessages.slice(0, 5).map((q, i) => ({
|
|
287
|
+
question: q.slice(0, 200),
|
|
288
|
+
answerPreview: (assistantMessages[i] || '').slice(0, 200)
|
|
289
|
+
})),
|
|
290
|
+
durationMs: sessionData.stats?.totalDurationMs || 0,
|
|
291
|
+
toolCalls: sessionData.stats?.totalToolCalls || 0
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
this.summaries.push(summary);
|
|
295
|
+
this.stats.totalConsolidations = this.summaries.length;
|
|
296
|
+
this.stats.lastConsolidationAt = new Date();
|
|
297
|
+
|
|
298
|
+
const archived = this.sessions.find(s => s.id === sessionData.id);
|
|
299
|
+
if (archived) {
|
|
300
|
+
archived.summary = summary;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await this.save();
|
|
304
|
+
return summary;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
_extractTopics(messages) {
|
|
308
|
+
const topicKeywords = {
|
|
309
|
+
'drama': ['drama', 'series', 'ละคร', '剧', '电视', 'tv series', 'film', 'movie', 'work'],
|
|
310
|
+
'music': ['song', 'music', 'mv', 'single', 'ost', 'เพลง', '歌', 'album', 'discography'],
|
|
311
|
+
'brand': ['brand', 'ambassador', 'endorsement', 'แบรนด์', '品牌', 'commercial'],
|
|
312
|
+
'awards': ['award', 'honor', 'ceremony', 'รางวัล', '奖', 'winner'],
|
|
313
|
+
'profile': ['profile', 'bio', 'birthday', 'age', 'ประวัติ', '资料', '个人信息'],
|
|
314
|
+
'timeline': ['career', 'timeline', 'history', 'era', 'เส้นทาง', '经历', 'ปี'],
|
|
315
|
+
'news': ['news', 'latest', 'recent', 'update', 'ข่าว', '最新', 'ล่าสุด'],
|
|
316
|
+
'video': ['video', 'stage', 'performance', 'live', 'คลิป', '视频', '舞台'],
|
|
317
|
+
'social': ['weibo', 'douyin', 'instagram', 'social media', 'โซเชียล', '微博'],
|
|
318
|
+
'gallery': ['photo', 'picture', 'gallery', 'fashion', 'photoshoot', 'ภาพ', '照片']
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const topicCounts = {};
|
|
322
|
+
const allText = messages.join(' ').toLowerCase();
|
|
323
|
+
|
|
324
|
+
for (const [topic, keywords] of Object.entries(topicKeywords)) {
|
|
325
|
+
const count = keywords.reduce((acc, kw) => {
|
|
326
|
+
const regex = new RegExp(`\\b${kw}\\b`, 'gi');
|
|
327
|
+
return acc + (allText.match(regex) || []).length;
|
|
328
|
+
}, 0);
|
|
329
|
+
if (count > 0) topicCounts[topic] = count;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return Object.entries(topicCounts)
|
|
333
|
+
.sort(([, a], [, b]) => b - a)
|
|
334
|
+
.slice(0, 5)
|
|
335
|
+
.map(([topic]) => topic);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
buildMemoryContext() {
|
|
340
|
+
const parts = [];
|
|
341
|
+
|
|
342
|
+
if (this.preferences.language) {
|
|
343
|
+
parts.push(`User's preferred language: ${this.preferences.language}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const topTopics = this.getTopTopics(5);
|
|
347
|
+
if (topTopics.length > 0) {
|
|
348
|
+
const topicList = topTopics.map(t => `${t.topic} (${t.count}x)`).join(', ');
|
|
349
|
+
parts.push(`User frequently asks about: ${topicList}`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const recentFacts = this.facts
|
|
353
|
+
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
|
354
|
+
.slice(0, 8);
|
|
355
|
+
if (recentFacts.length > 0) {
|
|
356
|
+
const factList = recentFacts.map(f => `- ${f.content}`).join('\n');
|
|
357
|
+
parts.push(`Known facts from previous conversations:\n${factList}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const recentSummaries = this.summaries
|
|
361
|
+
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
|
362
|
+
.slice(0, 3);
|
|
363
|
+
if (recentSummaries.length > 0) {
|
|
364
|
+
const sumList = recentSummaries.map(s => {
|
|
365
|
+
const date = new Date(s.createdAt).toLocaleDateString();
|
|
366
|
+
const topics = s.topics.join(', ');
|
|
367
|
+
return `- [${date}] Topics: ${topics} (${s.questionCount} questions)`;
|
|
368
|
+
}).join('\n');
|
|
369
|
+
parts.push(`Recent conversation themes:\n${sumList}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (parts.length === 0) return '';
|
|
373
|
+
|
|
374
|
+
return `\n\n=== LONG-TERM MEMORY CONTEXT ===\n${parts.join('\n\n')}\n=== END MEMORY CONTEXT ===`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
getStatus() {
|
|
378
|
+
return {
|
|
379
|
+
factsCount: this.facts.length,
|
|
380
|
+
factsByCategory: this.getFactsByCategory(),
|
|
381
|
+
preferences: this.preferences,
|
|
382
|
+
sessionsCount: this.sessions.length,
|
|
383
|
+
summariesCount: this.summaries.length,
|
|
384
|
+
topTopics: this.getTopTopics(10),
|
|
385
|
+
stats: this.stats,
|
|
386
|
+
lastConsolidationAt: this.stats.lastConsolidationAt,
|
|
387
|
+
memoryFileSize: null
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async importFromJSON(filePath) {
|
|
392
|
+
try {
|
|
393
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
394
|
+
const data = JSON.parse(raw);
|
|
395
|
+
|
|
396
|
+
if (!data.facts && !data.preferences && !data.sessions) {
|
|
397
|
+
return { success: false, message: 'Invalid memory file format — missing facts, preferences, or sessions.' };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
let factsImported = 0;
|
|
401
|
+
let factsSkipped = 0;
|
|
402
|
+
let topicsImported = 0;
|
|
403
|
+
|
|
404
|
+
if (Array.isArray(data.facts)) {
|
|
405
|
+
for (const fact of data.facts) {
|
|
406
|
+
const isDuplicate = this.facts.some(
|
|
407
|
+
existing => existing.content === fact.content
|
|
408
|
+
);
|
|
409
|
+
if (!isDuplicate && fact.content) {
|
|
410
|
+
this.facts.push({
|
|
411
|
+
id: fact.id || `fact_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
412
|
+
content: fact.content,
|
|
413
|
+
category: fact.category || 'imported',
|
|
414
|
+
source: fact.source || 'import',
|
|
415
|
+
confidence: fact.confidence || 'high',
|
|
416
|
+
createdAt: fact.createdAt ? new Date(fact.createdAt) : new Date(),
|
|
417
|
+
lastAccessedAt: new Date(),
|
|
418
|
+
accessCount: 0,
|
|
419
|
+
tags: fact.tags || []
|
|
420
|
+
});
|
|
421
|
+
factsImported++;
|
|
422
|
+
} else {
|
|
423
|
+
factsSkipped++;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (data.preferences) {
|
|
429
|
+
if (data.preferences.language) {
|
|
430
|
+
this.preferences.language = data.preferences.language;
|
|
431
|
+
}
|
|
432
|
+
if (data.preferences.responseStyle) {
|
|
433
|
+
this.preferences.responseStyle = data.preferences.responseStyle;
|
|
434
|
+
}
|
|
435
|
+
if (Array.isArray(data.preferences.topicsOfInterest)) {
|
|
436
|
+
for (const topic of data.preferences.topicsOfInterest) {
|
|
437
|
+
const existing = this.preferences.topicsOfInterest.find(
|
|
438
|
+
t => t.topic.toLowerCase() === topic.topic.toLowerCase()
|
|
439
|
+
);
|
|
440
|
+
if (existing) {
|
|
441
|
+
existing.count += topic.count || 1;
|
|
442
|
+
existing.lastSeen = new Date();
|
|
443
|
+
} else {
|
|
444
|
+
this.preferences.topicsOfInterest.push({
|
|
445
|
+
topic: topic.topic,
|
|
446
|
+
count: topic.count || 1,
|
|
447
|
+
lastSeen: topic.lastSeen ? new Date(topic.lastSeen) : new Date()
|
|
448
|
+
});
|
|
449
|
+
topicsImported++;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
this.preferences.topicsOfInterest.sort((a, b) => b.count - a.count);
|
|
453
|
+
this.preferences.topicsOfInterest = this.preferences.topicsOfInterest.slice(0, 20);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (Array.isArray(data.sessions)) {
|
|
458
|
+
for (const session of data.sessions) {
|
|
459
|
+
const isDuplicate = this.sessions.some(s => s.id === session.id);
|
|
460
|
+
if (!isDuplicate) {
|
|
461
|
+
this.sessions.push(session);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (Array.isArray(data.summaries)) {
|
|
467
|
+
for (const summary of data.summaries) {
|
|
468
|
+
const isDuplicate = this.summaries.some(s => s.id === summary.id);
|
|
469
|
+
if (!isDuplicate) {
|
|
470
|
+
this.summaries.push(summary);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
this.stats.totalFacts = this.facts.length;
|
|
476
|
+
this.stats.totalSessions = this.sessions.length;
|
|
477
|
+
this.stats.totalConsolidations = this.summaries.length;
|
|
478
|
+
|
|
479
|
+
await this.save();
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
success: true,
|
|
483
|
+
factsImported,
|
|
484
|
+
factsSkipped,
|
|
485
|
+
topicsImported,
|
|
486
|
+
totalFacts: this.facts.length,
|
|
487
|
+
totalSessions: this.sessions.length,
|
|
488
|
+
totalSummaries: this.summaries.length
|
|
489
|
+
};
|
|
490
|
+
} catch (error) {
|
|
491
|
+
return { success: false, message: error.message };
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async exportToJSON() {
|
|
496
|
+
const exportDir = path.resolve(process.cwd(), 'exports');
|
|
497
|
+
try {
|
|
498
|
+
await fs.mkdir(exportDir, { recursive: true });
|
|
499
|
+
} catch {}
|
|
500
|
+
|
|
501
|
+
const now = new Date();
|
|
502
|
+
const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
503
|
+
const filename = `kiku-memory-${timestamp}.json`;
|
|
504
|
+
const filePath = path.join(exportDir, filename);
|
|
505
|
+
|
|
506
|
+
const exportData = {
|
|
507
|
+
exportedAt: now.toISOString(),
|
|
508
|
+
stats: this.stats,
|
|
509
|
+
preferences: {
|
|
510
|
+
...this.preferences,
|
|
511
|
+
topicsOfInterest: (this.preferences.topicsOfInterest || []).map(t => ({
|
|
512
|
+
...t,
|
|
513
|
+
lastSeen: t.lastSeen ? t.lastSeen.toISOString() : null
|
|
514
|
+
}))
|
|
515
|
+
},
|
|
516
|
+
facts: this.facts.map(f => ({
|
|
517
|
+
...f,
|
|
518
|
+
createdAt: f.createdAt ? f.createdAt.toISOString() : f.createdAt,
|
|
519
|
+
lastAccessedAt: f.lastAccessedAt ? f.lastAccessedAt.toISOString() : f.lastAccessedAt
|
|
520
|
+
})),
|
|
521
|
+
factsByCategory: this.getFactsByCategory(),
|
|
522
|
+
sessions: this.sessions,
|
|
523
|
+
summaries: this.summaries
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
await fs.writeFile(filePath, JSON.stringify(exportData, null, 2), 'utf-8');
|
|
527
|
+
return { filePath, filename, size: (await fs.stat(filePath)).size };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async clearAll() {
|
|
531
|
+
this.facts = [];
|
|
532
|
+
this.preferences = {
|
|
533
|
+
language: null,
|
|
534
|
+
topicsOfInterest: [],
|
|
535
|
+
responseStyle: 'default'
|
|
536
|
+
};
|
|
537
|
+
this.sessions = [];
|
|
538
|
+
this.summaries = [];
|
|
539
|
+
this.stats = {
|
|
540
|
+
totalFacts: 0,
|
|
541
|
+
totalSessions: 0,
|
|
542
|
+
totalConsolidations: 0,
|
|
543
|
+
lastConsolidationAt: null,
|
|
544
|
+
createdAt: new Date()
|
|
545
|
+
};
|
|
546
|
+
await this.save();
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async getStorageSize() {
|
|
551
|
+
try {
|
|
552
|
+
const stat = await fs.stat(MEMORY_FILE);
|
|
553
|
+
return stat.size;
|
|
554
|
+
} catch {
|
|
555
|
+
return 0;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
static formatBytes(bytes) {
|
|
560
|
+
if (bytes === 0) return '0 B';
|
|
561
|
+
const k = 1024;
|
|
562
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
563
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
564
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
565
|
+
}
|
|
566
|
+
}
|