@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.
- package/GUIDE_TH.md +113 -0
- package/README.md +267 -78
- package/assets/CLI_Screen.png +0 -0
- package/main.js +76 -890
- package/mint-cli-logic.js +3 -107
- package/mint-cli.js +594 -29
- package/models/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +14 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +10 -0
- 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
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json +50 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json +1498 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3 +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +47 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json +6658 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json +1299 -0
- 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
- package/package.json +37 -4
- package/src/AI_Brain/Gemini_API.js +223 -65
- package/src/AI_Brain/autonomous_brain.js +11 -0
- package/src/AI_Brain/behavior_memory.js +26 -5
- package/src/AI_Brain/headless_agent.js +4 -0
- package/src/AI_Brain/knowledge_base.js +61 -8
- package/src/AI_Brain/memory_store.js +354 -10
- package/src/Automation_Layer/file_operations.js +1 -1
- package/src/CLI/chat_router.js +20 -7
- package/src/CLI/chat_ui.js +596 -825
- package/src/CLI/code_agent.js +347 -56
- package/src/CLI/gmail_auth.js +210 -0
- package/src/CLI/image_input.js +90 -0
- package/src/CLI/list_features.js +2 -0
- package/src/CLI/onboarding.js +364 -55
- package/src/CLI/updater.js +210 -0
- package/src/Channels/brave_search_bridge.js +35 -0
- package/src/Channels/discord_bridge.js +68 -0
- package/src/Channels/google_search_bridge.js +38 -0
- package/src/Channels/line_bridge.js +60 -0
- package/src/Channels/slack_bridge.js +53 -0
- package/src/Channels/telegram_bridge.js +49 -0
- package/src/Channels/whatsapp_bridge.js +55 -0
- package/src/Command_Parser/parser.js +12 -1
- package/src/Plugins/gmail.js +251 -0
- package/src/Plugins/google_calendar.js +245 -19
- package/src/Plugins/notion.js +256 -0
- package/src/System/action_executor.js +178 -0
- package/src/System/bridge_manager.js +76 -0
- package/src/System/chat_history_manager.js +23 -5
- package/src/System/config_manager.js +71 -7
- package/src/System/custom_workflows.js +31 -2
- package/src/System/google_tts_urls.js +51 -0
- package/src/System/granular_automation.js +122 -53
- package/src/System/ipc_handlers.js +238 -0
- package/src/System/proactive_loop.js +153 -0
- package/src/System/safety_manager.js +273 -0
- package/src/System/sandbox_runner.js +182 -0
- package/src/System/screen_capture.js +175 -0
- package/src/System/system_automation.js +127 -81
- package/src/System/system_info.js +70 -0
- package/src/System/task_manager.js +15 -5
- package/src/System/tool_registry.js +280 -0
- package/src/System/window_manager.js +212 -0
- package/src/UI/live2d_manager.js +368 -0
- package/src/UI/renderer.js +208 -24
- package/src/UI/settings.html +24 -0
- package/src/UI/settings.js +14 -4
- package/src/UI/styles.css +466 -32
- package/.codex +0 -0
- package/docs/assets/Agent_Mint.png +0 -0
- package/docs/assets/CLI_Screen.png +0 -0
- package/docs/assets/Settings.png +0 -0
- package/docs/assets/icon.png +0 -0
- package/docs/index.html +0 -132
- package/docs/style.css +0 -579
- package/index.html +0 -16
- package/src/UI/index.html +0 -126
- package/tech_news.txt +0 -3
- package/test_knowledge.txt +0 -3
- package/tests/agent_orchestrator.test.js +0 -41
- package/tests/chat_router.test.js +0 -42
- package/tests/code_agent.test.js +0 -69
- package/tests/config_manager.test.js +0 -141
- package/tests/docker.test.js +0 -46
- package/tests/file_operations.test.js +0 -57
- package/tests/memory_store.test.js +0 -185
- package/tests/provider_routing.test.js +0 -67
- package/tests/spotify.test.js +0 -201
- package/tests/system_monitor.test.js +0 -37
- 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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
|
158
|
-
content =
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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(`•
|
|
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
|
-
|
|
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(
|
|
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) => {
|