natureco-cli 2.23.29 → 2.23.30
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 +94 -11
- package/bin/natureco.js +402 -4
- package/package.json +1 -1
- package/src/commands/admin-rpc.js +219 -0
- package/src/commands/agent.js +89 -0
- package/src/commands/approvals.js +53 -0
- package/src/commands/backup.js +124 -0
- package/src/commands/bonjour.js +167 -0
- package/src/commands/capability.js +64 -0
- package/src/commands/clickclack.js +130 -0
- package/src/commands/commitments.js +32 -0
- package/src/commands/completion.js +76 -0
- package/src/commands/configure.js +93 -0
- package/src/commands/crestodian.js +92 -0
- package/src/commands/daemon.js +60 -0
- package/src/commands/device-pair.js +248 -0
- package/src/commands/devices.js +110 -0
- package/src/commands/directory.js +47 -0
- package/src/commands/dns.js +58 -0
- package/src/commands/docs.js +43 -0
- package/src/commands/exec-policy.js +71 -0
- package/src/commands/gateway-server.js +1155 -24
- package/src/commands/health.js +18 -0
- package/src/commands/imessage.js +128 -14
- package/src/commands/infer.js +73 -0
- package/src/commands/irc.js +64 -15
- package/src/commands/mattermost.js +114 -12
- package/src/commands/memory-cmd.js +134 -1
- package/src/commands/message.js +9 -3
- package/src/commands/migrate.js +213 -2
- package/src/commands/node.js +98 -0
- package/src/commands/nodes.js +106 -0
- package/src/commands/oc-path.js +200 -0
- package/src/commands/onboard.js +70 -0
- package/src/commands/open-prose.js +67 -0
- package/src/commands/policy.js +176 -0
- package/src/commands/proxy.js +155 -0
- package/src/commands/qr.js +28 -0
- package/src/commands/sandbox.js +125 -0
- package/src/commands/secrets.js +118 -0
- package/src/commands/setup.js +113 -7
- package/src/commands/signal.js +447 -18
- package/src/commands/sms.js +123 -19
- package/src/commands/system.js +53 -0
- package/src/commands/terminal.js +21 -0
- package/src/commands/thread-ownership.js +157 -0
- package/src/commands/transcripts.js +72 -0
- package/src/commands/voice.js +82 -0
- package/src/commands/vydra.js +98 -0
- package/src/commands/workboard.js +207 -0
- package/src/tools/audio_understanding.js +154 -0
- package/src/tools/browser.js +112 -0
- package/src/tools/canvas.js +104 -0
- package/src/tools/document_extract.js +84 -0
- package/src/tools/duckduckgo.js +54 -0
- package/src/tools/exa_search.js +66 -0
- package/src/tools/firecrawl.js +104 -0
- package/src/tools/image_generation.js +99 -0
- package/src/tools/llm_task.js +118 -0
- package/src/tools/media_understanding.js +128 -0
- package/src/tools/music_generation.js +113 -0
- package/src/tools/parallel_search.js +77 -0
- package/src/tools/phone_control.js +80 -0
- package/src/tools/phone_control_enhanced.js +184 -0
- package/src/tools/searxng.js +61 -0
- package/src/tools/speech_to_text.js +135 -0
- package/src/tools/text_to_speech.js +105 -0
- package/src/tools/thread_ownership.js +88 -0
- package/src/tools/video_generation.js +72 -0
- package/src/tools/web_readability.js +104 -0
- package/src/utils/memory.js +200 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const { getConfig, saveConfig } = require('../utils/config');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
name: 'thread_ownership',
|
|
5
|
+
description: 'Manage message thread ownership — assign threads to specific agents/bots',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
action: { type: 'string', description: 'Action: assign, release, status, list', enum: ['assign', 'release', 'status', 'list'] },
|
|
10
|
+
threadId: { type: 'string', description: 'Thread/channel/conversation ID' },
|
|
11
|
+
agentName: { type: 'string', description: 'Agent name to assign (for assign action)' },
|
|
12
|
+
channel: { type: 'string', description: 'Channel type: telegram, whatsapp, signal, irc, mattermost, discord, slack' }
|
|
13
|
+
},
|
|
14
|
+
required: ['action']
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
async execute(params) {
|
|
18
|
+
try {
|
|
19
|
+
const config = getConfig();
|
|
20
|
+
const ownership = config.threadOwnership || {};
|
|
21
|
+
|
|
22
|
+
if (params.action === 'list') {
|
|
23
|
+
const entries = Object.entries(ownership);
|
|
24
|
+
if (entries.length === 0) {
|
|
25
|
+
return { success: true, action: 'list', message: 'Atanmış thread yok.', threads: [] };
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
success: true,
|
|
29
|
+
action: 'list',
|
|
30
|
+
threads: entries.map(([id, agent]) => ({
|
|
31
|
+
threadId: id,
|
|
32
|
+
assignedAgent: agent
|
|
33
|
+
})),
|
|
34
|
+
count: entries.length
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (params.action === 'status') {
|
|
39
|
+
if (!params.threadId) {
|
|
40
|
+
return { success: false, error: 'threadId gerekli' };
|
|
41
|
+
}
|
|
42
|
+
const assigned = ownership[params.threadId];
|
|
43
|
+
return {
|
|
44
|
+
success: true,
|
|
45
|
+
action: 'status',
|
|
46
|
+
threadId: params.threadId,
|
|
47
|
+
assignedAgent: assigned || null,
|
|
48
|
+
isAssigned: !!assigned
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (params.action === 'assign') {
|
|
53
|
+
if (!params.threadId || !params.agentName) {
|
|
54
|
+
return { success: false, error: 'threadId ve agentName gerekli' };
|
|
55
|
+
}
|
|
56
|
+
ownership[params.threadId] = params.agentName;
|
|
57
|
+
config.threadOwnership = ownership;
|
|
58
|
+
saveConfig(config);
|
|
59
|
+
return {
|
|
60
|
+
success: true,
|
|
61
|
+
action: 'assign',
|
|
62
|
+
threadId: params.threadId,
|
|
63
|
+
agentName: params.agentName,
|
|
64
|
+
message: `Thread ${params.threadId} → ${params.agentName}`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (params.action === 'release') {
|
|
69
|
+
if (!params.threadId) {
|
|
70
|
+
return { success: false, error: 'threadId gerekli' };
|
|
71
|
+
}
|
|
72
|
+
delete ownership[params.threadId];
|
|
73
|
+
config.threadOwnership = ownership;
|
|
74
|
+
saveConfig(config);
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
action: 'release',
|
|
78
|
+
threadId: params.threadId,
|
|
79
|
+
message: `Thread ${params.threadId} serbest bırakıldı`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { success: false, error: `Bilinmeyen aksiyon: ${params.action}` };
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return { success: false, error: error.message };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const { getConfig } = require('../utils/config');
|
|
2
|
+
|
|
3
|
+
const PROVIDERS = {
|
|
4
|
+
runway: {
|
|
5
|
+
name: 'RunwayML',
|
|
6
|
+
async generate({ prompt, apiKey, model }) {
|
|
7
|
+
const response = await fetch('https://api.runwayml.com/v1/text_to_video', {
|
|
8
|
+
method: 'POST',
|
|
9
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
|
|
10
|
+
body: JSON.stringify({
|
|
11
|
+
model: model || 'gen3a_turbo',
|
|
12
|
+
prompt_text: prompt,
|
|
13
|
+
prompt_image: null,
|
|
14
|
+
duration: 5
|
|
15
|
+
})
|
|
16
|
+
});
|
|
17
|
+
if (!response.ok) throw new Error(`Runway error ${response.status}: ${await response.text()}`);
|
|
18
|
+
const data = await response.json();
|
|
19
|
+
return [{ url: data.video?.url || data.url, id: data.id }];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
name: 'video_generation',
|
|
26
|
+
description: 'Generate videos using AI. Supports RunwayML provider.',
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
prompt: { type: 'string', description: 'Text description of the video to generate' },
|
|
31
|
+
provider: { type: 'string', description: 'Video provider: runway (default: runway)', enum: ['runway'] },
|
|
32
|
+
model: { type: 'string', description: 'Model override (default: gen3a_turbo)' }
|
|
33
|
+
},
|
|
34
|
+
required: ['prompt']
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async execute(params) {
|
|
38
|
+
try {
|
|
39
|
+
const config = getConfig();
|
|
40
|
+
const provider = params.provider || 'runway';
|
|
41
|
+
|
|
42
|
+
const providerConfig = PROVIDERS[provider];
|
|
43
|
+
if (!providerConfig) {
|
|
44
|
+
return { success: false, error: `Desteklenmeyen provider: ${provider}` };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const apiKey = params.apiKey || config.runwayApiKey || process.env.RUNWAY_API_KEY;
|
|
48
|
+
if (!apiKey) {
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
error: `${providerConfig.name} API key gerekli.\nKur: natureco config set runwayApiKey <key>`
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const videos = await providerConfig.generate({
|
|
56
|
+
prompt: params.prompt,
|
|
57
|
+
apiKey,
|
|
58
|
+
model: params.model
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
success: true,
|
|
63
|
+
prompt: params.prompt,
|
|
64
|
+
provider,
|
|
65
|
+
videos,
|
|
66
|
+
count: videos.length
|
|
67
|
+
};
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return { success: false, error: error.message };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const { getConfig } = require('../utils/config');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
name: 'web_readability',
|
|
5
|
+
description: 'Extract readable content from any web page (Mozilla Readability algorithm, no API key needed)',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
url: { type: 'string', description: 'URL to extract readable content from' },
|
|
10
|
+
maxChars: { type: 'number', description: 'Maximum characters to return (default: 10000)', default: 10000 }
|
|
11
|
+
},
|
|
12
|
+
required: ['url']
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
async execute(params) {
|
|
16
|
+
try {
|
|
17
|
+
const maxChars = params.maxChars || 10000;
|
|
18
|
+
|
|
19
|
+
const response = await fetch(params.url, {
|
|
20
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NatureCo-CLI/2.0; +https://natureco.me)' },
|
|
21
|
+
signal: AbortSignal.timeout(15000)
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
return { success: false, error: `HTTP ${response.status}: ${response.statusText}` };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const html = await response.text();
|
|
29
|
+
const contentType = response.headers.get('content-type') || '';
|
|
30
|
+
const isHtml = contentType.includes('text/html') || contentType.includes('text/plain') || html.trim().startsWith('<');
|
|
31
|
+
|
|
32
|
+
if (!isHtml) {
|
|
33
|
+
return { success: false, error: 'URL does not contain HTML content' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Extract title
|
|
37
|
+
const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
38
|
+
const title = titleMatch ? titleMatch[1].trim() : '';
|
|
39
|
+
|
|
40
|
+
// Extract meta description
|
|
41
|
+
const descMatch = html.match(/<meta[^>]+name=["']description["'][^>]+content=["']([^"']*)["']/i);
|
|
42
|
+
const description = descMatch ? descMatch[1] : '';
|
|
43
|
+
|
|
44
|
+
// Strip HTML tags for readable text
|
|
45
|
+
let text = html
|
|
46
|
+
// Remove scripts and styles
|
|
47
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, ' ')
|
|
48
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, ' ')
|
|
49
|
+
.replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, ' ')
|
|
50
|
+
.replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, ' ')
|
|
51
|
+
.replace(/<header[^>]*>[\s\S]*?<\/header>/gi, ' ')
|
|
52
|
+
// Remove all HTML tags
|
|
53
|
+
.replace(/<[^>]+>/g, ' ')
|
|
54
|
+
// Decode HTML entities
|
|
55
|
+
.replace(/&/g, '&')
|
|
56
|
+
.replace(/</g, '<')
|
|
57
|
+
.replace(/>/g, '>')
|
|
58
|
+
.replace(/"/g, '"')
|
|
59
|
+
.replace(/'/g, "'")
|
|
60
|
+
.replace(///g, '/')
|
|
61
|
+
.replace(/&#\d+;/g, ' ')
|
|
62
|
+
// Normalize whitespace
|
|
63
|
+
.replace(/ /g, ' ')
|
|
64
|
+
.replace(/\s+/g, ' ')
|
|
65
|
+
.trim();
|
|
66
|
+
|
|
67
|
+
// Extract meaningful paragraphs
|
|
68
|
+
const paragraphs = [];
|
|
69
|
+
const pRegex = /<p[^>]*>([\s\S]*?)<\/p>/gi;
|
|
70
|
+
let pMatch;
|
|
71
|
+
while ((pMatch = pRegex.exec(html)) !== null) {
|
|
72
|
+
const pText = pMatch[1]
|
|
73
|
+
.replace(/<[^>]+>/g, '')
|
|
74
|
+
.replace(/ /g, ' ')
|
|
75
|
+
.replace(/\s+/g, ' ')
|
|
76
|
+
.trim();
|
|
77
|
+
if (pText.length > 20) paragraphs.push(pText);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Prefer paragraph extraction, fall back to full text
|
|
81
|
+
const content = paragraphs.length > 0
|
|
82
|
+
? paragraphs.join('\n\n')
|
|
83
|
+
: text;
|
|
84
|
+
|
|
85
|
+
const truncated = content.length > maxChars
|
|
86
|
+
? content.slice(0, maxChars) + '...'
|
|
87
|
+
: content;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
success: true,
|
|
91
|
+
url: params.url,
|
|
92
|
+
title,
|
|
93
|
+
description,
|
|
94
|
+
content: truncated,
|
|
95
|
+
wordCount: content.split(/\s+/).length,
|
|
96
|
+
totalChars: content.length,
|
|
97
|
+
truncated: content.length > maxChars,
|
|
98
|
+
source: 'readability'
|
|
99
|
+
};
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return { success: false, error: error.message };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
package/src/utils/memory.js
CHANGED
|
@@ -282,11 +282,211 @@ function clearMemory(botId) {
|
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
// ── Memory Wiki (structured pages) ──────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
const MEMORY_WIKI_DIR = path.join(CONFIG_DIR, 'memory-wiki');
|
|
288
|
+
|
|
289
|
+
function ensureWikiDir() {
|
|
290
|
+
if (!fs.existsSync(MEMORY_WIKI_DIR)) {
|
|
291
|
+
fs.mkdirSync(MEMORY_WIKI_DIR, { recursive: true });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function getWikiPage(slug) {
|
|
296
|
+
ensureWikiDir();
|
|
297
|
+
const file = path.join(MEMORY_WIKI_DIR, `${slug.replace(/[^a-z0-9_-]/gi, '_')}.json`);
|
|
298
|
+
if (!fs.existsSync(file)) return null;
|
|
299
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf-8')); } catch { return null; }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function saveWikiPage(slug, content) {
|
|
303
|
+
ensureWikiDir();
|
|
304
|
+
const file = path.join(MEMORY_WIKI_DIR, `${slug.replace(/[^a-z0-9_-]/gi, '_')}.json`);
|
|
305
|
+
fs.writeFileSync(file, JSON.stringify({ slug, content, updatedAt: new Date().toISOString() }, null, 2));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function listWikiPages() {
|
|
309
|
+
ensureWikiDir();
|
|
310
|
+
return fs.readdirSync(MEMORY_WIKI_DIR)
|
|
311
|
+
.filter(f => f.endsWith('.json'))
|
|
312
|
+
.map(f => {
|
|
313
|
+
try {
|
|
314
|
+
const data = JSON.parse(fs.readFileSync(path.join(MEMORY_WIKI_DIR, f), 'utf-8'));
|
|
315
|
+
return { slug: data.slug || f.replace('.json', ''), content: data.content || '', updatedAt: data.updatedAt };
|
|
316
|
+
} catch { return null; }
|
|
317
|
+
})
|
|
318
|
+
.filter(Boolean);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function searchWikiPages(query) {
|
|
322
|
+
const pages = listWikiPages();
|
|
323
|
+
const q = query.toLowerCase();
|
|
324
|
+
return pages.filter(p =>
|
|
325
|
+
p.slug.toLowerCase().includes(q) ||
|
|
326
|
+
p.content.toLowerCase().includes(q)
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── Semantic Memory Search ─────────────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
function semanticSearchMemory(query, limit = 5) {
|
|
333
|
+
const results = [];
|
|
334
|
+
const q = query.toLowerCase();
|
|
335
|
+
|
|
336
|
+
if (!fs.existsSync(MEMORY_DIR)) return results;
|
|
337
|
+
|
|
338
|
+
const files = fs.readdirSync(MEMORY_DIR).filter(f => f.endsWith('.json'));
|
|
339
|
+
for (const file of files) {
|
|
340
|
+
const botId = file.replace('.json', '');
|
|
341
|
+
const mem = loadMemory(botId);
|
|
342
|
+
|
|
343
|
+
// Score each fact by relevance
|
|
344
|
+
const scored = (mem.facts || [])
|
|
345
|
+
.map(f => {
|
|
346
|
+
const val = typeof f === 'string' ? f : f.value || '';
|
|
347
|
+
const words = q.split(/\s+/).filter(Boolean);
|
|
348
|
+
const matches = words.filter(w => val.toLowerCase().includes(w)).length;
|
|
349
|
+
const score = words.length > 0 ? matches / words.length : 0;
|
|
350
|
+
const boost = typeof f === 'object' ? (f.score || 5) / 10 : 0.5;
|
|
351
|
+
return { bot: mem.botName || botId, value: val, score: score * 0.7 + boost * 0.3 };
|
|
352
|
+
})
|
|
353
|
+
.filter(f => f.score > 0.1)
|
|
354
|
+
.sort((a, b) => b.score - a.score)
|
|
355
|
+
.slice(0, limit);
|
|
356
|
+
|
|
357
|
+
results.push(...scored);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return results.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ── Memory Categories ──────────────────────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
const MEMORY_CATEGORIES = {
|
|
366
|
+
personal: { label: 'Kişisel Bilgiler', icon: '👤' },
|
|
367
|
+
work: { label: 'İş/Okul', icon: '💼' },
|
|
368
|
+
preferences: { label: 'Tercihler', icon: '⭐' },
|
|
369
|
+
health: { label: 'Sağlık', icon: '❤️' },
|
|
370
|
+
social: { label: 'Sosyal', icon: '👥' },
|
|
371
|
+
goals: { label: 'Hedefler', icon: '🎯' },
|
|
372
|
+
projects: { label: 'Projeler', icon: '📁' },
|
|
373
|
+
general: { label: 'Genel', icon: '📝' }
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
function addMemoryEntryWithCategory(botId, key, value, category = 'general') {
|
|
377
|
+
const memory = loadMemory(botId);
|
|
378
|
+
const now = new Date().toISOString();
|
|
379
|
+
const validCategory = MEMORY_CATEGORIES[category] ? category : 'general';
|
|
380
|
+
|
|
381
|
+
if (key === 'name') { memory.name = value; }
|
|
382
|
+
else if (key === 'nickname') { memory.nickname = value; }
|
|
383
|
+
else if (key === 'botName') { memory.botName = value; }
|
|
384
|
+
else if (key === 'preference') {
|
|
385
|
+
const existing = memory.preferences.find(p => p.value.toLowerCase() === value.toLowerCase());
|
|
386
|
+
if (existing) {
|
|
387
|
+
existing.score = Math.min(existing.score + 1, 10);
|
|
388
|
+
existing.updatedAt = now;
|
|
389
|
+
} else {
|
|
390
|
+
memory.preferences.push({ value, score: 5, updatedAt: now, category: validCategory });
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
const existing = memory.facts.find(f => f.value.toLowerCase() === value.toLowerCase());
|
|
394
|
+
if (existing) {
|
|
395
|
+
existing.score = Math.min(existing.score + 1, 10);
|
|
396
|
+
existing.updatedAt = now;
|
|
397
|
+
if (!existing.category) existing.category = validCategory;
|
|
398
|
+
} else {
|
|
399
|
+
memory.facts.push({ value, score: 5, updatedAt: now, category: validCategory });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Decay old entries
|
|
404
|
+
const sixMonthsAgo = new Date();
|
|
405
|
+
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
|
|
406
|
+
|
|
407
|
+
memory.preferences = memory.preferences.map(p => {
|
|
408
|
+
const age = new Date(p.updatedAt);
|
|
409
|
+
if (age < sixMonthsAgo) p.score = Math.max(p.score - 1, 1);
|
|
410
|
+
return p;
|
|
411
|
+
}).filter(p => p.score > 0);
|
|
412
|
+
|
|
413
|
+
memory.facts = memory.facts.map(f => {
|
|
414
|
+
const age = new Date(f.updatedAt);
|
|
415
|
+
if (age < sixMonthsAgo) f.score = Math.max(f.score - 1, 1);
|
|
416
|
+
return f;
|
|
417
|
+
}).filter(f => f.score > 0);
|
|
418
|
+
|
|
419
|
+
saveMemory(botId, memory);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── Memory Import/Export ───────────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
function exportMemory(botId) {
|
|
425
|
+
return loadMemory(botId);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function importMemory(botId, data) {
|
|
429
|
+
const current = loadMemory(botId);
|
|
430
|
+
const merged = {
|
|
431
|
+
name: data.name || current.name,
|
|
432
|
+
nickname: data.nickname || current.nickname,
|
|
433
|
+
botName: data.botName || current.botName,
|
|
434
|
+
preferences: [...(current.preferences || []), ...(data.preferences || [])],
|
|
435
|
+
facts: [...(current.facts || []), ...(data.facts || [])],
|
|
436
|
+
lastSeen: current.lastSeen
|
|
437
|
+
};
|
|
438
|
+
saveMemory(botId, merged);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ── Active Memory (pre-prompt injection) ──────────────────────────────────────
|
|
442
|
+
|
|
443
|
+
function getActiveMemoryPrompt(botId, messageContext = '') {
|
|
444
|
+
const memory = loadMemory(botId);
|
|
445
|
+
const facts = memory.facts || [];
|
|
446
|
+
const prefs = memory.preferences || [];
|
|
447
|
+
|
|
448
|
+
if (facts.length === 0 && prefs.length === 0 && !memory.name) return '';
|
|
449
|
+
|
|
450
|
+
// Score facts by relevance to current context
|
|
451
|
+
const contextWords = messageContext.toLowerCase().split(/\s+/).filter(w => w.length > 3);
|
|
452
|
+
const scored = facts
|
|
453
|
+
.map(f => {
|
|
454
|
+
let relevance = 0;
|
|
455
|
+
if (contextWords.length > 0) {
|
|
456
|
+
const val = (f.value || '').toLowerCase();
|
|
457
|
+
relevance = contextWords.filter(w => val.includes(w)).length / contextWords.length;
|
|
458
|
+
}
|
|
459
|
+
return { ...f, relevance };
|
|
460
|
+
})
|
|
461
|
+
.sort((a, b) => (b.score || 0) + b.relevance - (a.score || 0) - a.relevance)
|
|
462
|
+
.slice(0, 5);
|
|
463
|
+
|
|
464
|
+
const parts = [];
|
|
465
|
+
if (memory.name) parts.push(`👤 ${memory.name}`);
|
|
466
|
+
if (memory.nickname) parts.push(`🏷 ${memory.nickname}`);
|
|
467
|
+
if (scored.length > 0) {
|
|
468
|
+
parts.push('📌 ' + scored.map(f => f.value).join(' | '));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return parts.length > 0 ? `<active_memory>\n${parts.join('\n')}\n</active_memory>` : '';
|
|
472
|
+
}
|
|
473
|
+
|
|
285
474
|
module.exports = {
|
|
286
475
|
loadMemory,
|
|
287
476
|
saveMemory,
|
|
288
477
|
addMemoryEntry,
|
|
478
|
+
addMemoryEntryWithCategory,
|
|
289
479
|
extractMemoryFromMessage,
|
|
290
480
|
getMemoryPrompt,
|
|
481
|
+
getActiveMemoryPrompt,
|
|
291
482
|
clearMemory,
|
|
483
|
+
semanticSearchMemory,
|
|
484
|
+
exportMemory,
|
|
485
|
+
importMemory,
|
|
486
|
+
// Wiki
|
|
487
|
+
getWikiPage,
|
|
488
|
+
saveWikiPage,
|
|
489
|
+
listWikiPages,
|
|
490
|
+
searchWikiPages,
|
|
491
|
+
MEMORY_CATEGORIES,
|
|
292
492
|
};
|