@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.
- package/GUIDE_TH.md +113 -0
- package/README.md +214 -142
- package/assets/CLI_Screen.png +0 -0
- package/docs/assets/CLI_Screen.png +0 -0
- package/docs/guide.html +632 -0
- package/docs/index.html +5 -4
- package/main.js +66 -894
- package/mint-cli-logic.js +15 -8
- package/mint-cli.js +305 -195
- package/package.json +12 -4
- package/src/AI_Brain/Gemini_API.js +77 -20
- package/src/AI_Brain/agent_orchestrator.js +6 -6
- package/src/AI_Brain/autonomous_brain.js +10 -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 +55 -7
- package/src/Automation_Layer/file_operations.js +14 -3
- package/src/CLI/chat_router.js +21 -7
- package/src/CLI/chat_ui.js +264 -710
- package/src/CLI/code_agent.js +370 -124
- package/src/CLI/gmail_auth.js +210 -0
- package/src/CLI/list_features.js +5 -1
- package/src/CLI/onboarding.js +307 -55
- package/src/CLI/updater.js +208 -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 +129 -0
- package/src/System/bridge_manager.js +76 -0
- package/src/System/chat_history_manager.js +23 -5
- package/src/System/config_manager.js +41 -7
- package/src/System/custom_workflows.js +31 -2
- package/src/System/google_tts_urls.js +51 -0
- package/src/System/ipc_handlers.js +238 -0
- package/src/System/proactive_loop.js +137 -0
- package/src/System/safety_manager.js +165 -0
- package/src/System/screen_capture.js +175 -0
- package/src/System/task_manager.js +15 -5
- package/src/System/window_manager.js +210 -0
- package/src/UI/renderer.js +33 -7
- package/src/UI/settings.html +24 -0
- package/src/UI/settings.js +14 -4
- package/src/UI/styles.css +14 -1
- package/tests/action_executor_safety.test.js +67 -0
- package/tests/gmail.test.js +135 -0
- package/tests/gmail_auth.test.js +129 -0
- package/tests/google_calendar.test.js +113 -0
- package/tests/google_tts_urls.test.js +24 -0
- package/tests/notion.test.js +121 -0
- package/tests/provider_routing.test.js +17 -1
- package/tests/safety_manager.test.js +40 -0
- 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.
|
|
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": [
|
|
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
|
-
"
|
|
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
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
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
|
|
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
|
|
180
|
-
|
|
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 =
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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 });
|
|
30
33
|
}
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
}
|
|
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(`•
|
|
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
|
-
|
|
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
|
|
224
|
-
resolve(
|
|
234
|
+
console.error("Failed to open path:", err);
|
|
235
|
+
resolve(`ไม่สามารถเปิดได้ค่ะ: ${err.message}`);
|
|
225
236
|
} else {
|
|
226
237
|
resolve(true);
|
|
227
238
|
}
|