@pheem49/mint 1.4.2 → 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 (60) hide show
  1. package/GUIDE_TH.md +113 -0
  2. package/README.md +239 -76
  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 +13 -1
  9. package/mint-cli.js +100 -9
  10. package/package.json +12 -4
  11. package/src/AI_Brain/Gemini_API.js +77 -20
  12. package/src/AI_Brain/autonomous_brain.js +10 -0
  13. package/src/AI_Brain/behavior_memory.js +26 -5
  14. package/src/AI_Brain/headless_agent.js +4 -0
  15. package/src/AI_Brain/knowledge_base.js +61 -8
  16. package/src/AI_Brain/memory_store.js +55 -7
  17. package/src/Automation_Layer/file_operations.js +1 -1
  18. package/src/CLI/chat_router.js +3 -2
  19. package/src/CLI/chat_ui.js +263 -838
  20. package/src/CLI/code_agent.js +144 -42
  21. package/src/CLI/gmail_auth.js +210 -0
  22. package/src/CLI/list_features.js +2 -0
  23. package/src/CLI/onboarding.js +307 -55
  24. package/src/CLI/updater.js +208 -0
  25. package/src/Channels/brave_search_bridge.js +35 -0
  26. package/src/Channels/discord_bridge.js +68 -0
  27. package/src/Channels/google_search_bridge.js +38 -0
  28. package/src/Channels/line_bridge.js +60 -0
  29. package/src/Channels/slack_bridge.js +53 -0
  30. package/src/Channels/telegram_bridge.js +49 -0
  31. package/src/Channels/whatsapp_bridge.js +55 -0
  32. package/src/Command_Parser/parser.js +12 -1
  33. package/src/Plugins/gmail.js +251 -0
  34. package/src/Plugins/google_calendar.js +245 -19
  35. package/src/Plugins/notion.js +256 -0
  36. package/src/System/action_executor.js +129 -0
  37. package/src/System/bridge_manager.js +76 -0
  38. package/src/System/chat_history_manager.js +23 -5
  39. package/src/System/config_manager.js +41 -7
  40. package/src/System/custom_workflows.js +31 -2
  41. package/src/System/google_tts_urls.js +51 -0
  42. package/src/System/ipc_handlers.js +238 -0
  43. package/src/System/proactive_loop.js +137 -0
  44. package/src/System/safety_manager.js +165 -0
  45. package/src/System/screen_capture.js +175 -0
  46. package/src/System/task_manager.js +15 -5
  47. package/src/System/window_manager.js +210 -0
  48. package/src/UI/renderer.js +33 -7
  49. package/src/UI/settings.html +24 -0
  50. package/src/UI/settings.js +14 -4
  51. package/src/UI/styles.css +14 -1
  52. package/tests/action_executor_safety.test.js +67 -0
  53. package/tests/gmail.test.js +135 -0
  54. package/tests/gmail_auth.test.js +129 -0
  55. package/tests/google_calendar.test.js +113 -0
  56. package/tests/google_tts_urls.test.js +24 -0
  57. package/tests/notion.test.js +121 -0
  58. package/tests/provider_routing.test.js +17 -1
  59. package/tests/safety_manager.test.js +40 -0
  60. package/tests/updater.test.js +32 -0
package/mint-cli-logic.js CHANGED
@@ -2,11 +2,23 @@
2
2
  const { openApp } = require('./src/Automation_Layer/open_app');
3
3
  const { openWebsite, openSearch } = require('./src/Automation_Layer/open_website');
4
4
  const { createFolder, openFile, deleteFile, findPath } = require('./src/Automation_Layer/file_operations');
5
+ const safetyManager = require('./src/System/safety_manager');
5
6
 
6
- async function executeAction(action) {
7
+ async function executeAction(action, options = {}) {
7
8
  if (!action || action.type === 'none') return null;
8
9
 
9
10
  try {
11
+ const safety = safetyManager.assertActionAllowed(action, {
12
+ allowDangerous: options.allowDangerous === true
13
+ });
14
+ safetyManager.appendActionLog({
15
+ source: options.source || 'mint_cli_logic',
16
+ action: action.type,
17
+ target: action.target || action.path || '',
18
+ tier: safety.tier,
19
+ approved: options.allowDangerous === true || safety.tier !== safetyManager.TIERS.DANGEROUS
20
+ });
21
+
10
22
  switch (action.type) {
11
23
  case 'open_url':
12
24
  openWebsite(action.target);
package/mint-cli.js CHANGED
@@ -23,16 +23,27 @@ const { executeCodeTask } = require('./src/CLI/code_agent');
23
23
  const { detectCodeIntent, runChatRoutedTask } = require('./src/CLI/chat_router');
24
24
  const readline = require('readline');
25
25
  const { createChatUI } = require('./src/CLI/chat_ui');
26
+ const { runUpdate, runStartupAutoUpdate, shouldRunAutoUpdate } = require('./src/CLI/updater');
27
+ const { runGmailAuth } = require('./src/CLI/gmail_auth');
26
28
 
27
29
  // Startup Info
28
30
  const startupConfig = readConfig();
29
- const startupModel = startupConfig.geminiModel || 'gemini-2.5-flash';
31
+ const startupProvider = startupConfig.aiProvider || 'gemini';
32
+ const startupModel = startupProvider === 'openai'
33
+ ? (startupConfig.openaiModel || 'gpt-4o')
34
+ : startupProvider === 'anthropic'
35
+ ? (startupConfig.anthropicModel || 'claude-3-5-sonnet-latest')
36
+ : startupProvider === 'local_openai'
37
+ ? (startupConfig.localModelName || 'local-model')
38
+ : startupProvider === 'ollama'
39
+ ? (startupConfig.ollamaModel || 'llama3:latest')
40
+ : (startupConfig.geminiModel || 'gemini-2.5-flash');
30
41
  const startupNow = new Date();
31
42
  const startupTime = startupNow.toLocaleString('th-TH', {
32
43
  day: '2-digit', month: '2-digit', year: 'numeric',
33
44
  hour: '2-digit', minute: '2-digit', hour12: false
34
45
  }).replace(',', '');
35
- console.log(`\x1b[38;5;121m[Mint] v${pkg.version} | ${startupTime} | Active Model: ${startupModel}\x1b[0m`);
46
+ console.log(`\x1b[38;5;121m[Mint] v${pkg.version} | ${startupTime} | Active AI: ${startupProvider} • ${startupModel}\x1b[0m`);
36
47
 
37
48
  // ANSI Colors
38
49
  const colors = {
@@ -84,6 +95,29 @@ program
84
95
  .description('Mint - Your Personal AI Assistant CLI')
85
96
  .version(pkg.version);
86
97
 
98
+ program.hook('preAction', async (thisCommand, actionCommand) => {
99
+ if (actionCommand.name() === 'update' || process.env.MINT_SKIP_AUTO_UPDATE === '1') {
100
+ return;
101
+ }
102
+
103
+ const config = readConfig();
104
+ if (config.enableAutoUpdate === false) {
105
+ return;
106
+ }
107
+
108
+ if (!shouldRunAutoUpdate(config)) {
109
+ return;
110
+ }
111
+
112
+ console.log(`${colors.gray}[Mint Update] Checking for updates...${colors.reset}`);
113
+ const result = await runStartupAutoUpdate(config, writeConfig);
114
+ if (result.status === 'updated') {
115
+ console.log(`${colors.mint}[Mint Update] ${result.message}${colors.reset}`);
116
+ } else if (result.status === 'error') {
117
+ console.log(`${colors.gray}[Mint Update] ${result.message}${colors.reset}`);
118
+ }
119
+ });
120
+
87
121
  // Chat Command (Interactive Mode)
88
122
  program
89
123
  .command('chat', { isDefault: true })
@@ -139,6 +173,32 @@ program
139
173
  console.log(`${colors.gray}You will receive a notification when it's done.${colors.reset}\n`);
140
174
  });
141
175
 
176
+ program
177
+ .command('update')
178
+ .description('Check for and install the latest Mint CLI version from npm')
179
+ .option('--check', 'Only check whether an update is available')
180
+ .option('--dry-run', 'Show the npm update operation without installing')
181
+ .action(async (options) => {
182
+ console.log(`\n${colors.mint}${colors.bright}[Mint Update]${colors.reset} Checking npm for updates...`);
183
+
184
+ try {
185
+ const result = await runUpdate({
186
+ checkOnly: options.check === true,
187
+ dryRun: options.dryRun === true
188
+ });
189
+
190
+ const color = result.status === 'error' ? colors.pink : colors.mint;
191
+ console.log(`${color}${result.message}${colors.reset}\n`);
192
+
193
+ if (result.status === 'error') {
194
+ process.exitCode = 1;
195
+ }
196
+ } catch (error) {
197
+ console.error(`${colors.pink}Update failed: ${error.message}${colors.reset}\n`);
198
+ process.exitCode = 1;
199
+ }
200
+ });
201
+
142
202
  program
143
203
  .command('mcp')
144
204
  .description('Manage MCP (Model Context Protocol) servers')
@@ -212,6 +272,29 @@ program
212
272
  })
213
273
  );
214
274
 
275
+ program
276
+ .command('gmail')
277
+ .description('Manage Gmail integration')
278
+ .addCommand(new Command('auth')
279
+ .description('Open Google OAuth login and save a Gmail refresh token')
280
+ .option('--port <port>', 'Local callback port, defaults to a random available port')
281
+ .option('--no-open', 'Print the auth link without opening a browser')
282
+ .action(async (options) => {
283
+ try {
284
+ const result = await runGmailAuth({
285
+ port: options.port ? Number(options.port) : 0,
286
+ openBrowser: options.open,
287
+ logger: console
288
+ });
289
+ console.log(`\n${colors.mint}✓${colors.reset} Gmail connected for ${result.userId}. Refresh token saved.`);
290
+ console.log(`${colors.gray}Scopes: ${result.scopes.join(', ')}${colors.reset}\n`);
291
+ } catch (error) {
292
+ console.error(`\n${colors.pink}Gmail auth failed:${colors.reset} ${error.message}\n`);
293
+ process.exitCode = 1;
294
+ }
295
+ })
296
+ );
297
+
215
298
  program
216
299
  .command('code')
217
300
  .description('Run Mint in workspace-aware coding mode for the current project')
@@ -239,15 +322,21 @@ program
239
322
  }
240
323
  });
241
324
 
242
- program.parse(process.argv);
325
+ program.parseAsync(process.argv).catch((error) => {
326
+ console.error(`${colors.pink}${error.message}${colors.reset}`);
327
+ process.exitCode = 1;
328
+ });
243
329
 
244
330
  /**
245
331
  * The Interactive Chat Loop — Gemini-style TUI
246
332
  */
247
333
  async function startInteractiveChat(initialMessage = null) {
248
334
  let lastResponseText = "";
249
- const { screen, appendMessage, streamMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode, appendCodeStep, updateWorkspace, askUser } = createChatUI({
335
+ const formatErrorMessage = (err) => err && err.message ? err.message : String(err || 'Unknown error');
336
+
337
+ const ui = await createChatUI({
250
338
  onSubmit: async (text) => {
339
+ const { screen, appendMessage, streamMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode, appendCodeStep, updateWorkspace, askUser } = ui;
251
340
  if (text.startsWith('/')) {
252
341
  if (text.startsWith('/agent')) {
253
342
  const args = text.split(' ');
@@ -375,18 +464,17 @@ async function startInteractiveChat(initialMessage = null) {
375
464
  clearInterval(timer);
376
465
  setThinking(false);
377
466
  lastResponseText = result.summary;
378
- appendMessage('assistant', result.summary);
467
+ appendMessage('assistant', result.summary, { providerInfo: result.providerInfo });
379
468
 
380
469
  } catch (err) {
381
470
  clearInterval(timer);
382
471
  setThinking(false);
383
- appendMessage('error', err.message);
472
+ appendMessage('error', formatErrorMessage(err));
384
473
  } finally {
385
474
  if (setMode) setMode('Chat');
386
475
  }
387
476
  },
388
477
  onExit: () => {
389
- screen.destroy();
390
478
  // Explicitly restore terminal state and disable ALL mouse tracking modes
391
479
  process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l');
392
480
  process.stdout.write('\x1b[?25h'); // Show cursor
@@ -397,6 +485,7 @@ async function startInteractiveChat(initialMessage = null) {
397
485
 
398
486
  // Handle initial message if passed via CLI arg
399
487
  if (initialMessage) {
488
+ const { appendMessage, streamMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode, appendCodeStep, updateWorkspace, askUser } = ui;
400
489
  appendMessage('user', initialMessage);
401
490
  const transcript = await getChatTranscript();
402
491
  if (setMode) setMode('Agent');
@@ -427,12 +516,12 @@ async function startInteractiveChat(initialMessage = null) {
427
516
  clearInterval(timer);
428
517
  setThinking(false);
429
518
  lastResponseText = result.summary;
430
- appendMessage('assistant', result.summary);
519
+ appendMessage('assistant', result.summary, { providerInfo: result.providerInfo });
431
520
 
432
521
  } catch (err) {
433
522
  clearInterval(timer);
434
523
  setThinking(false);
435
- appendMessage('error', err.message);
524
+ appendMessage('error', formatErrorMessage(err));
436
525
  } finally {
437
526
  if (setMode) setMode('Chat');
438
527
  }
@@ -539,8 +628,10 @@ async function handleSlashCommandUI(input, appendMessage, updateStatusModel, cop
539
628
  await runChatRoutedTask(`/code ${args.join(' ')}`, {
540
629
  appendMessage,
541
630
  setThinking,
631
+ requestApproval,
542
632
  appendCodeStep,
543
633
  setMode,
634
+ askUser: () => Promise.resolve(''),
544
635
  history: await getChatTranscript()
545
636
  });
546
637
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pheem49/mint",
3
- "version": "1.4.2",
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;
@@ -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
  }