natureco-cli 2.23.28 → 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.
Files changed (96) hide show
  1. package/README.md +94 -11
  2. package/bin/natureco.js +470 -10
  3. package/package.json +10 -6
  4. package/src/commands/admin-rpc.js +219 -0
  5. package/src/commands/agent.js +89 -0
  6. package/src/commands/approvals.js +53 -0
  7. package/src/commands/backup.js +124 -0
  8. package/src/commands/bonjour.js +167 -0
  9. package/src/commands/capability.js +64 -0
  10. package/src/commands/channels.js +94 -4
  11. package/src/commands/chat.js +11 -25
  12. package/src/commands/clickclack.js +130 -0
  13. package/src/commands/commitments.js +32 -0
  14. package/src/commands/completion.js +76 -0
  15. package/src/commands/config.js +111 -68
  16. package/src/commands/configure.js +93 -0
  17. package/src/commands/crestodian.js +92 -0
  18. package/src/commands/daemon.js +60 -0
  19. package/src/commands/device-pair.js +248 -0
  20. package/src/commands/devices.js +110 -0
  21. package/src/commands/directory.js +47 -0
  22. package/src/commands/dns.js +58 -0
  23. package/src/commands/docs.js +43 -0
  24. package/src/commands/doctor.js +121 -16
  25. package/src/commands/exec-policy.js +71 -0
  26. package/src/commands/gateway-server.js +1175 -30
  27. package/src/commands/gateway.js +11 -20
  28. package/src/commands/health.js +18 -0
  29. package/src/commands/help.js +6 -0
  30. package/src/commands/imessage.js +169 -0
  31. package/src/commands/infer.js +73 -0
  32. package/src/commands/irc.js +119 -0
  33. package/src/commands/mattermost.js +164 -0
  34. package/src/commands/memory-cmd.js +134 -1
  35. package/src/commands/message.js +30 -4
  36. package/src/commands/migrate.js +213 -2
  37. package/src/commands/models.js +584 -216
  38. package/src/commands/node.js +98 -0
  39. package/src/commands/nodes.js +106 -0
  40. package/src/commands/oc-path.js +200 -0
  41. package/src/commands/onboard.js +70 -0
  42. package/src/commands/open-prose.js +67 -0
  43. package/src/commands/plugins.js +415 -172
  44. package/src/commands/policy.js +176 -0
  45. package/src/commands/proxy.js +155 -0
  46. package/src/commands/qr.js +28 -0
  47. package/src/commands/sandbox.js +125 -0
  48. package/src/commands/secrets.js +118 -0
  49. package/src/commands/security.js +149 -1
  50. package/src/commands/setup.js +114 -10
  51. package/src/commands/signal.js +495 -0
  52. package/src/commands/skills.js +20 -29
  53. package/src/commands/sms.js +168 -0
  54. package/src/commands/system.js +53 -0
  55. package/src/commands/tasks.js +328 -79
  56. package/src/commands/terminal.js +21 -0
  57. package/src/commands/thread-ownership.js +157 -0
  58. package/src/commands/transcripts.js +72 -0
  59. package/src/commands/voice.js +82 -0
  60. package/src/commands/vydra.js +98 -0
  61. package/src/commands/webhooks.js +79 -0
  62. package/src/commands/whatsapp.js +7 -21
  63. package/src/commands/workboard.js +207 -0
  64. package/src/tools/audio_understanding.js +154 -0
  65. package/src/tools/bash.js +63 -29
  66. package/src/tools/browser.js +112 -0
  67. package/src/tools/canvas.js +104 -0
  68. package/src/tools/document_extract.js +84 -0
  69. package/src/tools/duckduckgo.js +54 -0
  70. package/src/tools/exa_search.js +66 -0
  71. package/src/tools/firecrawl.js +104 -0
  72. package/src/tools/image_generation.js +99 -0
  73. package/src/tools/llm_task.js +118 -0
  74. package/src/tools/media_understanding.js +128 -0
  75. package/src/tools/music_generation.js +113 -0
  76. package/src/tools/parallel_search.js +77 -0
  77. package/src/tools/phone_control.js +80 -0
  78. package/src/tools/phone_control_enhanced.js +184 -0
  79. package/src/tools/searxng.js +61 -0
  80. package/src/tools/speech_to_text.js +135 -0
  81. package/src/tools/text_to_speech.js +105 -0
  82. package/src/tools/thread_ownership.js +88 -0
  83. package/src/tools/video_generation.js +72 -0
  84. package/src/tools/web_readability.js +104 -0
  85. package/src/utils/api.js +3 -20
  86. package/src/utils/approvals.js +297 -0
  87. package/src/utils/background.js +223 -66
  88. package/src/utils/baileys.js +21 -0
  89. package/src/utils/config.js +141 -10
  90. package/src/utils/errors.js +148 -0
  91. package/src/utils/inquirer-wrapper.js +1 -2
  92. package/src/utils/memory.js +200 -0
  93. package/src/utils/path-utils.js +13 -13
  94. package/src/utils/plugin-registry.js +238 -0
  95. package/src/utils/secrets.js +177 -0
  96. package/src/utils/skills.js +10 -23
@@ -1,11 +1,24 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
+ const crypto = require('crypto');
5
+ const { ConfigParseError, ConfigMutationConflictError, ConfigValidationError, handleError } = require('./errors');
6
+
7
+ let json5;
8
+ try {
9
+ json5 = require('json5');
10
+ } catch {
11
+ json5 = null;
12
+ }
4
13
 
5
14
  const CONFIG_DIR = path.join(os.homedir(), '.natureco');
6
15
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
16
+ const CONFIG_BACKUP_DIR = path.join(CONFIG_DIR, 'backups');
17
+ const MAX_BACKUPS = 10;
18
+
19
+ let _configCache = null;
20
+ let _configHash = null;
7
21
 
8
- // --profile flag desteği: ~/.natureco-<profile>/
9
22
  function getProfileDir() {
10
23
  const profileArg = process.argv.find(a => a.startsWith('--profile='));
11
24
  const profileIdx = process.argv.indexOf('--profile');
@@ -27,50 +40,134 @@ function ensureConfigDir() {
27
40
  }
28
41
  }
29
42
 
30
- function saveConfig(data) {
43
+ function computeHash(data) {
44
+ return crypto.createHash('sha256').update(JSON.stringify(data)).digest('hex');
45
+ }
46
+
47
+ function createBackup() {
48
+ if (!fs.existsSync(ACTIVE_CONFIG_FILE)) return;
31
49
  ensureConfigDir();
32
- fs.writeFileSync(ACTIVE_CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8');
50
+ if (!fs.existsSync(CONFIG_BACKUP_DIR)) {
51
+ fs.mkdirSync(CONFIG_BACKUP_DIR, { recursive: true });
52
+ }
53
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
54
+ const backupFile = path.join(CONFIG_BACKUP_DIR, `config-${timestamp}.json`);
55
+ fs.copyFileSync(ACTIVE_CONFIG_FILE, backupFile);
56
+ const backups = fs.readdirSync(CONFIG_BACKUP_DIR)
57
+ .filter(f => f.startsWith('config-') && f.endsWith('.json'))
58
+ .sort()
59
+ .reverse();
60
+ if (backups.length > MAX_BACKUPS) {
61
+ backups.slice(MAX_BACKUPS).forEach(f => {
62
+ try { fs.unlinkSync(path.join(CONFIG_BACKUP_DIR, f)); } catch {}
63
+ });
64
+ }
65
+ }
66
+
67
+ function parseConfigContent(content) {
68
+ if (!content || !content.trim()) return {};
69
+ const trimmed = content.trim();
70
+ if (trimmed.startsWith('{')) {
71
+ if (json5) {
72
+ try { return json5.parse(trimmed); } catch {}
73
+ }
74
+ return JSON.parse(trimmed);
75
+ }
76
+ return JSON.parse(trimmed);
77
+ }
78
+
79
+ function validateConfig(data) {
80
+ if (data === null || data === undefined) throw new ConfigValidationError('Config cannot be null', { field: 'root' });
81
+ if (typeof data !== 'object' || Array.isArray(data)) throw new ConfigValidationError('Config must be a JSON object', { field: 'root' });
82
+ if (data.apiKey !== undefined && typeof data.apiKey !== 'string') throw new ConfigValidationError('apiKey must be a string', { field: 'apiKey' });
83
+ if (data.providerUrl !== undefined && typeof data.providerUrl !== 'string') throw new ConfigValidationError('providerUrl must be a string', { field: 'providerUrl' });
84
+ if (data.providerModel !== undefined && typeof data.providerModel !== 'string') throw new ConfigValidationError('providerModel must be a string', { field: 'providerModel' });
85
+ return true;
86
+ }
87
+
88
+ function saveConfig(data, options = {}) {
89
+ const { skipBackup = false, skipValidation = false } = options;
90
+ ensureConfigDir();
91
+ if (!skipValidation) validateConfig(data);
92
+ if (!skipBackup) createBackup();
93
+ const content = JSON.stringify(data, null, 2);
94
+ fs.writeFileSync(ACTIVE_CONFIG_FILE, content, 'utf8');
95
+ _configCache = data;
96
+ _configHash = computeHash(data);
33
97
  }
34
98
 
35
- function loadConfig() {
99
+ function loadConfig(options = {}) {
100
+ const { useCache = true, skipValidation = false } = options;
101
+ if (useCache && _configCache) return _configCache;
36
102
  if (!fs.existsSync(ACTIVE_CONFIG_FILE)) {
103
+ _configCache = null;
104
+ _configHash = null;
37
105
  return null;
38
106
  }
39
107
  try {
40
108
  const content = fs.readFileSync(ACTIVE_CONFIG_FILE, 'utf8');
41
- return JSON.parse(content);
109
+ const data = parseConfigContent(content);
110
+ if (!skipValidation) validateConfig(data);
111
+ _configCache = data;
112
+ _configHash = computeHash(data);
113
+ return data;
42
114
  } catch (err) {
115
+ _configCache = null;
116
+ _configHash = null;
117
+ if (err instanceof ConfigValidationError || err instanceof ConfigParseError) throw err;
43
118
  return null;
44
119
  }
45
120
  }
46
121
 
122
+ function loadConfigWithRetry(maxRetries = 3) {
123
+ for (let i = 0; i < maxRetries; i++) {
124
+ try {
125
+ return loadConfig({ useCache: false });
126
+ } catch (err) {
127
+ if (i === maxRetries - 1) throw err;
128
+ }
129
+ }
130
+ return null;
131
+ }
132
+
47
133
  function deleteConfig() {
48
134
  if (fs.existsSync(ACTIVE_CONFIG_FILE)) {
135
+ createBackup();
49
136
  fs.unlinkSync(ACTIVE_CONFIG_FILE);
50
137
  }
138
+ _configCache = null;
139
+ _configHash = null;
51
140
  }
52
141
 
53
142
  function getApiKey() {
54
143
  const config = loadConfig();
55
- return config?.apiKey || null;
144
+ return config?.apiKey ?? null;
56
145
  }
57
146
 
58
147
  function saveApiKey(apiKey) {
59
- const config = loadConfig() || {};
148
+ const config = loadConfig() ?? {};
60
149
  config.apiKey = apiKey;
61
150
  saveConfig(config);
62
151
  }
63
152
 
64
153
  function getConfig() {
65
- return loadConfig() || {};
154
+ try {
155
+ return loadConfig() ?? {};
156
+ } catch {
157
+ return {};
158
+ }
66
159
  }
67
160
 
68
161
  function getAllConfig() {
69
- return loadConfig() || {};
162
+ try {
163
+ return loadConfig() ?? {};
164
+ } catch {
165
+ return {};
166
+ }
70
167
  }
71
168
 
72
169
  function setConfigValue(key, value) {
73
- const config = loadConfig() || {};
170
+ const config = loadConfig() ?? {};
74
171
  const keys = key.split('.');
75
172
  let current = config;
76
173
  for (let i = 0; i < keys.length - 1; i++) {
@@ -83,15 +180,49 @@ function setConfigValue(key, value) {
83
180
  saveConfig(config);
84
181
  }
85
182
 
183
+ function getConfigHash() {
184
+ return _configHash;
185
+ }
186
+
187
+ function listBackups() {
188
+ if (!fs.existsSync(CONFIG_BACKUP_DIR)) return [];
189
+ return fs.readdirSync(CONFIG_BACKUP_DIR)
190
+ .filter(f => f.startsWith('config-') && f.endsWith('.json'))
191
+ .sort()
192
+ .reverse();
193
+ }
194
+
195
+ function restoreConfig(backupFile) {
196
+ const backupPath = path.isAbsolute(backupFile)
197
+ ? backupFile
198
+ : path.join(CONFIG_BACKUP_DIR, backupFile);
199
+ if (!fs.existsSync(backupPath)) {
200
+ throw new ConfigValidationError(`Yedek dosyası bulunamadı: ${backupPath}`, { field: 'backupFile' });
201
+ }
202
+ const content = fs.readFileSync(backupPath, 'utf8');
203
+ const data = parseConfigContent(content);
204
+ validateConfig(data);
205
+ createBackup();
206
+ fs.writeFileSync(ACTIVE_CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8');
207
+ _configCache = data;
208
+ _configHash = computeHash(data);
209
+ return { path: backupPath, timestamp: path.basename(backupPath).replace(/^config-|\.json$/g, '') };
210
+ }
211
+
86
212
  module.exports = {
87
213
  saveConfig,
88
214
  loadConfig,
215
+ loadConfigWithRetry,
89
216
  deleteConfig,
90
217
  getApiKey,
91
218
  saveApiKey,
92
219
  getConfig,
93
220
  getAllConfig,
94
221
  setConfigValue,
222
+ getConfigHash,
223
+ listBackups,
224
+ restoreConfig,
95
225
  CONFIG_FILE: ACTIVE_CONFIG_FILE,
96
226
  CONFIG_DIR: ACTIVE_CONFIG_DIR,
227
+ CONFIG_BACKUP_DIR,
97
228
  };
@@ -0,0 +1,148 @@
1
+ const chalk = require('chalk');
2
+
3
+ class NatureCoError extends Error {
4
+ constructor(message, options = {}) {
5
+ super(message);
6
+ this.name = this.constructor.name;
7
+ this.cause = options.cause || null;
8
+ this.exitCode = options.exitCode || 1;
9
+ if (Error.captureStackTrace) {
10
+ Error.captureStackTrace(this, this.constructor);
11
+ }
12
+ }
13
+ }
14
+
15
+ class ConfigError extends NatureCoError {
16
+ constructor(message, options = {}) {
17
+ super(message, options);
18
+ this.configPath = options.configPath || null;
19
+ }
20
+ }
21
+
22
+ class ConfigParseError extends ConfigError {
23
+ constructor(message, options = {}) {
24
+ super(`Config parse error: ${message}`, options);
25
+ }
26
+ }
27
+
28
+ class ConfigMutationConflictError extends ConfigError {
29
+ constructor(message, options = {}) {
30
+ super(`Config conflict: ${message}`, options);
31
+ this.currentHash = options.currentHash || null;
32
+ }
33
+ }
34
+
35
+ class ConfigValidationError extends ConfigError {
36
+ constructor(message, options = {}) {
37
+ super(`Config validation error: ${message}`, options);
38
+ this.field = options.field || null;
39
+ }
40
+ }
41
+
42
+ class ApiError extends NatureCoError {
43
+ constructor(message, options = {}) {
44
+ super(message, options);
45
+ this.statusCode = options.statusCode || null;
46
+ this.provider = options.provider || null;
47
+ }
48
+ }
49
+
50
+ class ProviderError extends ApiError {
51
+ constructor(message, options = {}) {
52
+ super(`Provider error (${options.provider || 'unknown'}): ${message}`, options);
53
+ }
54
+ }
55
+
56
+ class AuthenticationError extends ApiError {
57
+ constructor(message, options = {}) {
58
+ super(`Authentication error: ${message}`, options);
59
+ }
60
+ }
61
+
62
+ class ToolError extends NatureCoError {
63
+ constructor(message, options = {}) {
64
+ super(message, options);
65
+ this.toolName = options.toolName || null;
66
+ }
67
+ }
68
+
69
+ class ToolInputError extends ToolError {
70
+ constructor(message, options = {}) {
71
+ super(`Invalid tool input: ${message}`, options);
72
+ }
73
+ }
74
+
75
+ class ToolExecutionError extends ToolError {
76
+ constructor(message, options = {}) {
77
+ super(`Tool execution failed: ${message}`, options);
78
+ }
79
+ }
80
+
81
+ class ChannelError extends NatureCoError {
82
+ constructor(message, options = {}) {
83
+ super(message, options);
84
+ this.channel = options.channel || null;
85
+ }
86
+ }
87
+
88
+ class GatewayError extends NatureCoError {
89
+ constructor(message, options = {}) {
90
+ super(message, options);
91
+ }
92
+ }
93
+
94
+ class PluginError extends NatureCoError {
95
+ constructor(message, options = {}) {
96
+ super(message, options);
97
+ this.plugin = options.plugin || null;
98
+ }
99
+ }
100
+
101
+ class SkillError extends NatureCoError {
102
+ constructor(message, options = {}) {
103
+ super(message, options);
104
+ this.skill = options.skill || null;
105
+ }
106
+ }
107
+
108
+ class MigrationError extends NatureCoError {
109
+ constructor(message, options = {}) {
110
+ super(message, options);
111
+ this.step = options.step || null;
112
+ }
113
+ }
114
+
115
+ function handleError(err, options = {}) {
116
+ const { prefix = '', exit = true, log = true } = options;
117
+
118
+ if (log) {
119
+ const message = err instanceof NatureCoError
120
+ ? `${prefix}${err.message}`
121
+ : `${prefix}${err.message || 'An unknown error occurred'}`;
122
+ console.log(chalk.red(`\n${message}\n`));
123
+ }
124
+
125
+ if (exit) {
126
+ process.exit(err instanceof NatureCoError ? err.exitCode : 1);
127
+ }
128
+ }
129
+
130
+ module.exports = {
131
+ NatureCoError,
132
+ ConfigError,
133
+ ConfigParseError,
134
+ ConfigMutationConflictError,
135
+ ConfigValidationError,
136
+ ApiError,
137
+ ProviderError,
138
+ AuthenticationError,
139
+ ToolError,
140
+ ToolInputError,
141
+ ToolExecutionError,
142
+ ChannelError,
143
+ GatewayError,
144
+ PluginError,
145
+ SkillError,
146
+ MigrationError,
147
+ handleError,
148
+ };
@@ -1,4 +1,4 @@
1
- const { select, input, password, confirm } = require('@inquirer/prompts');
1
+ const { select, input, password, confirm, checkbox } = require('@inquirer/prompts');
2
2
 
3
3
  module.exports = {
4
4
  async prompt(questions) {
@@ -14,7 +14,6 @@ module.exports = {
14
14
  } else if (q.type === 'password') {
15
15
  results[q.name] = await password({ message: q.message, mask: q.mask });
16
16
  } else if (q.type === 'checkbox') {
17
- const { checkbox } = require('@inquirer/prompts');
18
17
  results[q.name] = await checkbox({
19
18
  message: q.message,
20
19
  choices: q.choices.map(c =>
@@ -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
  };
@@ -3,19 +3,19 @@ const os = require('os');
3
3
  function normalizeWindowsPaths(str) {
4
4
  let result = str.replace(/\\/g, '/');
5
5
 
6
- result = result
7
- .replace(/C:\/\/\/\/Users\/\/\/\/user\/\/\/\//g, `${os.homedir()}/`)
8
- .replace(/C:\/Users\/user\//g, `${os.homedir()}/`)
9
- .replace(/C:\/Users\/user\//g, `${os.homedir()}/`)
10
- .replace(/C:\/\/Users\/\/user\/\/\.natureco\/\//g, `${os.homedir()}/.natureco/`)
11
- .replace(/C:\/\/Users\/\/user\/\//g, `${os.homedir()}/`)
12
- .replace(/E:\/\/\.natureco\/\//g, `${os.homedir()}/.natureco/`)
13
- .replace(/'C:\/Users\/user\/\.natureco\//g, `'${os.homedir()}/.natureco/`)
14
- .replace(/"C:\/Users\/user\/\.natureco\//g, `"${os.homedir()}/.natureco/`)
15
- .replace(/'C:\/\/\/\/Users\/\/\/\/user\/\/\/\/\.natureco\/\/\/\//g, `'${os.homedir()}/.natureco/`)
16
- .replace(/E:\/\.openclaw\//g, `${os.homedir()}/.natureco/`)
17
- .replace(/\.openclaw\//g, '.natureco/')
18
- .replace(/workspace\/scripts\\/g, 'workspace/scripts/');
6
+ const homeDir = os.homedir().replace(/\\/g, '/');
7
+
8
+ // Replace any Windows user profile path with actual homedir
9
+ result = result.replace(/[A-Za-z]:\/Users\/[^/]+\//g, `${homeDir}/`);
10
+
11
+ // Replace bare drive-letter references pointing to .openclaw
12
+ result = result.replace(/[A-Za-z]:\/\.openclaw\//g, `${homeDir}/.natureco/`);
13
+
14
+ // Migrate .openclaw paths to .natureco
15
+ result = result.replace(/\.openclaw\//g, '.natureco/');
16
+
17
+ // Normalize mixed path separators
18
+ result = result.replace(/workspace\/scripts\\/g, 'workspace/scripts/');
19
19
 
20
20
  return result;
21
21
  }