@pheem49/mint 1.4.2 → 1.5.1

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 (97) hide show
  1. package/GUIDE_TH.md +113 -0
  2. package/README.md +267 -78
  3. package/assets/CLI_Screen.png +0 -0
  4. package/main.js +76 -890
  5. package/mint-cli-logic.js +3 -107
  6. package/mint-cli.js +594 -29
  7. package/models/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
  8. package/models/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +14 -0
  9. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +10 -0
  10. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253/347/234/274/347/217/240/346/221/207/346/231/203.exp3.json +15 -0
  11. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json +10 -0
  12. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json +50 -0
  13. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json +10 -0
  14. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +10 -0
  15. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json +10 -0
  16. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json +10 -0
  17. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png +0 -0
  18. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png +0 -0
  19. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png +0 -0
  20. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png +0 -0
  21. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json +1498 -0
  22. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3 +0 -0
  23. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +47 -0
  24. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json +6658 -0
  25. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json +1299 -0
  26. package/models/Shiroko_Model/Shiroko//342/232/241/351/253/230/344/272/256/342/232/241/344/275/277/347/224/250/346/225/231/347/250/213/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.txt +23 -0
  27. package/package.json +37 -4
  28. package/src/AI_Brain/Gemini_API.js +223 -65
  29. package/src/AI_Brain/autonomous_brain.js +11 -0
  30. package/src/AI_Brain/behavior_memory.js +26 -5
  31. package/src/AI_Brain/headless_agent.js +4 -0
  32. package/src/AI_Brain/knowledge_base.js +61 -8
  33. package/src/AI_Brain/memory_store.js +354 -10
  34. package/src/Automation_Layer/file_operations.js +1 -1
  35. package/src/CLI/chat_router.js +20 -7
  36. package/src/CLI/chat_ui.js +596 -825
  37. package/src/CLI/code_agent.js +347 -56
  38. package/src/CLI/gmail_auth.js +210 -0
  39. package/src/CLI/image_input.js +90 -0
  40. package/src/CLI/list_features.js +2 -0
  41. package/src/CLI/onboarding.js +364 -55
  42. package/src/CLI/updater.js +210 -0
  43. package/src/Channels/brave_search_bridge.js +35 -0
  44. package/src/Channels/discord_bridge.js +68 -0
  45. package/src/Channels/google_search_bridge.js +38 -0
  46. package/src/Channels/line_bridge.js +60 -0
  47. package/src/Channels/slack_bridge.js +53 -0
  48. package/src/Channels/telegram_bridge.js +49 -0
  49. package/src/Channels/whatsapp_bridge.js +55 -0
  50. package/src/Command_Parser/parser.js +12 -1
  51. package/src/Plugins/gmail.js +251 -0
  52. package/src/Plugins/google_calendar.js +245 -19
  53. package/src/Plugins/notion.js +256 -0
  54. package/src/System/action_executor.js +178 -0
  55. package/src/System/bridge_manager.js +76 -0
  56. package/src/System/chat_history_manager.js +23 -5
  57. package/src/System/config_manager.js +71 -7
  58. package/src/System/custom_workflows.js +31 -2
  59. package/src/System/google_tts_urls.js +51 -0
  60. package/src/System/granular_automation.js +122 -53
  61. package/src/System/ipc_handlers.js +238 -0
  62. package/src/System/proactive_loop.js +153 -0
  63. package/src/System/safety_manager.js +273 -0
  64. package/src/System/sandbox_runner.js +182 -0
  65. package/src/System/screen_capture.js +175 -0
  66. package/src/System/system_automation.js +127 -81
  67. package/src/System/system_info.js +70 -0
  68. package/src/System/task_manager.js +15 -5
  69. package/src/System/tool_registry.js +280 -0
  70. package/src/System/window_manager.js +212 -0
  71. package/src/UI/live2d_manager.js +368 -0
  72. package/src/UI/renderer.js +208 -24
  73. package/src/UI/settings.html +24 -0
  74. package/src/UI/settings.js +14 -4
  75. package/src/UI/styles.css +466 -32
  76. package/.codex +0 -0
  77. package/docs/assets/Agent_Mint.png +0 -0
  78. package/docs/assets/CLI_Screen.png +0 -0
  79. package/docs/assets/Settings.png +0 -0
  80. package/docs/assets/icon.png +0 -0
  81. package/docs/index.html +0 -132
  82. package/docs/style.css +0 -579
  83. package/index.html +0 -16
  84. package/src/UI/index.html +0 -126
  85. package/tech_news.txt +0 -3
  86. package/test_knowledge.txt +0 -3
  87. package/tests/agent_orchestrator.test.js +0 -41
  88. package/tests/chat_router.test.js +0 -42
  89. package/tests/code_agent.test.js +0 -69
  90. package/tests/config_manager.test.js +0 -141
  91. package/tests/docker.test.js +0 -46
  92. package/tests/file_operations.test.js +0 -57
  93. package/tests/memory_store.test.js +0 -185
  94. package/tests/provider_routing.test.js +0 -67
  95. package/tests/spotify.test.js +0 -201
  96. package/tests/system_monitor.test.js +0 -37
  97. package/tests/workspace_manager.test.js +0 -56
@@ -3,6 +3,7 @@ const { readConfig } = require('../System/config_manager');
3
3
  const { performWebAutomation } = require('../Automation_Layer/browser_automation');
4
4
  const { createFolder, deleteFile } = require('../Automation_Layer/file_operations');
5
5
  const { searchKnowledge } = require('./knowledge_base');
6
+ const safetyManager = require('../System/safety_manager');
6
7
  const fs = require('fs');
7
8
  const path = require('path');
8
9
 
@@ -99,8 +100,16 @@ async function executeAutonomousTask(taskDescription, notifyCallback) {
99
100
  break;
100
101
  case 'write_file':
101
102
  const filePath = expandHome(actionObj.target);
103
+ safetyManager.assertPathCapability(filePath, 'write');
102
104
  if (notifyCallback) notifyCallback(`✍️ กำลังบันทึกไฟล์: ${actionObj.target}`);
103
105
  try {
106
+ safetyManager.appendActionLog({
107
+ source: 'autonomous_brain',
108
+ action: 'write_file',
109
+ target: filePath,
110
+ tier: safetyManager.TIERS.APPROVAL,
111
+ approved: true
112
+ });
104
113
  fs.writeFileSync(filePath, actionObj.data || '');
105
114
  observation = `File written successfully to ${actionObj.target}`;
106
115
  } catch (e) {
@@ -109,6 +118,8 @@ async function executeAutonomousTask(taskDescription, notifyCallback) {
109
118
  break;
110
119
  case 'delete_file':
111
120
  const delPath = expandHome(actionObj.target);
121
+ safetyManager.assertActionAllowed({ type: 'delete_file', target: delPath });
122
+ safetyManager.assertPathCapability(delPath, 'write');
112
123
  if (notifyCallback) notifyCallback(`🗑️ มิ้นท์ขอย้ายไฟล์ไปที่ถังขยะ: ${actionObj.target}`);
113
124
  const resDel = await deleteFile(delPath);
114
125
  observation = resDel.success ? "File moved to trash." : `Failed: ${resDel.message}`;
@@ -1,12 +1,33 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { app } = require('electron');
3
+ const os = require('os');
4
+
5
+ // Handle electron dependency safely
6
+ let app;
7
+ try {
8
+ const electron = require('electron');
9
+ app = electron.app;
10
+ } catch (e) {
11
+ app = null;
12
+ }
13
+
14
+ const CONFIG_DIR = path.join(os.homedir(), '.config', 'mint');
15
+ if (!fs.existsSync(CONFIG_DIR)) {
16
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
17
+ }
4
18
 
5
- // ============================================================
6
- // Behavior Memory — Tracks user behavior patterns over time
7
- // ============================================================
19
+ const MEMORY_FILE = path.join(CONFIG_DIR, 'behavior_memory.json');
8
20
 
9
- const MEMORY_FILE = path.join(app.getPath('userData'), 'behavior_memory.json');
21
+ // Migration Logic: Move from Electron userData to ~/.config/mint
22
+ if (!fs.existsSync(MEMORY_FILE) && app && app.getPath) {
23
+ const electronPath = path.join(app.getPath('userData'), 'behavior_memory.json');
24
+ if (fs.existsSync(electronPath)) {
25
+ try {
26
+ fs.copyFileSync(electronPath, MEMORY_FILE);
27
+ console.log('[BehaviorMemory] Migrated memory from Electron userData');
28
+ } catch (e) { console.error('[BehaviorMemory] Migration failed:', e); }
29
+ }
30
+ }
10
31
  const MAX_CONTEXT_HISTORY = 20; // Keep last 20 context snapshots
11
32
 
12
33
  /**
@@ -27,6 +27,10 @@ async function startAgent() {
27
27
  // Initialize System Monitoring
28
28
  systemEvents.startMonitoring();
29
29
 
30
+ // Initialize Messaging Bridges
31
+ const bridgeManager = require('../System/bridge_manager');
32
+ bridgeManager.init().catch(err => console.error('[BridgeManager] Init Error:', err));
33
+
30
34
  // Listen for Battery Events
31
35
  systemEvents.on('low-battery', (level) => {
32
36
  sendNotification(
@@ -5,7 +5,7 @@ const crypto = require('crypto');
5
5
  const { GoogleGenAI } = require('@google/genai');
6
6
  const pdf = require('pdf-parse');
7
7
  const mammoth = require('mammoth');
8
- const xlsx = require('xlsx');
8
+ const readXlsxFile = require('read-excel-file/node');
9
9
  const { readConfig } = require('../System/config_manager');
10
10
 
11
11
  // Handle electron dependency safely
@@ -44,12 +44,32 @@ function getAiClient() {
44
44
 
45
45
  function getDbPath() {
46
46
  const fileName = 'mint-knowledge.sqlite';
47
- if (app && app.getPath) {
48
- return path.join(app.getPath('userData'), fileName);
47
+ const configDir = path.join(os.homedir(), '.config', 'mint');
48
+ const dbPath = path.join(configDir, fileName);
49
+
50
+ if (!fs.existsSync(configDir)) {
51
+ fs.mkdirSync(configDir, { recursive: true });
52
+ }
53
+
54
+ // Migration Logic
55
+ if (!fs.existsSync(dbPath)) {
56
+ const electronDb = app && app.getPath ? path.join(app.getPath('userData'), fileName) : null;
57
+ const legacyDb = path.join(os.homedir(), '.mint', fileName);
58
+
59
+ if (electronDb && fs.existsSync(electronDb)) {
60
+ try {
61
+ fs.copyFileSync(electronDb, dbPath);
62
+ console.log('[RAG] Migrated database from Electron userData');
63
+ } catch (e) { console.error('[RAG] Migration from Electron failed:', e); }
64
+ } else if (fs.existsSync(legacyDb)) {
65
+ try {
66
+ fs.copyFileSync(legacyDb, dbPath);
67
+ console.log('[RAG] Migrated database from ~/.mint');
68
+ } catch (e) { console.error('[RAG] Migration from ~/.mint failed:', e); }
69
+ }
49
70
  }
50
- const mintDir = path.join(os.homedir(), '.mint');
51
- if (!fs.existsSync(mintDir)) fs.mkdirSync(mintDir, { recursive: true });
52
- return path.join(mintDir, fileName);
71
+
72
+ return dbPath;
53
73
  }
54
74
 
55
75
  function getDatabaseSync() {
@@ -67,8 +87,13 @@ function getDb() {
67
87
  const Database = getDatabaseSync();
68
88
  dbInstance = new Database(dbPath);
69
89
 
90
+ // Enable WAL mode for better concurrency
91
+ dbInstance.exec('PRAGMA journal_mode = WAL;');
92
+ dbInstance.exec('PRAGMA synchronous = NORMAL;');
93
+
70
94
  // Create Tables
71
95
  dbInstance.exec(`
96
+ -- Shared knowledge tables
72
97
  CREATE TABLE IF NOT EXISTS sources (
73
98
  id INTEGER PRIMARY KEY AUTOINCREMENT,
74
99
  path TEXT UNIQUE,
@@ -84,6 +109,29 @@ function getDb() {
84
109
  FOREIGN KEY(source_id) REFERENCES sources(id) ON DELETE CASCADE
85
110
  );
86
111
  CREATE INDEX IF NOT EXISTS idx_chunks_source ON chunks(source_id);
112
+
113
+ -- Shared memory tables (ensuring consistency)
114
+ CREATE TABLE IF NOT EXISTS user_profile (
115
+ key TEXT PRIMARY KEY,
116
+ value TEXT,
117
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
118
+ );
119
+ CREATE TABLE IF NOT EXISTS session_memories (
120
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
121
+ summary TEXT NOT NULL,
122
+ tags TEXT DEFAULT '',
123
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
124
+ );
125
+ CREATE TABLE IF NOT EXISTS usage_patterns (
126
+ pattern TEXT PRIMARY KEY,
127
+ count INTEGER DEFAULT 1,
128
+ last_used DATETIME DEFAULT CURRENT_TIMESTAMP
129
+ );
130
+ CREATE TABLE IF NOT EXISTS response_cache (
131
+ query_hash TEXT PRIMARY KEY,
132
+ response TEXT NOT NULL,
133
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
134
+ );
87
135
  `);
88
136
  return dbInstance;
89
137
  }
@@ -154,8 +202,13 @@ async function indexFile(filePath) {
154
202
  const res = await mammoth.extractRawText({ path: filePath });
155
203
  content = res.value;
156
204
  } else if (ext === '.xlsx') {
157
- const wb = xlsx.readFile(filePath);
158
- content = wb.SheetNames.map(n => xlsx.utils.sheet_to_csv(wb.Sheets[n])).join('\n');
205
+ const sheets = await readXlsxFile(filePath);
206
+ content = sheets
207
+ .map(({ sheet, data }) => [
208
+ `Sheet: ${sheet}`,
209
+ ...data.map(row => row.map(value => value == null ? '' : String(value)).join(','))
210
+ ].join('\n'))
211
+ .join('\n');
159
212
  } else {
160
213
  content = fs.readFileSync(filePath, 'utf8');
161
214
  }
@@ -25,12 +25,32 @@ try {
25
25
 
26
26
  function getDbPath() {
27
27
  const fileName = 'mint-knowledge.sqlite'; // shared DB with knowledge_base
28
- if (app && app.getPath) {
29
- return path.join(app.getPath('userData'), fileName);
28
+ const configDir = path.join(os.homedir(), '.config', 'mint');
29
+ const dbPath = path.join(configDir, fileName);
30
+
31
+ if (!fs.existsSync(configDir)) {
32
+ fs.mkdirSync(configDir, { recursive: true });
33
+ }
34
+
35
+ // Migration Logic
36
+ if (!fs.existsSync(dbPath)) {
37
+ const electronDb = app && app.getPath ? path.join(app.getPath('userData'), fileName) : null;
38
+ const legacyDb = path.join(os.homedir(), '.mint', fileName);
39
+
40
+ if (electronDb && fs.existsSync(electronDb)) {
41
+ try {
42
+ fs.copyFileSync(electronDb, dbPath);
43
+ console.log('[Memory] Migrated database from Electron userData');
44
+ } catch (e) { console.error('[Memory] Migration from Electron failed:', e); }
45
+ } else if (fs.existsSync(legacyDb)) {
46
+ try {
47
+ fs.copyFileSync(legacyDb, dbPath);
48
+ console.log('[Memory] Migrated database from ~/.mint');
49
+ } catch (e) { console.error('[Memory] Migration from ~/.mint failed:', e); }
50
+ }
30
51
  }
31
- const mintDir = path.join(os.homedir(), '.mint');
32
- if (!fs.existsSync(mintDir)) fs.mkdirSync(mintDir, { recursive: true });
33
- return path.join(mintDir, fileName);
52
+
53
+ return dbPath;
34
54
  }
35
55
 
36
56
  // ── Lazy DatabaseSync init ─────────────────────────────────────────────────
@@ -45,6 +65,10 @@ function getDb() {
45
65
  if (dbInstance) return dbInstance;
46
66
  const Database = getDatabaseSync();
47
67
  dbInstance = new Database(getDbPath());
68
+
69
+ // Enable WAL mode for better concurrency
70
+ dbInstance.exec('PRAGMA journal_mode = WAL;');
71
+ dbInstance.exec('PRAGMA synchronous = NORMAL;');
48
72
 
49
73
  dbInstance.exec(`
50
74
  -- User profile: arbitrary key-value pairs
@@ -69,17 +93,49 @@ function getDb() {
69
93
  last_used DATETIME DEFAULT CURRENT_TIMESTAMP
70
94
  );
71
95
 
96
+ -- Raw episodic memories of user/assistant turns.
97
+ CREATE TABLE IF NOT EXISTS interaction_memories (
98
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
99
+ user_text TEXT NOT NULL,
100
+ ai_text TEXT NOT NULL,
101
+ keywords TEXT DEFAULT '',
102
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
103
+ );
104
+
72
105
  -- Response Cache: For repetitive exact queries
73
106
  CREATE TABLE IF NOT EXISTS response_cache (
74
107
  query_hash TEXT PRIMARY KEY,
75
108
  response TEXT NOT NULL,
76
109
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
77
110
  );
111
+
112
+ -- Learned skill/instruction documents imported from local files.
113
+ CREATE TABLE IF NOT EXISTS learned_skills (
114
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
115
+ name TEXT NOT NULL,
116
+ source_path TEXT NOT NULL UNIQUE,
117
+ content TEXT NOT NULL,
118
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
119
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
120
+ );
78
121
  `);
79
122
 
80
123
  return dbInstance;
81
124
  }
82
125
 
126
+ function ensureLearnedSkillsTable() {
127
+ getDb().exec(`
128
+ CREATE TABLE IF NOT EXISTS learned_skills (
129
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
130
+ name TEXT NOT NULL,
131
+ source_path TEXT NOT NULL UNIQUE,
132
+ content TEXT NOT NULL,
133
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
134
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
135
+ );
136
+ `);
137
+ }
138
+
83
139
  // ── Profile helpers ────────────────────────────────────────────────────────
84
140
  function setProfile(key, value) {
85
141
  try {
@@ -94,6 +150,19 @@ function setProfile(key, value) {
94
150
  }
95
151
  }
96
152
 
153
+ function deleteProfile(key) {
154
+ try {
155
+ getDb().prepare('DELETE FROM user_profile WHERE key = ?').run(key);
156
+ } catch (err) {
157
+ console.error('[Memory] deleteProfile error:', err.message);
158
+ }
159
+ }
160
+
161
+ function clearConversationScopedProfile() {
162
+ deleteProfile('preferred_language');
163
+ clearResponseCache();
164
+ }
165
+
97
166
  function getProfile(key, defaultValue = null) {
98
167
  try {
99
168
  const row = getDb().prepare('SELECT value FROM user_profile WHERE key = ?').get(key);
@@ -166,6 +235,116 @@ function getTopPatterns(limit = 8) {
166
235
  }
167
236
  }
168
237
 
238
+ const MAX_INTERACTION_MEMORIES = 1000;
239
+
240
+ function stripRelevantMemoryBlock(text) {
241
+ return String(text || '')
242
+ .replace(/\n?\[Relevant long-term memory for this user message\][\s\S]*?\[End relevant memory\]\n?/g, '\n')
243
+ .replace(/^\s*\[Relevant long-term memory for this user message\][\s\S]*?\[End relevant memory\]\s*/g, '')
244
+ .replace(/\n?\[LOCAL KNOWLEDGE BASE - USE THIS CONTEXT TO ANSWER\][\s\S]*/g, '')
245
+ .trim();
246
+ }
247
+
248
+ function addInteractionMemory(userMessage, aiResponseText, keywords = []) {
249
+ try {
250
+ const db = getDb();
251
+ db.prepare(`
252
+ INSERT INTO interaction_memories (user_text, ai_text, keywords)
253
+ VALUES (?, ?, ?)
254
+ `).run(
255
+ String(userMessage || '').slice(0, 1200),
256
+ String(aiResponseText || '').slice(0, 1200),
257
+ keywords.join(',')
258
+ );
259
+ db.exec(`
260
+ DELETE FROM interaction_memories WHERE id NOT IN (
261
+ SELECT id FROM interaction_memories ORDER BY id DESC LIMIT ${MAX_INTERACTION_MEMORIES}
262
+ )
263
+ `);
264
+ } catch (err) {
265
+ console.error('[Memory] addInteractionMemory error:', err.message);
266
+ }
267
+ }
268
+
269
+ function getRecentInteractions(limit = 5) {
270
+ try {
271
+ return getDb()
272
+ .prepare('SELECT id, user_text, ai_text, keywords, created_at FROM interaction_memories ORDER BY id DESC LIMIT ?')
273
+ .all(limit);
274
+ } catch (_) {
275
+ return [];
276
+ }
277
+ }
278
+
279
+ function deleteInteractionMemory(id) {
280
+ try {
281
+ const result = getDb().prepare('DELETE FROM interaction_memories WHERE id = ?').run(id);
282
+ return result.changes > 0;
283
+ } catch (err) {
284
+ console.error('[Memory] deleteInteractionMemory error:', err.message);
285
+ return false;
286
+ }
287
+ }
288
+
289
+ function searchInteractions(query, limit = 8) {
290
+ try {
291
+ const keywords = extractKeywords(query);
292
+ const terms = keywords.length > 0 ? keywords : [String(query || '').trim()].filter(Boolean);
293
+ if (terms.length === 0) return [];
294
+
295
+ const rows = [];
296
+ const seen = new Set();
297
+ const stmt = getDb().prepare(`
298
+ SELECT id, user_text, ai_text, keywords, created_at
299
+ FROM interaction_memories
300
+ WHERE user_text LIKE ? OR ai_text LIKE ? OR keywords LIKE ?
301
+ ORDER BY id DESC
302
+ LIMIT ?
303
+ `);
304
+
305
+ for (const term of terms.slice(0, 5)) {
306
+ const like = `%${term}%`;
307
+ for (const row of stmt.all(like, like, like, limit)) {
308
+ if (!seen.has(row.id)) {
309
+ seen.add(row.id);
310
+ rows.push(row);
311
+ if (rows.length >= limit) return rows;
312
+ }
313
+ }
314
+ }
315
+ return rows;
316
+ } catch (_) {
317
+ return [];
318
+ }
319
+ }
320
+
321
+ function clearInteractionMemories() {
322
+ try {
323
+ getDb().prepare('DELETE FROM interaction_memories').run();
324
+ } catch (err) {
325
+ console.error('[Memory] clearInteractionMemories error:', err.message);
326
+ }
327
+ }
328
+
329
+ function exportMemorySnapshot() {
330
+ try {
331
+ return {
332
+ profile: getAllProfile(),
333
+ session_memories: getRecentMemories(MAX_SESSION_MEMORIES),
334
+ usage_patterns: getTopPatterns(50),
335
+ interaction_memories: getRecentInteractions(MAX_INTERACTION_MEMORIES)
336
+ };
337
+ } catch (err) {
338
+ console.error('[Memory] exportMemorySnapshot error:', err.message);
339
+ return {
340
+ profile: {},
341
+ session_memories: [],
342
+ usage_patterns: [],
343
+ interaction_memories: []
344
+ };
345
+ }
346
+ }
347
+
169
348
  // ── Simple keyword extractor (no external deps) ────────────────────────────
170
349
  const STOP_WORDS = new Set([
171
350
  'ที่', 'ให้', 'และ', 'ของ', 'กับ', 'ใน', 'บน', 'เป็น', 'อยู่', 'มี', 'ได้', 'the', 'a', 'an',
@@ -183,6 +362,37 @@ function extractKeywords(text) {
183
362
  .slice(0, 6);
184
363
  }
185
364
 
365
+ function cleanProfileValue(value) {
366
+ return String(value || '')
367
+ .replace(/[.,!?;:()[\]{}"'`“”‘’]+$/g, '')
368
+ .replace(/(นะ|น่ะ|ครับ|ค่ะ|คะ|จ้า|จ๊ะ|ฮะ|ค้าบ|ค่า)+$/u, '')
369
+ .trim();
370
+ }
371
+
372
+ function extractUserName(text) {
373
+ const input = String(text || '').trim();
374
+ const patterns = [
375
+ /(?:ผม|ฉัน|ชั้น|หนู|เรา|ข้า|ดิฉัน)?\s*ชื่อ(?:เล่น)?\s*(?:คือ|ว่า|เป็น)?\s*([A-Za-z\u0E00-\u0E7F][A-Za-z\u0E00-\u0E7F\s]{0,40})/iu,
376
+ /(?:เรียก(?:ผม|ฉัน|ชั้น|หนู|เรา)?ว่า)\s*([A-Za-z\u0E00-\u0E7F][A-Za-z\u0E00-\u0E7F\s]{0,40})/iu,
377
+ /\bmy name is\s+([A-Za-z][A-Za-z\s'-]{0,40})/iu,
378
+ /\bcall me\s+([A-Za-z][A-Za-z\s'-]{0,40})/iu,
379
+ /\bi am\s+([A-Za-z][A-Za-z\s'-]{0,40})/iu,
380
+ /\bi'm\s+([A-Za-z][A-Za-z\s'-]{0,40})/iu
381
+ ];
382
+
383
+ for (const pattern of patterns) {
384
+ const match = input.match(pattern);
385
+ if (match && match[1]) {
386
+ const name = cleanProfileValue(match[1])
387
+ .split(/\s+(?:and|แล้ว|นะ|ครับ|ค่ะ|คะ)\s+/i)[0]
388
+ .trim();
389
+ if (name && name.length <= 40) return name;
390
+ }
391
+ }
392
+
393
+ return '';
394
+ }
395
+
186
396
  // ── Main public API ────────────────────────────────────────────────────────
187
397
 
188
398
  /**
@@ -196,12 +406,18 @@ function recordInteraction(userMessage, aiResponseText) {
196
406
  // Extract keywords as usage patterns
197
407
  const keywords = extractKeywords(userMessage);
198
408
  keywords.forEach(kw => recordPattern(kw));
409
+ addInteractionMemory(userMessage, aiResponseText, keywords);
199
410
 
200
411
  // Detect preferred language
201
412
  const thaiRatio = (userMessage.match(/[\u0E00-\u0E7F]/g) || []).length / userMessage.length;
202
413
  if (thaiRatio > 0.3) setProfile('preferred_language', 'thai');
203
414
  else setProfile('preferred_language', 'english');
204
415
 
416
+ const userName = extractUserName(userMessage);
417
+ if (userName) {
418
+ setProfile('user_name', userName);
419
+ }
420
+
205
421
  // Detect coding intent (update project activity)
206
422
  const codingKeywords = ['code', 'fix', 'debug', 'function', 'class', 'import', 'script',
207
423
  'แก้', 'เขียน', 'โค้ด', 'สคริปต์', 'ฟังก์ชัน'];
@@ -231,22 +447,88 @@ function saveSessionSummary(summary, tags = []) {
231
447
  addSessionMemory(summary.trim(), tags);
232
448
  }
233
449
 
450
+ function addLearnedSkill(name, sourcePath, content) {
451
+ const cleanName = String(name || '').trim() || path.basename(sourcePath || 'skill.md');
452
+ const cleanPath = path.resolve(String(sourcePath || ''));
453
+ const cleanContent = String(content || '').trim();
454
+ if (!cleanContent) {
455
+ throw new Error('Skill file is empty.');
456
+ }
457
+
458
+ const storedContent = cleanContent.slice(0, 12000);
459
+ ensureLearnedSkillsTable();
460
+ const db = getDb();
461
+ db.prepare(`
462
+ INSERT INTO learned_skills (name, source_path, content, created_at, updated_at)
463
+ VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
464
+ ON CONFLICT(source_path) DO UPDATE SET
465
+ name = excluded.name,
466
+ content = excluded.content,
467
+ updated_at = CURRENT_TIMESTAMP
468
+ `).run(cleanName, cleanPath, storedContent);
469
+
470
+ return {
471
+ name: cleanName,
472
+ source_path: cleanPath,
473
+ content_length: cleanContent.length,
474
+ stored_length: storedContent.length
475
+ };
476
+ }
477
+
478
+ function getLearnedSkills(limit = 10) {
479
+ try {
480
+ ensureLearnedSkillsTable();
481
+ return getDb().prepare(`
482
+ SELECT id, name, source_path, content, created_at, updated_at
483
+ FROM learned_skills
484
+ ORDER BY updated_at DESC, id DESC
485
+ LIMIT ?
486
+ `).all(limit);
487
+ } catch (err) {
488
+ console.error('[Memory] getLearnedSkills error:', err.message);
489
+ return [];
490
+ }
491
+ }
492
+
493
+ function deleteLearnedSkill(identifier) {
494
+ try {
495
+ ensureLearnedSkillsTable();
496
+ const input = String(identifier || '').trim();
497
+ if (!input) return 0;
498
+
499
+ const db = getDb();
500
+ if (/^\d+$/.test(input)) {
501
+ return db.prepare('DELETE FROM learned_skills WHERE id = ?').run(Number(input)).changes;
502
+ }
503
+
504
+ const resolved = path.resolve(input);
505
+ return db.prepare('DELETE FROM learned_skills WHERE source_path = ? OR name = ?').run(resolved, input).changes;
506
+ } catch (err) {
507
+ console.error('[Memory] deleteLearnedSkill error:', err.message);
508
+ return 0;
509
+ }
510
+ }
511
+
234
512
  /**
235
513
  * Returns a formatted context string to inject into the AI system prompt.
236
514
  * Lightweight — no async calls.
237
515
  */
238
- function getUserContext() {
516
+ function getUserContext(query = '') {
239
517
  try {
240
518
  const profile = getAllProfile();
241
519
  const patterns = getTopPatterns(6);
242
520
  const memories = getRecentMemories(3);
521
+ const interactions = getRecentInteractions(6);
522
+ const relevantInteractions = query ? searchInteractions(query, 5) : [];
243
523
 
244
524
  const lines = ['\n\n[LONG-TERM USER CONTEXT — use this to personalize responses]'];
245
525
 
246
526
  // Profile info
247
527
  if (Object.keys(profile).length > 0) {
528
+ if (profile.user_name)
529
+ lines.push(`• User name: ${profile.user_name}`);
248
530
  if (profile.preferred_language)
249
- lines.push(`• Preferred language: ${profile.preferred_language}`);
531
+ lines.push(`• Previously inferred language: ${profile.preferred_language} (do not override the current user message language)`);
250
532
  if (profile.last_active_project)
251
533
  lines.push(`• Last active project: ${profile.last_active_project} (${profile.last_active_project_path || ''})`);
252
534
  if (profile.total_interactions)
@@ -269,6 +551,36 @@ function getUserContext() {
269
551
  memories.forEach((m, i) => lines.push(` ${i + 1}. ${m.summary}`));
270
552
  }
271
553
 
554
+ if (interactions.length > 0) {
555
+ lines.push('\nRecent remembered interactions:');
556
+ interactions.forEach((m, i) => {
557
+ lines.push(` ${i + 1}. User: ${m.user_text}`);
558
+ lines.push(` Mint: ${m.ai_text}`);
559
+ });
560
+ }
561
+
562
+ if (relevantInteractions.length > 0) {
563
+ lines.push('\nRelevant remembered interactions for the current request:');
564
+ relevantInteractions.forEach((m, i) => {
565
+ lines.push(` ${i + 1}. User: ${m.user_text}`);
566
+ lines.push(` Mint: ${m.ai_text}`);
567
+ });
568
+ }
569
+
570
+ const learnedSkills = getLearnedSkills(8);
571
+ if (learnedSkills.length > 0) {
572
+ lines.push('\nLearned skill/instruction files:');
573
+ learnedSkills.forEach((skill, i) => {
574
+ lines.push(`\n ${i + 1}. ${skill.name}`);
575
+ lines.push(` Source: ${skill.source_path}`);
576
+ lines.push(' Content:');
577
+ lines.push(skill.content
578
+ .split('\n')
579
+ .map(line => ` ${line}`)
580
+ .join('\n'));
581
+ });
582
+ }
583
+
272
584
  if (lines.length === 1) return ''; // nothing to add
273
585
  lines.push('[END USER CONTEXT]\n');
274
586
  return lines.join('\n');
@@ -287,7 +599,11 @@ function getCachedResponse(query) {
287
599
  // Optional: check TTL (e.g., 24 hours)
288
600
  const age = Date.now() - new Date(row.created_at).getTime();
289
601
  if (age < 24 * 60 * 60 * 1000) {
290
- return JSON.parse(row.response);
602
+ const parsed = JSON.parse(row.response);
603
+ if (parsed && typeof parsed.response === 'string') {
604
+ parsed.response = stripRelevantMemoryBlock(parsed.response);
605
+ }
606
+ return parsed;
291
607
  }
292
608
  }
293
609
  } catch (_) {}
@@ -297,22 +613,50 @@ function getCachedResponse(query) {
297
613
  function cacheResponse(query, responseObj) {
298
614
  try {
299
615
  const hash = crypto.createHash('md5').update(query.trim().toLowerCase()).digest('hex');
616
+ const sanitized = (responseObj && typeof responseObj === 'object')
617
+ ? {
618
+ ...responseObj,
619
+ response: typeof responseObj.response === 'string'
620
+ ? stripRelevantMemoryBlock(responseObj.response)
621
+ : responseObj.response
622
+ }
623
+ : responseObj;
300
624
  getDb().prepare(`
301
625
  INSERT INTO response_cache (query_hash, response, created_at)
302
626
  VALUES (?, ?, CURRENT_TIMESTAMP)
303
627
  ON CONFLICT(query_hash) DO UPDATE SET response = excluded.response, created_at = CURRENT_TIMESTAMP
304
- `).run(hash, JSON.stringify(responseObj));
628
+ `).run(hash, JSON.stringify(sanitized));
305
629
  } catch (_) {}
306
630
  }
307
631
 
632
+ function clearResponseCache() {
633
+ try {
634
+ getDb().prepare('DELETE FROM response_cache').run();
635
+ } catch (err) {
636
+ console.error('[Memory] clearResponseCache error:', err.message);
637
+ }
638
+ }
639
+
308
640
  module.exports = {
309
641
  recordInteraction,
310
642
  saveSessionSummary,
311
643
  getUserContext,
312
644
  setProfile,
645
+ deleteProfile,
646
+ clearConversationScopedProfile,
313
647
  getProfile,
648
+ getAllProfile,
649
+ addLearnedSkill,
650
+ getLearnedSkills,
651
+ deleteLearnedSkill,
314
652
  getTopPatterns,
653
+ getRecentInteractions,
654
+ searchInteractions,
655
+ deleteInteractionMemory,
656
+ clearInteractionMemories,
657
+ exportMemorySnapshot,
315
658
  getRecentMemories,
316
659
  getCachedResponse,
317
- cacheResponse
660
+ cacheResponse,
661
+ clearResponseCache
318
662
  };
@@ -226,7 +226,7 @@ async function openFile(target) {
226
226
  // ใช้ exec เพื่อให้รันผ่าน shell และรองรับการทำ fallback
227
227
  let cmd = `${platformCmd} "${resolvedPath}"`;
228
228
  if (process.platform === 'linux') {
229
- cmd = `xdg-open "${resolvedPath}" || gio open "${resolvedPath}" || nautilus "${resolvedPath}"`;
229
+ cmd = `xdg-open "${resolvedPath}" || gio open "${resolvedPath}" || nautilus "${resolvedPath}" || nemo "${resolvedPath}" || thunar "${resolvedPath}" || dolphin "${resolvedPath}"`;
230
230
  }
231
231
 
232
232
  exec(cmd, (err) => {