@pheem49/mint 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.codex +0 -0
  2. package/README.md +174 -126
  3. package/main.js +21 -1
  4. package/mint-cli-logic.js +21 -1
  5. package/mint-cli.js +287 -45
  6. package/package.json +13 -2
  7. package/src/AI_Brain/Gemini_API.js +331 -64
  8. package/src/AI_Brain/agent_orchestrator.js +73 -0
  9. package/src/AI_Brain/autonomous_brain.js +2 -0
  10. package/src/AI_Brain/memory_store.js +318 -0
  11. package/src/AI_Brain/proactive_engine.js +2 -8
  12. package/src/Automation_Layer/file_operations.js +123 -4
  13. package/src/Automation_Layer/open_app.js +72 -43
  14. package/src/Automation_Layer/open_website.js +3 -3
  15. package/src/CLI/chat_router.js +57 -9
  16. package/src/CLI/chat_ui.js +117 -11
  17. package/src/CLI/code_agent.js +249 -36
  18. package/src/CLI/onboarding.js +53 -6
  19. package/src/CLI/workspace_manager.js +90 -0
  20. package/src/Plugins/docker.js +12 -10
  21. package/src/Plugins/spotify.js +168 -40
  22. package/src/Plugins/system_monitor.js +72 -0
  23. package/src/System/config_manager.js +35 -2
  24. package/src/System/custom_workflows.js +9 -2
  25. package/src/System/notifications.js +23 -0
  26. package/src/UI/settings.html +143 -65
  27. package/src/UI/settings.js +155 -41
  28. package/tests/agent_orchestrator.test.js +41 -0
  29. package/tests/chat_router.test.js +42 -0
  30. package/tests/code_agent.test.js +69 -0
  31. package/tests/config_manager.test.js +141 -0
  32. package/tests/docker.test.js +46 -0
  33. package/tests/file_operations.test.js +57 -0
  34. package/tests/memory_store.test.js +185 -0
  35. package/tests/provider_routing.test.js +67 -0
  36. package/tests/spotify.test.js +201 -0
  37. package/tests/system_monitor.test.js +37 -0
  38. package/tests/workspace_manager.test.js +56 -0
@@ -1,8 +1,11 @@
1
1
  const { GoogleGenAI } = require('@google/genai');
2
2
  const { readChatHistory, writeChatHistory, clearChatHistory } = require('../System/chat_history_manager');
3
- const { readConfig } = require('../System/config_manager');
3
+ const { readConfig, getAvailableProviders } = require('../System/config_manager');
4
4
  const pluginManager = require('../Plugins/plugin_manager');
5
5
  const mcpManager = require('../Plugins/mcp_manager');
6
+ const memoryStore = require('./memory_store');
7
+ const agentOrchestrator = require('./agent_orchestrator');
8
+ const workspaceManager = require('../CLI/workspace_manager');
6
9
 
7
10
  let ai = null;
8
11
  let activeApiKey = '';
@@ -55,11 +58,13 @@ Always respond exactly with valid JSON containing NO MARKDOWN FORMATTING (do not
55
58
  {
56
59
  "response": "Your conversational reply here (Matches user language).",
57
60
  "action": {
58
- "type": "none" | "open_url" | "open_app" | "search" | "web_automation" | "create_folder" | "open_file" | "open_folder" | "delete_file" | "clipboard_write" | "system_info" | "plugin" | "learn_file" | "learn_folder" | "system_automation" | "mcp_tool" | "mouse_click" | "mouse_move" | "type_text" | "key_tap",
61
+ "type": "none" | "open_url" | "open_app" | "search" | "web_automation" | "create_folder" | "open_file" | "open_folder" | "find_path" | "delete_file" | "clipboard_write" | "system_info" | "plugin" | "learn_file" | "learn_folder" | "system_automation" | "mcp_tool" | "mouse_click" | "mouse_move" | "type_text" | "key_tap",
59
62
 
60
63
  "pluginName": "only if type is plugin",
61
64
  "server": "only if type is mcp_tool (server name)",
62
65
  "target": "target string based on type (tool name if mcp_tool, text to type if type_text, key name if key_tap)",
66
+ "pathType": "optional for find_path: 'file' | 'dir' | 'any'",
67
+ "openAfter": true,
63
68
  "x": 0-1000, // required for mouse_click and mouse_move
64
69
  "y": 0-1000, // required for mouse_click and mouse_move
65
70
  "button": 1 | 2 | 3, // optional for mouse_click, 1=left, 2=middle, 3=right
@@ -83,6 +88,12 @@ Output: { "response": "สวัสดีค่ะ! หนูชื่อมิ
83
88
  Input: "Create a folder named Projects"
84
89
  Output: { "response": "Sure thing! I'm creating a folder named 'Projects' for you right now.", "action": { "type": "create_folder", "target": "Projects" } }
85
90
 
91
+ Input: "หาโฟลเดอร์ xidaidai ให้หน่อย" or "find the xidaidai folder"
92
+ Output: { "response": "ได้เลยค่ะ มิ้นท์จะค้นหาโฟลเดอร์ xidaidai ให้", "action": { "type": "find_path", "target": "xidaidai", "pathType": "dir", "openAfter": false } }
93
+
94
+ Input: "เปิดโฟลเดอร์ xidaidai ให้หน่อย" or "open the xidaidai folder"
95
+ Output: { "response": "ได้เลยค่ะ มิ้นท์จะหาแล้วเปิดโฟลเดอร์ xidaidai ให้", "action": { "type": "find_path", "target": "xidaidai", "pathType": "dir", "openAfter": true } }
96
+
86
97
  Input: "วันนี้วันที่เท่าไร" or "What date is today?" or "today's date" or "วันเวลา"
87
98
  Output: { "response": "แป๊บนึงนะคะ มิ้นท์จะดูให้ค่า", "action": { "type": "system_info", "target": "" } }
88
99
 
@@ -92,6 +103,41 @@ Input: "อากาศวันนี้เป็นยังไง" or "What's
92
103
  Output: { "response": "มิ้นท์ไปดูอากาศให้เลยนะคะ", "action": { "type": "system_info", "target": "Bangkok" } }
93
104
  `;
94
105
 
106
+ // ─────────────────────────────────────────────────────────────────────────────
107
+ // buildSystemPrompt() — single source of truth for all provider system prompts
108
+ // Replaces 5 previously duplicated mcpPrompt blocks.
109
+ // ─────────────────────────────────────────────────────────────────────────────
110
+ function buildSystemPrompt() {
111
+ pluginManager.loadPlugins();
112
+ const mcpTools = mcpManager.getAllTools();
113
+
114
+ let mcpSection = '\n\nAVAILABLE MCP TOOLS (Model Context Protocol):\n';
115
+ if (mcpTools.length > 0) {
116
+ mcpTools.forEach(tool => {
117
+ mcpSection += `- Server: ${tool.serverName}, Tool: ${tool.name}\n Desc: ${tool.description}\n Args: ${JSON.stringify(tool.inputSchema.properties)}\n`;
118
+ });
119
+ mcpSection += "\nTo use these tools, use action type 'mcp_tool', specify the 'server' name, set 'target' to the tool name, and provide 'args'.\n";
120
+ } else {
121
+ mcpSection += 'No MCP tools currently connected.\n';
122
+ }
123
+
124
+ // Inject long-term user context (non-blocking read from SQLite)
125
+ const userContext = memoryStore.getUserContext();
126
+
127
+ // Get current specialized persona instruction
128
+ const agent = agentOrchestrator.getCurrentAgent();
129
+ const personaInstruction = `\n\n[CURRENT PERSONA: ${agent.name}]\n${agent.instruction}\n`;
130
+
131
+ // Inject Workspace Context if available
132
+ let workspaceSection = "";
133
+ const ws = workspaceManager.getWorkspaceByPath(process.cwd());
134
+ if (ws) {
135
+ workspaceSection = `\n\n[WORKSPACE DETECTED: ${ws.name}]\nPath: ${ws.path}\nProject Instructions: ${ws.instructions}\n`;
136
+ }
137
+
138
+ return systemInstruction + personaInstruction + workspaceSection + pluginManager.getPromptDescriptions() + mcpSection + userContext;
139
+ }
140
+
95
141
  function resolveApiKey() {
96
142
  let settingsKey = '';
97
143
  try {
@@ -127,6 +173,13 @@ function resolveGeminiModel() {
127
173
  }
128
174
  }
129
175
 
176
+ function getProviderAttemptOrder(config) {
177
+ const provider = config.aiProvider || 'gemini';
178
+ const availableProviders = getAvailableProviders(config);
179
+ const alternates = availableProviders.filter(p => p !== provider);
180
+ return [provider, ...alternates];
181
+ }
182
+
130
183
  // Chat session — maintains conversation history within the session
131
184
  let chat = null;
132
185
  let activeModel = resolveGeminiModel();
@@ -134,22 +187,6 @@ let lastLoggedModel = '';
134
187
  const MAX_HISTORY_MESSAGES = 20; // Keep only the last 20 messages (approx 10 turns)
135
188
 
136
189
  function createChat(history = []) {
137
- // Load plugins and get dynamic description for the prompt
138
- pluginManager.loadPlugins();
139
- // Inject MCP Tools
140
- const mcpTools = mcpManager.getAllTools();
141
- let mcpPrompt = "\n\nAVAILABLE MCP TOOLS (Model Context Protocol):\n";
142
- if (mcpTools.length > 0) {
143
- mcpTools.forEach(tool => {
144
- mcpPrompt += `- Server: ${tool.serverName}, Tool: ${tool.name}\n Desc: ${tool.description}\n Args: ${JSON.stringify(tool.inputSchema.properties)}\n`;
145
- });
146
- mcpPrompt += "\nTo use these tools, use action type 'mcp_tool', specify the 'server' name, set 'target' to the tool name, and provide 'args'.\n";
147
- } else {
148
- mcpPrompt += "No MCP tools currently connected.\n";
149
- }
150
-
151
- const dynamicPrompt = systemInstruction + pluginManager.getPromptDescriptions() + mcpPrompt;
152
-
153
190
  // Truncate history and strip custom fields like 'timestamp' before passing to SDK
154
191
  const cleanedHistory = (history || []).map(msg => ({
155
192
  role: msg.role,
@@ -159,13 +196,12 @@ function createChat(history = []) {
159
196
 
160
197
  activeModel = resolveGeminiModel();
161
198
  if (activeModel && activeModel !== lastLoggedModel) {
162
- // console.log(`[Gemini] Using model: ${activeModel}`);
163
199
  lastLoggedModel = activeModel;
164
200
  }
165
201
  chat = ai.chats.create({
166
202
  model: activeModel,
167
203
  config: {
168
- systemInstruction: dynamicPrompt,
204
+ systemInstruction: buildSystemPrompt(),
169
205
  responseMimeType: "application/json"
170
206
  },
171
207
  history: truncatedHistory
@@ -193,21 +229,6 @@ function shouldUseKnowledgeSearch(message) {
193
229
  async function handleChat(message, base64Image = null, base64Audio = null) {
194
230
  try {
195
231
  const config = readConfig();
196
- const provider = config.aiProvider || 'gemini';
197
-
198
- // Ensure API Key is loaded and Client is initialized before every chat
199
- const currentKey = resolveApiKey();
200
- if (!currentKey) {
201
- return {
202
- response: "I couldn't find your Gemini API Key. Please run 'mint onboard' to set it up!",
203
- action: { type: "none", target: "" }
204
- };
205
- }
206
-
207
- if (!ai || activeApiKey !== currentKey) {
208
- initAiClient();
209
- createChat(readChatHistory());
210
- }
211
232
 
212
233
  let finalMessage = message;
213
234
 
@@ -224,18 +245,68 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
224
245
  }
225
246
  }
226
247
 
227
- if (provider === 'ollama') {
228
- return await handleOllamaChat(finalMessage, base64Image, base64Audio, config);
229
- }
230
-
231
- if (provider === 'anthropic') {
232
- return await handleAnthropicChat(finalMessage, base64Image, config);
233
- }
234
-
235
- if (provider === 'openai') {
236
- return await handleOpenAIChat(finalMessage, base64Image, config);
248
+ const providersToTry = getProviderAttemptOrder(config);
249
+
250
+ for (let i = 0; i < providersToTry.length; i++) {
251
+ const currentProv = providersToTry[i];
252
+ try {
253
+ if (currentProv === 'ollama') {
254
+ return await handleOllamaChat(finalMessage, base64Image, base64Audio, config);
255
+ }
256
+ if (currentProv === 'anthropic') {
257
+ return await handleAnthropicChat(finalMessage, base64Image, config);
258
+ }
259
+ if (currentProv === 'openai') {
260
+ return await handleOpenAIChat(finalMessage, base64Image, config);
261
+ }
262
+ if (currentProv === 'local_openai') {
263
+ return await handleLocalOpenAIChat(finalMessage, base64Image, config);
264
+ }
265
+ if (currentProv === 'huggingface') {
266
+ return await handleHuggingFaceChat(finalMessage, base64Image, config);
267
+ }
268
+
269
+ const currentKey = resolveApiKey();
270
+ if (!currentKey) {
271
+ if (i === providersToTry.length - 1) {
272
+ return {
273
+ response: "I couldn't find your Gemini API Key. Please run 'mint onboard' to set it up!",
274
+ action: { type: "none", target: "" }
275
+ };
276
+ }
277
+ console.warn("[Fallback System] Gemini API key missing. Skipping Gemini provider.");
278
+ continue;
279
+ }
280
+
281
+ if (!ai || activeApiKey !== currentKey) {
282
+ initAiClient();
283
+ createChat(readChatHistory());
284
+ }
285
+
286
+ return await handleGeminiChat(finalMessage, base64Image, base64Audio);
287
+ } catch (error) {
288
+ console.error(`[Fallback System] Provider '${currentProv}' failed:`, error.message);
289
+ if (i === providersToTry.length - 1) {
290
+ console.error("[Fallback System] All available providers failed.");
291
+ throw error; // No more providers to fallback to
292
+ }
293
+ console.log(`[Fallback System] Switching to next available provider: '${providersToTry[i+1]}'`);
294
+ // Continue the loop to try the next provider
295
+ }
237
296
  }
297
+ } catch (globalError) {
298
+ console.error("handleChat error:", globalError);
299
+ throw globalError;
300
+ }
301
+ }
238
302
 
303
+ async function handleGeminiChat(finalMessage, base64Image, base64Audio) {
304
+ try {
305
+ // 1. Check cache first for text-only messages
306
+ if (finalMessage && !base64Image && !base64Audio) {
307
+ const cached = memoryStore.getCachedResponse(finalMessage);
308
+ if (cached) return cached;
309
+ }
239
310
 
240
311
  const desiredModel = resolveGeminiModel();
241
312
  if (!chat || activeModel !== desiredModel) {
@@ -318,7 +389,7 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
318
389
  }
319
390
  }
320
391
 
321
- // Finally, decode any remaining unicode escapes in the response text
392
+ // Decode any remaining unicode escapes in the response text
322
393
  if (parsedResult && typeof parsedResult.response === 'string') {
323
394
  parsedResult.response = decodeUnicode(parsedResult.response);
324
395
  }
@@ -326,6 +397,17 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
326
397
  // Attach timestamp to the result
327
398
  parsedResult.timestamp = now;
328
399
 
400
+ // Record interaction for long-term memory (non-blocking)
401
+ if (finalMessage && parsedResult.response) {
402
+ setImmediate(() => {
403
+ memoryStore.recordInteraction(finalMessage, parsedResult.response);
404
+ // Cache text-only responses
405
+ if (!base64Image && !base64Audio) {
406
+ memoryStore.cacheResponse(finalMessage, parsedResult);
407
+ }
408
+ });
409
+ }
410
+
329
411
  return parsedResult;
330
412
 
331
413
  } catch (error) {
@@ -334,18 +416,115 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
334
416
  }
335
417
  }
336
418
 
419
+ // ─────────────────────────────────────────────────────────────────────────────
420
+ // handleGeminiChatStream() — Streaming async generator (CLI only)
421
+ // Yields: { chunk: string } during streaming
422
+ // { done: true, parsed: object, timestamp: string } when complete
423
+ // ─────────────────────────────────────────────────────────────────────────────
424
+ async function* handleGeminiChatStream(finalMessage, base64Image, base64Audio) {
425
+ try {
426
+ // 1. Check cache first
427
+ if (finalMessage && !base64Image && !base64Audio) {
428
+ const cached = memoryStore.getCachedResponse(finalMessage);
429
+ if (cached) {
430
+ yield { chunk: `{"response":"${cached.response.replace(/"/g, '\\"')}", "action": {"type":"none"}}` };
431
+ yield { done: true, parsed: cached, timestamp: cached.timestamp || new Date().toISOString() };
432
+ return;
433
+ }
434
+ }
435
+
436
+ const desiredModel = resolveGeminiModel();
437
+ if (!chat || activeModel !== desiredModel) {
438
+ createChat(readChatHistory());
439
+ }
440
+
441
+ const parts = [];
442
+ if (finalMessage) {
443
+ parts.push({ text: finalMessage });
444
+ } else if (base64Audio && !base64Image) {
445
+ parts.push({ text: "Please listen to this voice command and respond in Thai with the appropriate JSON action if needed." });
446
+ } else if (!base64Image && !base64Audio) {
447
+ parts.push({ text: "Analyze this input." });
448
+ }
449
+ if (base64Image) {
450
+ const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
451
+ parts.push({ inlineData: { mimeType: "image/png", data: base64Data } });
452
+ }
453
+ if (base64Audio) {
454
+ let mimeType = "audio/webm";
455
+ const mimeMatch = base64Audio.match(/^data:(audio\/\w+);base64,/);
456
+ if (mimeMatch) mimeType = mimeMatch[1];
457
+ const base64Data = base64Audio.replace(/^data:audio\/\w+;base64,/, '');
458
+ parts.push({ inlineData: { mimeType, data: base64Data } });
459
+ }
460
+
461
+ const stream = await chat.sendMessageStream({ message: parts });
462
+ let fullText = '';
463
+
464
+ for await (const chunk of stream) {
465
+ let chunkText = '';
466
+ try {
467
+ chunkText = (typeof chunk.text === 'function') ? chunk.text() : (chunk.text || '');
468
+ } catch (_) {}
469
+ if (chunkText) {
470
+ fullText += chunkText;
471
+ yield { chunk: chunkText };
472
+ }
473
+ }
474
+
475
+ // Save history
476
+ const history = await chat.getHistory();
477
+ const now = new Date().toISOString();
478
+ if (history.length >= 2) {
479
+ const modelMsg = history[history.length - 1];
480
+ const userMsg = history[history.length - 2];
481
+ if (!modelMsg.timestamp) modelMsg.timestamp = now;
482
+ if (!userMsg.timestamp) userMsg.timestamp = now;
483
+ }
484
+ writeChatHistory(history);
485
+
486
+ // Parse complete JSON response
487
+ let parsedResult;
488
+ try {
489
+ parsedResult = JSON.parse(fullText);
490
+ } catch (_) {
491
+ const jsonMatch = fullText.match(/```json\n([\s\S]*?)\n```/) || fullText.match(/\{[\s\S]*\}/);
492
+ if (jsonMatch) {
493
+ parsedResult = JSON.parse(jsonMatch[jsonMatch.length > 1 ? 1 : 0]);
494
+ } else {
495
+ parsedResult = { response: fullText, action: { type: 'none', target: '' } };
496
+ }
497
+ }
498
+ if (parsedResult && typeof parsedResult.response === 'string') {
499
+ parsedResult.response = decodeUnicode(parsedResult.response);
500
+ }
501
+ parsedResult.timestamp = now;
502
+
503
+ // Record for long-term memory
504
+ if (finalMessage && parsedResult.response) {
505
+ setImmediate(() => {
506
+ memoryStore.recordInteraction(finalMessage, parsedResult.response);
507
+ // Cache text-only responses
508
+ if (!base64Image && !base64Audio) {
509
+ memoryStore.cacheResponse(finalMessage, parsedResult);
510
+ }
511
+ });
512
+ }
513
+
514
+ yield { done: true, parsed: parsedResult, timestamp: now };
515
+
516
+ } catch (error) {
517
+ console.error('[Stream] Gemini stream error:', error);
518
+ throw error;
519
+ }
520
+ }
521
+
337
522
  async function handleAnthropicChat(finalMessage, base64Image, config) {
338
523
  const history = readChatHistory() || [];
339
524
  const apiKey = config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
340
525
  if (!apiKey) return { response: "กรุณาใส่ Anthropic API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
341
526
 
342
- const mcpTools = mcpManager.getAllTools();
343
- let mcpPrompt = "\n\nAVAILABLE MCP TOOLS:\n";
344
- mcpTools.forEach(tool => {
345
- mcpPrompt += `- Server: ${tool.serverName}, Tool: ${tool.name}\n Desc: ${tool.description}\n Args: ${JSON.stringify(tool.inputSchema.properties)}\n`;
346
- });
347
-
348
- const systemPrompt = systemInstruction + pluginManager.getPromptDescriptions() + mcpPrompt;
527
+ const systemPrompt = buildSystemPrompt();
349
528
 
350
529
  const messages = [];
351
530
  for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
@@ -392,13 +571,7 @@ async function handleOpenAIChat(finalMessage, base64Image, config) {
392
571
  const apiKey = config.openaiApiKey || process.env.OPENAI_API_KEY;
393
572
  if (!apiKey) return { response: "กรุณาใส่ OpenAI API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
394
573
 
395
- const mcpTools = mcpManager.getAllTools();
396
- let mcpPrompt = "\n\nAVAILABLE MCP TOOLS:\n";
397
- mcpTools.forEach(tool => {
398
- mcpPrompt += `- Server: ${tool.serverName}, Tool: ${tool.name}\n Desc: ${tool.description}\n Args: ${JSON.stringify(tool.inputSchema.properties)}\n`;
399
- });
400
-
401
- const systemPrompt = systemInstruction + pluginManager.getPromptDescriptions() + mcpPrompt;
574
+ const systemPrompt = buildSystemPrompt();
402
575
 
403
576
  const messages = [{ role: "system", content: systemPrompt }];
404
577
  for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
@@ -435,6 +608,96 @@ async function handleOpenAIChat(finalMessage, base64Image, config) {
435
608
  return parseAiResponse(outputText);
436
609
  }
437
610
 
611
+ async function handleLocalOpenAIChat(finalMessage, base64Image, config) {
612
+ const history = readChatHistory() || [];
613
+ const apiKey = 'lm-studio';
614
+ const baseUrl = config.localApiBaseUrl || 'http://localhost:1234/v1';
615
+
616
+ const systemPrompt = buildSystemPrompt();
617
+
618
+ const messages = [{ role: "system", content: systemPrompt }];
619
+ for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
620
+ const role = msg.role === 'model' ? 'assistant' : 'user';
621
+ let text = Array.isArray(msg.parts) ? msg.parts.map(p => p.text || '').join('\n') : '';
622
+ if (text) messages.push({ role, content: text });
623
+ }
624
+
625
+ const content = [{ type: "text", text: finalMessage || "Analyze this." }];
626
+ if (base64Image) {
627
+ content.push({
628
+ type: "image_url",
629
+ image_url: { url: base64Image }
630
+ });
631
+ }
632
+ messages.push({ role: "user", content });
633
+
634
+ const response = await axios.post(`${baseUrl.replace(/\/$/, '')}/chat/completions`, {
635
+ model: config.localModelName || 'local-model',
636
+ messages: messages,
637
+ // response_format json_object is sometimes problematic on weak local models, but required by our prompt.
638
+ // We'll keep it as some local servers like LM Studio support it for specific models.
639
+ // If not supported, the system prompt usually coerces it anyway.
640
+ response_format: { type: "json_object" }
641
+ }, {
642
+ headers: {
643
+ 'Authorization': `Bearer ${apiKey}`,
644
+ 'Content-Type': 'application/json'
645
+ }
646
+ });
647
+
648
+ const outputText = response.data.choices[0].message.content;
649
+ history.push({ role: 'user', parts: [{ text: finalMessage }] });
650
+ history.push({ role: 'model', parts: [{ text: outputText }] });
651
+ writeChatHistory(history.slice(-MAX_HISTORY_MESSAGES));
652
+
653
+ return parseAiResponse(outputText);
654
+ }
655
+
656
+ async function handleHuggingFaceChat(finalMessage, base64Image, config) {
657
+ const history = readChatHistory() || [];
658
+ const apiKey = config.hfApiKey || process.env.HF_API_KEY;
659
+ if (!apiKey) return { response: "กรุณาใส่ Hugging Face API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
660
+
661
+ const modelId = config.hfModel || 'meta-llama/Meta-Llama-3-8B-Instruct';
662
+ const baseUrl = `https://api-inference.huggingface.co/models/${modelId}/v1/chat/completions`;
663
+
664
+ const systemPrompt = buildSystemPrompt();
665
+
666
+ const messages = [{ role: "system", content: systemPrompt }];
667
+ for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
668
+ const role = msg.role === 'model' ? 'assistant' : 'user';
669
+ let text = Array.isArray(msg.parts) ? msg.parts.map(p => p.text || '').join('\n') : '';
670
+ if (text) messages.push({ role, content: text });
671
+ }
672
+
673
+ const content = [{ type: "text", text: finalMessage || "Analyze this." }];
674
+ if (base64Image) {
675
+ content.push({
676
+ type: "image_url",
677
+ image_url: { url: base64Image }
678
+ });
679
+ }
680
+ messages.push({ role: "user", content });
681
+
682
+ const response = await axios.post(baseUrl, {
683
+ model: modelId,
684
+ messages: messages,
685
+ max_tokens: 4096
686
+ }, {
687
+ headers: {
688
+ 'Authorization': `Bearer ${apiKey}`,
689
+ 'Content-Type': 'application/json'
690
+ }
691
+ });
692
+
693
+ const outputText = response.data.choices[0].message.content;
694
+ history.push({ role: 'user', parts: [{ text: finalMessage }] });
695
+ history.push({ role: 'model', parts: [{ text: outputText }] });
696
+ writeChatHistory(history.slice(-MAX_HISTORY_MESSAGES));
697
+
698
+ return parseAiResponse(outputText);
699
+ }
700
+
438
701
  function parseAiResponse(outputText) {
439
702
  let parsedResult;
440
703
  try {
@@ -456,10 +719,9 @@ function parseAiResponse(outputText) {
456
719
 
457
720
  async function handleOllamaChat(finalMessage, base64Image, base64Audio, config) {
458
721
  const history = readChatHistory() || [];
459
- pluginManager.loadPlugins();
460
722
 
461
723
  const ollamaMessages = [
462
- { role: 'system', content: systemInstruction + pluginManager.getPromptDescriptions() }
724
+ { role: 'system', content: buildSystemPrompt() }
463
725
  ];
464
726
 
465
727
  for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
@@ -486,7 +748,8 @@ async function handleOllamaChat(finalMessage, base64Image, base64Audio, config)
486
748
 
487
749
  ollamaMessages.push(userMessage);
488
750
 
489
- const response = await axios.post('http://localhost:11434/api/chat', {
751
+ const ollamaBaseUrl = (config.ollamaHost || 'http://localhost:11434').replace(/\/$/, '');
752
+ const response = await axios.post(`${ollamaBaseUrl}/api/chat`, {
490
753
  model: config.ollamaModel || 'llama3:latest',
491
754
  messages: ollamaMessages,
492
755
  format: 'json',
@@ -631,8 +894,12 @@ async function translateImageContent(base64Image) {
631
894
 
632
895
  module.exports = {
633
896
  handleChat,
897
+ handleGeminiChatStream,
634
898
  resetChat,
635
899
  getChatTranscript,
636
900
  translateImageContent,
637
- refreshApiKeyFromConfig
901
+ refreshApiKeyFromConfig,
902
+ _helpers: {
903
+ getProviderAttemptOrder
904
+ }
638
905
  };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Mint Agent Orchestrator
3
+ * -----------------------
4
+ * Manages specialized AI personas (Experts) and their system prompts.
5
+ * Allows switching the agent's behavior on the fly.
6
+ */
7
+
8
+ const AGENT_PERSONAS = {
9
+ 'general': {
10
+ name: 'Mint Default',
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.'
13
+ },
14
+ 'coder': {
15
+ name: 'Mint Coder',
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.'
18
+ },
19
+ 'researcher': {
20
+ name: 'Mint Researcher',
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.'
23
+ },
24
+ 'creative': {
25
+ name: 'Mint Creative',
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.'
28
+ },
29
+ 'manager': {
30
+ name: 'Mint Manager',
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.'
33
+ },
34
+ 'reviewer': {
35
+ name: 'Mint Reviewer',
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.'
38
+ }
39
+ };
40
+
41
+ let currentAgentType = 'general';
42
+
43
+ function getAgent(type) {
44
+ return AGENT_PERSONAS[type] || AGENT_PERSONAS['general'];
45
+ }
46
+
47
+ function setAgent(type) {
48
+ if (AGENT_PERSONAS[type]) {
49
+ currentAgentType = type;
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ function getCurrentAgent() {
56
+ return getAgent(currentAgentType);
57
+ }
58
+
59
+ function listAgents() {
60
+ return Object.keys(AGENT_PERSONAS);
61
+ }
62
+
63
+ function resetAgent() {
64
+ currentAgentType = 'general';
65
+ }
66
+
67
+ module.exports = {
68
+ getAgent,
69
+ setAgent,
70
+ getCurrentAgent,
71
+ listAgents,
72
+ resetAgent
73
+ };
@@ -7,6 +7,7 @@ const fs = require('fs');
7
7
  const path = require('path');
8
8
 
9
9
  const os = require('os');
10
+ const { sendNotification } = require('../System/notifications');
10
11
 
11
12
  const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
12
13
 
@@ -119,6 +120,7 @@ async function executeAutonomousTask(taskDescription, notifyCallback) {
119
120
  break;
120
121
  case 'propose_bash':
121
122
  if (notifyCallback) notifyCallback(`💡 มิ้นท์เสนอให้รันคำสั่ง: ${actionObj.target}`);
123
+ sendNotification('Mint Bash Proposal', `Mint wants to run: ${actionObj.target}`);
122
124
  observation = `USER NOTIFIED of bash command: ${actionObj.target}. Note: You must wait for user to run it manually. If you can continue without it, do so. Otherwise, indicate you are waiting or done with this phase.`;
123
125
  break;
124
126
  default: