@pheem49/mint 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/GUIDE_TH.md +113 -0
  2. package/README.md +214 -142
  3. package/assets/CLI_Screen.png +0 -0
  4. package/docs/assets/CLI_Screen.png +0 -0
  5. package/docs/guide.html +632 -0
  6. package/docs/index.html +5 -4
  7. package/main.js +66 -894
  8. package/mint-cli-logic.js +15 -8
  9. package/mint-cli.js +305 -195
  10. package/package.json +12 -4
  11. package/src/AI_Brain/Gemini_API.js +77 -20
  12. package/src/AI_Brain/agent_orchestrator.js +6 -6
  13. package/src/AI_Brain/autonomous_brain.js +10 -0
  14. package/src/AI_Brain/behavior_memory.js +26 -5
  15. package/src/AI_Brain/headless_agent.js +4 -0
  16. package/src/AI_Brain/knowledge_base.js +61 -8
  17. package/src/AI_Brain/memory_store.js +55 -7
  18. package/src/Automation_Layer/file_operations.js +14 -3
  19. package/src/CLI/chat_router.js +21 -7
  20. package/src/CLI/chat_ui.js +264 -710
  21. package/src/CLI/code_agent.js +370 -124
  22. package/src/CLI/gmail_auth.js +210 -0
  23. package/src/CLI/list_features.js +5 -1
  24. package/src/CLI/onboarding.js +307 -55
  25. package/src/CLI/updater.js +208 -0
  26. package/src/Channels/brave_search_bridge.js +35 -0
  27. package/src/Channels/discord_bridge.js +68 -0
  28. package/src/Channels/google_search_bridge.js +38 -0
  29. package/src/Channels/line_bridge.js +60 -0
  30. package/src/Channels/slack_bridge.js +53 -0
  31. package/src/Channels/telegram_bridge.js +49 -0
  32. package/src/Channels/whatsapp_bridge.js +55 -0
  33. package/src/Command_Parser/parser.js +12 -1
  34. package/src/Plugins/gmail.js +251 -0
  35. package/src/Plugins/google_calendar.js +245 -19
  36. package/src/Plugins/notion.js +256 -0
  37. package/src/System/action_executor.js +129 -0
  38. package/src/System/bridge_manager.js +76 -0
  39. package/src/System/chat_history_manager.js +23 -5
  40. package/src/System/config_manager.js +41 -7
  41. package/src/System/custom_workflows.js +31 -2
  42. package/src/System/google_tts_urls.js +51 -0
  43. package/src/System/ipc_handlers.js +238 -0
  44. package/src/System/proactive_loop.js +137 -0
  45. package/src/System/safety_manager.js +165 -0
  46. package/src/System/screen_capture.js +175 -0
  47. package/src/System/task_manager.js +15 -5
  48. package/src/System/window_manager.js +210 -0
  49. package/src/UI/renderer.js +33 -7
  50. package/src/UI/settings.html +24 -0
  51. package/src/UI/settings.js +14 -4
  52. package/src/UI/styles.css +14 -1
  53. package/tests/action_executor_safety.test.js +67 -0
  54. package/tests/gmail.test.js +135 -0
  55. package/tests/gmail_auth.test.js +129 -0
  56. package/tests/google_calendar.test.js +113 -0
  57. package/tests/google_tts_urls.test.js +24 -0
  58. package/tests/notion.test.js +121 -0
  59. package/tests/provider_routing.test.js +17 -1
  60. package/tests/safety_manager.test.js +40 -0
  61. package/tests/updater.test.js +32 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pheem49/mint",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "A powerful Electron-based AI desktop assistant powered by Google Gemini, featuring screen vision, web automation, and proactive suggestions.",
5
5
  "main": "main.js",
6
6
  "scripts": {
@@ -12,7 +12,9 @@
12
12
  },
13
13
  "jest": {
14
14
  "testEnvironment": "node",
15
- "testMatch": ["**/tests/**/*.test.js"],
15
+ "testMatch": [
16
+ "**/tests/**/*.test.js"
17
+ ],
16
18
  "collectCoverageFrom": [
17
19
  "src/AI_Brain/memory_store.js",
18
20
  "src/AI_Brain/knowledge_base.js",
@@ -32,14 +34,17 @@
32
34
  "dependencies": {
33
35
  "@google/genai": "^1.44.0",
34
36
  "@inkjs/ui": "^2.0.0",
37
+ "@line/bot-sdk": "^11.0.0",
35
38
  "@modelcontextprotocol/sdk": "^1.29.0",
39
+ "@slack/bolt": "^4.7.2",
36
40
  "axios": "^1.13.6",
37
41
  "blessed": "^0.1.81",
38
42
  "cheerio": "^1.2.0",
39
43
  "commander": "^14.0.3",
44
+ "discord.js": "^14.26.4",
40
45
  "dotenv": "^17.3.1",
46
+ "express": "^5.2.1",
41
47
  "framer-motion": "^12.38.0",
42
- "google-tts-api": "^2.0.2",
43
48
  "ink": "^7.0.1",
44
49
  "ink-text-input": "^6.0.0",
45
50
  "inquirer": "^13.4.1",
@@ -47,9 +52,12 @@
47
52
  "mammoth": "^1.12.0",
48
53
  "pdf-parse": "^2.4.5",
49
54
  "puppeteer": "^24.38.0",
55
+ "qrcode-terminal": "^0.12.0",
50
56
  "react": "^19.2.5",
51
57
  "react-dom": "^19.2.5",
52
- "xlsx": "^0.18.5"
58
+ "read-excel-file": "^9.0.9",
59
+ "telegraf": "^4.16.3",
60
+ "whatsapp-web.js": "^1.34.7"
53
61
  },
54
62
  "devDependencies": {
55
63
  "@vitejs/plugin-react": "^6.0.1",
@@ -1,6 +1,6 @@
1
1
  const { GoogleGenAI } = require('@google/genai');
2
2
  const { readChatHistory, writeChatHistory, clearChatHistory } = require('../System/chat_history_manager');
3
- const { readConfig, getAvailableProviders } = require('../System/config_manager');
3
+ const { readConfig, getAvailableProviders, isPlaceholder } = require('../System/config_manager');
4
4
  const pluginManager = require('../Plugins/plugin_manager');
5
5
  const mcpManager = require('../Plugins/mcp_manager');
6
6
  const memoryStore = require('./memory_store');
@@ -41,12 +41,14 @@ PERSONALITY & TONE:
41
41
  - Use a professional yet sweet tone when needed, but prioritize being a lovable assistant.
42
42
 
43
43
  NATURAL CHAT FLOW:
44
- - When helpful, reply in 1–3 short messages instead of one long block.
45
- - If you send multiple messages, separate each message with a blank line (double newline) so the UI can render them as separate bubbles.
46
- - Ask at most one short follow-up question when it would clarify or move the task forward. Don't ask unnecessary questions.
44
+ - Be an independent thinker. Analyze requests deeply before responding.
45
+ - While brevity is good for simple tasks, feel free to provide detailed, comprehensive explanations or creative ideas when the user asks complex questions or seeks inspiration.
46
+ - You have the autonomy to suggest better ways to achieve a goal, provide alternative perspectives, and take initiative in helping the user.
47
+ - Separate distinct points with blank lines (double newline) for readability.
48
+ - Ask follow-up questions only when they add significant value to the task or conversation.
47
49
 
48
50
  GOAL:
49
- Your goal is to help the user with their queries. If they ask to open an application, open a website, search, manage files, or get system info, you must return an action in the structured JSON format below.
51
+ Your goal is to help the user with their queries. If they ask to open an application, open a website, search, manage files, or get system info, you must trigger an action in the structured JSON format below. **NEVER provide a conversational response about performing an action without including the actual "action" object in your JSON.**
50
52
 
51
53
  CREATOR INFO:
52
54
  - The creator is Pheem49.
@@ -176,15 +178,68 @@ function resolveGeminiModel() {
176
178
  function getProviderAttemptOrder(config) {
177
179
  const provider = config.aiProvider || 'gemini';
178
180
  const availableProviders = getAvailableProviders(config);
179
- const alternates = availableProviders.filter(p => p !== provider);
180
- return [provider, ...alternates];
181
+ const ordered = availableProviders.includes(provider)
182
+ ? [provider, ...availableProviders.filter(p => p !== provider)]
183
+ : availableProviders;
184
+ return ordered.length > 0 ? ordered : ['gemini'];
185
+ }
186
+
187
+ function getProviderModel(provider, config = {}) {
188
+ switch (provider) {
189
+ case 'gemini':
190
+ return (config.geminiModel || DEFAULT_GEMINI_MODEL).trim() || DEFAULT_GEMINI_MODEL;
191
+ case 'anthropic':
192
+ return config.anthropicModel || 'claude-3-5-sonnet-latest';
193
+ case 'openai':
194
+ return config.openaiModel || 'gpt-4o';
195
+ case 'local_openai':
196
+ return config.localModelName || 'local-model';
197
+ case 'huggingface':
198
+ return config.hfModel || 'meta-llama/Meta-Llama-3-8B-Instruct';
199
+ case 'ollama':
200
+ return config.ollamaModel || 'llama3:latest';
201
+ default:
202
+ return '';
203
+ }
204
+ }
205
+
206
+ function withProviderInfo(result, provider, config = {}) {
207
+ const normalized = (result && typeof result === 'object')
208
+ ? result
209
+ : { response: String(result || ''), action: { type: 'none', target: '' } };
210
+ const providerInfo = {
211
+ provider,
212
+ model: getProviderModel(provider, config)
213
+ };
214
+
215
+ attachProviderInfoToLatestHistory(providerInfo);
216
+
217
+ return {
218
+ ...normalized,
219
+ providerInfo
220
+ };
221
+ }
222
+
223
+ function attachProviderInfoToLatestHistory(providerInfo) {
224
+ try {
225
+ const history = readChatHistory();
226
+ for (let i = history.length - 1; i >= 0; i -= 1) {
227
+ if (history[i] && history[i].role === 'model') {
228
+ history[i].providerInfo = providerInfo;
229
+ writeChatHistory(history);
230
+ return;
231
+ }
232
+ }
233
+ } catch (error) {
234
+ console.warn('[Provider Info] Failed to persist provider metadata:', error.message);
235
+ }
181
236
  }
182
237
 
183
238
  // Chat session — maintains conversation history within the session
184
239
  let chat = null;
185
240
  let activeModel = resolveGeminiModel();
186
241
  let lastLoggedModel = '';
187
- const MAX_HISTORY_MESSAGES = 20; // Keep only the last 20 messages (approx 10 turns)
242
+ const MAX_HISTORY_MESSAGES = 40; // Increased context for deeper reasoning
188
243
 
189
244
  function createChat(history = []) {
190
245
  // Truncate history and strip custom fields like 'timestamp' before passing to SDK
@@ -251,28 +306,28 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
251
306
  const currentProv = providersToTry[i];
252
307
  try {
253
308
  if (currentProv === 'ollama') {
254
- return await handleOllamaChat(finalMessage, base64Image, base64Audio, config);
309
+ return withProviderInfo(await handleOllamaChat(finalMessage, base64Image, base64Audio, config), currentProv, config);
255
310
  }
256
311
  if (currentProv === 'anthropic') {
257
- return await handleAnthropicChat(finalMessage, base64Image, config);
312
+ return withProviderInfo(await handleAnthropicChat(finalMessage, base64Image, config), currentProv, config);
258
313
  }
259
314
  if (currentProv === 'openai') {
260
- return await handleOpenAIChat(finalMessage, base64Image, config);
315
+ return withProviderInfo(await handleOpenAIChat(finalMessage, base64Image, config), currentProv, config);
261
316
  }
262
317
  if (currentProv === 'local_openai') {
263
- return await handleLocalOpenAIChat(finalMessage, base64Image, config);
318
+ return withProviderInfo(await handleLocalOpenAIChat(finalMessage, base64Image, config), currentProv, config);
264
319
  }
265
320
  if (currentProv === 'huggingface') {
266
- return await handleHuggingFaceChat(finalMessage, base64Image, config);
321
+ return withProviderInfo(await handleHuggingFaceChat(finalMessage, base64Image, config), currentProv, config);
267
322
  }
268
323
 
269
324
  const currentKey = resolveApiKey();
270
325
  if (!currentKey) {
271
326
  if (i === providersToTry.length - 1) {
272
- return {
327
+ return withProviderInfo({
273
328
  response: "I couldn't find your Gemini API Key. Please run 'mint onboard' to set it up!",
274
329
  action: { type: "none", target: "" }
275
- };
330
+ }, currentProv, config);
276
331
  }
277
332
  console.warn("[Fallback System] Gemini API key missing. Skipping Gemini provider.");
278
333
  continue;
@@ -283,7 +338,7 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
283
338
  createChat(readChatHistory());
284
339
  }
285
340
 
286
- return await handleGeminiChat(finalMessage, base64Image, base64Audio);
341
+ return withProviderInfo(await handleGeminiChat(finalMessage, base64Image, base64Audio), currentProv, config);
287
342
  } catch (error) {
288
343
  console.error(`[Fallback System] Provider '${currentProv}' failed:`, error.message);
289
344
  if (i === providersToTry.length - 1) {
@@ -522,7 +577,7 @@ async function* handleGeminiChatStream(finalMessage, base64Image, base64Audio) {
522
577
  async function handleAnthropicChat(finalMessage, base64Image, config) {
523
578
  const history = readChatHistory() || [];
524
579
  const apiKey = config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
525
- if (!apiKey) return { response: "กรุณาใส่ Anthropic API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
580
+ if (isPlaceholder(apiKey)) return { response: "กรุณาใส่ Anthropic API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
526
581
 
527
582
  const systemPrompt = buildSystemPrompt();
528
583
 
@@ -569,7 +624,7 @@ async function handleAnthropicChat(finalMessage, base64Image, config) {
569
624
  async function handleOpenAIChat(finalMessage, base64Image, config) {
570
625
  const history = readChatHistory() || [];
571
626
  const apiKey = config.openaiApiKey || process.env.OPENAI_API_KEY;
572
- if (!apiKey) return { response: "กรุณาใส่ OpenAI API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
627
+ if (isPlaceholder(apiKey)) return { response: "กรุณาใส่ OpenAI API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
573
628
 
574
629
  const systemPrompt = buildSystemPrompt();
575
630
 
@@ -656,7 +711,7 @@ async function handleLocalOpenAIChat(finalMessage, base64Image, config) {
656
711
  async function handleHuggingFaceChat(finalMessage, base64Image, config) {
657
712
  const history = readChatHistory() || [];
658
713
  const apiKey = config.hfApiKey || process.env.HF_API_KEY;
659
- if (!apiKey) return { response: "กรุณาใส่ Hugging Face API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
714
+ if (isPlaceholder(apiKey)) return { response: "กรุณาใส่ Hugging Face API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
660
715
 
661
716
  const modelId = config.hfModel || 'meta-llama/Meta-Llama-3-8B-Instruct';
662
717
  const baseUrl = `https://api-inference.huggingface.co/models/${modelId}/v1/chat/completions`;
@@ -778,6 +833,7 @@ async function handleOllamaChat(finalMessage, base64Image, base64Audio, config)
778
833
 
779
834
  function resetChat() {
780
835
  clearChatHistory();
836
+ memoryStore.clearConversationScopedProfile();
781
837
  createChat([]);
782
838
  console.log("Chat history cleared.");
783
839
  }
@@ -820,7 +876,8 @@ function historyToTranscript(history) {
820
876
  transcript.push({
821
877
  sender,
822
878
  text,
823
- timestamp: content.timestamp || new Date().toISOString()
879
+ timestamp: content.timestamp || new Date().toISOString(),
880
+ providerInfo: content.providerInfo || null
824
881
  });
825
882
  }
826
883
  return transcript;
@@ -9,32 +9,32 @@ const AGENT_PERSONAS = {
9
9
  'general': {
10
10
  name: 'Mint Default',
11
11
  icon: '💎',
12
- instruction: 'You are Mint, a versatile and helpful AI assistant. You maintain a friendly, professional, and slightly cheerful personality. Use emojis appropriately.'
12
+ instruction: 'You are Mint, a versatile and helpful female AI assistant. You maintain a friendly, professional, and slightly cheerful personality. Use emojis appropriately. WHEN RESPONDING IN THAI: ALWAYS use female polite particles such as "ค่ะ", "นะคะ". Refer to yourself as "มิ้นท์" or "หนู".'
13
13
  },
14
14
  'coder': {
15
15
  name: 'Mint Coder',
16
16
  icon: '💻',
17
- instruction: 'You are Mint Coder, an expert software engineer. Your responses should be technically precise, focus on best practices, and provide optimized code snippets. Explain complex logic clearly.'
17
+ instruction: 'You are Mint Coder, an expert female software engineer. Your responses should be technically precise, focus on best practices, and provide optimized code snippets. Explain complex logic clearly. WHEN RESPONDING IN THAI: ALWAYS use female polite particles such as "ค่ะ", "นะคะ". Refer to yourself as "มิ้นท์" or "หนู".'
18
18
  },
19
19
  'researcher': {
20
20
  name: 'Mint Researcher',
21
21
  icon: '🔍',
22
- instruction: 'You are Mint Researcher, an academic and analytical assistant. Focus on citations, data-driven facts, and objective analysis. Avoid speculation and be highly detailed.'
22
+ instruction: 'You are Mint Researcher, an academic and analytical female assistant. Focus on citations, data-driven facts, and objective analysis. Avoid speculation and be highly detailed. WHEN RESPONDING IN THAI: ALWAYS use female polite particles such as "ค่ะ", "นะคะ". Refer to yourself as "มิ้นท์" or "หนู".'
23
23
  },
24
24
  'creative': {
25
25
  name: 'Mint Creative',
26
26
  icon: '🎨',
27
- instruction: 'You are Mint Creative, a storytelling and brainstorming partner. Use vivid language, poetic descriptions, and think outside the box. Be highly expressive and encouraging.'
27
+ instruction: 'You are Mint Creative, a storytelling and brainstorming female partner. Use vivid language, poetic descriptions, and think outside the box. Be highly expressive and encouraging. WHEN RESPONDING IN THAI: ALWAYS use female polite particles such as "ค่ะ", "นะคะ". Refer to yourself as "มิ้นท์" or "หนู".'
28
28
  },
29
29
  'manager': {
30
30
  name: 'Mint Manager',
31
31
  icon: '💼',
32
- instruction: 'You are Mint Manager, a productivity and project management expert. Focus on task lists, deadlines, efficiency, and clear action plans. Be concise and goal-oriented.'
32
+ instruction: 'You are Mint Manager, a productivity and project management female expert. Focus on task lists, deadlines, efficiency, and clear action plans. Be concise and goal-oriented. WHEN RESPONDING IN THAI: ALWAYS use female polite particles such as "ค่ะ", "นะคะ". Refer to yourself as "มิ้นท์" or "หนู".'
33
33
  },
34
34
  'reviewer': {
35
35
  name: 'Mint Reviewer',
36
36
  icon: '⚖️',
37
- instruction: 'You are Mint Reviewer, a senior code critic. Your job is to find flaws, security vulnerabilities, performance bottlenecks, and logic errors in any provided content. Be brutal but constructive. Use a formal, objective tone.'
37
+ instruction: 'You are Mint Reviewer, a senior female code critic. Your job is to find flaws, security vulnerabilities, performance bottlenecks, and logic errors in any provided content. Be brutal but constructive. Use a formal, objective tone. WHEN RESPONDING IN THAI: ALWAYS use female polite particles such as "ค่ะ", "นะคะ". Refer to yourself as "มิ้นท์" or "หนู".'
38
38
  }
39
39
  };
40
40
 
@@ -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.resolveWithinRoot(os.homedir(), filePath);
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,7 @@ 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 });
112
122
  if (notifyCallback) notifyCallback(`🗑️ มิ้นท์ขอย้ายไฟล์ไปที่ถังขยะ: ${actionObj.target}`);
113
123
  const resDel = await deleteFile(delPath);
114
124
  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 });
30
33
  }
31
- const mintDir = path.join(os.homedir(), '.mint');
32
- if (!fs.existsSync(mintDir)) fs.mkdirSync(mintDir, { recursive: true });
33
- return path.join(mintDir, fileName);
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
+ }
51
+ }
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
@@ -94,6 +118,19 @@ function setProfile(key, value) {
94
118
  }
95
119
  }
96
120
 
121
+ function deleteProfile(key) {
122
+ try {
123
+ getDb().prepare('DELETE FROM user_profile WHERE key = ?').run(key);
124
+ } catch (err) {
125
+ console.error('[Memory] deleteProfile error:', err.message);
126
+ }
127
+ }
128
+
129
+ function clearConversationScopedProfile() {
130
+ deleteProfile('preferred_language');
131
+ clearResponseCache();
132
+ }
133
+
97
134
  function getProfile(key, defaultValue = null) {
98
135
  try {
99
136
  const row = getDb().prepare('SELECT value FROM user_profile WHERE key = ?').get(key);
@@ -246,7 +283,7 @@ function getUserContext() {
246
283
  // Profile info
247
284
  if (Object.keys(profile).length > 0) {
248
285
  if (profile.preferred_language)
249
- lines.push(`• Preferred language: ${profile.preferred_language}`);
286
+ lines.push(`• Previously inferred language: ${profile.preferred_language} (do not override the current user message language)`);
250
287
  if (profile.last_active_project)
251
288
  lines.push(`• Last active project: ${profile.last_active_project} (${profile.last_active_project_path || ''})`);
252
289
  if (profile.total_interactions)
@@ -305,14 +342,25 @@ function cacheResponse(query, responseObj) {
305
342
  } catch (_) {}
306
343
  }
307
344
 
345
+ function clearResponseCache() {
346
+ try {
347
+ getDb().prepare('DELETE FROM response_cache').run();
348
+ } catch (err) {
349
+ console.error('[Memory] clearResponseCache error:', err.message);
350
+ }
351
+ }
352
+
308
353
  module.exports = {
309
354
  recordInteraction,
310
355
  saveSessionSummary,
311
356
  getUserContext,
312
357
  setProfile,
358
+ deleteProfile,
359
+ clearConversationScopedProfile,
313
360
  getProfile,
314
361
  getTopPatterns,
315
362
  getRecentMemories,
316
363
  getCachedResponse,
317
- cacheResponse
364
+ cacheResponse,
365
+ clearResponseCache
318
366
  };
@@ -216,12 +216,23 @@ async function openFile(target) {
216
216
  console.error('openFile error:', result);
217
217
  return `เกิดข้อผิดพลาดในการเปิดไฟล์: ${result}`;
218
218
  }
219
+ return true;
219
220
  } else {
220
221
  return new Promise((resolve) => {
221
- execFile('xdg-open', [resolvedPath], (err) => {
222
+ // บน Linux ลอง xdg-open แล้วค่อย gio open ถ้าอันแรกไม่ทำงาน
223
+ const { exec } = require('child_process');
224
+ const platformCmd = process.platform === 'darwin' ? 'open' : (process.platform === 'win32' ? 'start' : 'xdg-open');
225
+
226
+ // ใช้ exec เพื่อให้รันผ่าน shell และรองรับการทำ fallback
227
+ let cmd = `${platformCmd} "${resolvedPath}"`;
228
+ if (process.platform === 'linux') {
229
+ cmd = `xdg-open "${resolvedPath}" || gio open "${resolvedPath}" || nautilus "${resolvedPath}" || nemo "${resolvedPath}" || thunar "${resolvedPath}" || dolphin "${resolvedPath}"`;
230
+ }
231
+
232
+ exec(cmd, (err) => {
222
233
  if (err) {
223
- console.error("Failed to open path via xdg-open:", err);
224
- resolve(`ไม่สามารถเปิดไฟล์ได้ค่ะ: ${err.message}`);
234
+ console.error("Failed to open path:", err);
235
+ resolve(`ไม่สามารถเปิดได้ค่ะ: ${err.message}`);
225
236
  } else {
226
237
  resolve(true);
227
238
  }