@pheem49/mint 1.2.4 → 1.4.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.
@@ -1,12 +1,17 @@
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
+ 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');
5
9
 
6
10
  let ai = null;
7
11
  let activeApiKey = '';
8
12
  const initialEnvKey = (process.env.GEMINI_API_KEY || '').trim();
9
- const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash'; // Optimized model
13
+ const axios = require('axios');
14
+ const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
10
15
 
11
16
  function decodeUnicode(str) {
12
17
  if (!str) return '';
@@ -53,12 +58,24 @@ Always respond exactly with valid JSON containing NO MARKDOWN FORMATTING (do not
53
58
  {
54
59
  "response": "Your conversational reply here (Matches user language).",
55
60
  "action": {
56
- "type": "none" | "open_url" | "open_app" | "search" | "web_automation" | "create_folder" | "open_file" | "delete_file" | "clipboard_write" | "system_info" | "plugin" | "learn_file" | "system_automation",
61
+ "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",
62
+
57
63
  "pluginName": "only if type is plugin",
58
- "target": "target string based on type or plugin instruction"
64
+ "server": "only if type is mcp_tool (server name)",
65
+ "target": "target string based on type (tool name if mcp_tool, text to type if type_text, key name if key_tap)",
66
+ "x": 0-1000, // required for mouse_click and mouse_move
67
+ "y": 0-1000, // required for mouse_click and mouse_move
68
+ "button": 1 | 2 | 3, // optional for mouse_click, 1=left, 2=middle, 3=right
69
+ "args": { "param": "value" } // only if type is mcp_tool
59
70
  }
60
71
  }
61
72
 
73
+ COORDINATE SYSTEM:
74
+ - When analyzing an image, use a coordinate system from 0 to 1000.
75
+ - (0, 0) is the Top-Left corner.
76
+ - (1000, 1000) is the Bottom-Right corner.
77
+ - To click an element, estimate its center point and provide x and y.
78
+
62
79
  Examples:
63
80
  Input: "Hi, what is your name?"
64
81
  Output: { "response": "Hello! My name is Mint, your personal AI assistant. How can I help you today?", "action": { "type": "none", "target": "" } }
@@ -78,6 +95,41 @@ Input: "อากาศวันนี้เป็นยังไง" or "What's
78
95
  Output: { "response": "มิ้นท์ไปดูอากาศให้เลยนะคะ", "action": { "type": "system_info", "target": "Bangkok" } }
79
96
  `;
80
97
 
98
+ // ─────────────────────────────────────────────────────────────────────────────
99
+ // buildSystemPrompt() — single source of truth for all provider system prompts
100
+ // Replaces 5 previously duplicated mcpPrompt blocks.
101
+ // ─────────────────────────────────────────────────────────────────────────────
102
+ function buildSystemPrompt() {
103
+ pluginManager.loadPlugins();
104
+ const mcpTools = mcpManager.getAllTools();
105
+
106
+ let mcpSection = '\n\nAVAILABLE MCP TOOLS (Model Context Protocol):\n';
107
+ if (mcpTools.length > 0) {
108
+ mcpTools.forEach(tool => {
109
+ mcpSection += `- Server: ${tool.serverName}, Tool: ${tool.name}\n Desc: ${tool.description}\n Args: ${JSON.stringify(tool.inputSchema.properties)}\n`;
110
+ });
111
+ mcpSection += "\nTo use these tools, use action type 'mcp_tool', specify the 'server' name, set 'target' to the tool name, and provide 'args'.\n";
112
+ } else {
113
+ mcpSection += 'No MCP tools currently connected.\n';
114
+ }
115
+
116
+ // Inject long-term user context (non-blocking read from SQLite)
117
+ const userContext = memoryStore.getUserContext();
118
+
119
+ // Get current specialized persona instruction
120
+ const agent = agentOrchestrator.getCurrentAgent();
121
+ const personaInstruction = `\n\n[CURRENT PERSONA: ${agent.name}]\n${agent.instruction}\n`;
122
+
123
+ // Inject Workspace Context if available
124
+ let workspaceSection = "";
125
+ const ws = workspaceManager.getWorkspaceByPath(process.cwd());
126
+ if (ws) {
127
+ workspaceSection = `\n\n[WORKSPACE DETECTED: ${ws.name}]\nPath: ${ws.path}\nProject Instructions: ${ws.instructions}\n`;
128
+ }
129
+
130
+ return systemInstruction + personaInstruction + workspaceSection + pluginManager.getPromptDescriptions() + mcpSection + userContext;
131
+ }
132
+
81
133
  function resolveApiKey() {
82
134
  let settingsKey = '';
83
135
  try {
@@ -120,10 +172,6 @@ let lastLoggedModel = '';
120
172
  const MAX_HISTORY_MESSAGES = 20; // Keep only the last 20 messages (approx 10 turns)
121
173
 
122
174
  function createChat(history = []) {
123
- // Load plugins and get dynamic description for the prompt
124
- pluginManager.loadPlugins();
125
- const dynamicPrompt = systemInstruction + pluginManager.getPromptDescriptions();
126
-
127
175
  // Truncate history and strip custom fields like 'timestamp' before passing to SDK
128
176
  const cleanedHistory = (history || []).map(msg => ({
129
177
  role: msg.role,
@@ -133,13 +181,12 @@ function createChat(history = []) {
133
181
 
134
182
  activeModel = resolveGeminiModel();
135
183
  if (activeModel && activeModel !== lastLoggedModel) {
136
- // console.log(`[Gemini] Using model: ${activeModel}`);
137
184
  lastLoggedModel = activeModel;
138
185
  }
139
186
  chat = ai.chats.create({
140
187
  model: activeModel,
141
188
  config: {
142
- systemInstruction: dynamicPrompt,
189
+ systemInstruction: buildSystemPrompt(),
143
190
  responseMimeType: "application/json"
144
191
  },
145
192
  history: truncatedHistory
@@ -151,7 +198,18 @@ resolveApiKey();
151
198
  initAiClient();
152
199
  createChat(readChatHistory());
153
200
 
154
- const { searchKnowledge } = require('./knowledge_base');
201
+ function shouldUseKnowledgeSearch(message) {
202
+ const text = (message || '').trim().toLowerCase();
203
+ if (!text) return false;
204
+
205
+ const knowledgeHints = [
206
+ 'readme', 'docs', 'documentation', 'manual', 'guide', 'knowledge', 'rag',
207
+ 'search local', 'search files', 'learn file', 'project files', 'source code',
208
+ 'ไฟล์', 'เอกสาร', 'คู่มือ', 'ค้นหาในเครื่อง', 'ค้นหาไฟล์', 'ข้อมูลในเครื่อง', 'โค้ดโปรเจค'
209
+ ];
210
+
211
+ return knowledgeHints.some(hint => text.includes(hint));
212
+ }
155
213
 
156
214
  async function handleChat(message, base64Image = null, base64Audio = null) {
157
215
  try {
@@ -175,7 +233,8 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
175
233
  let finalMessage = message;
176
234
 
177
235
  // Inject Local RAG Context
178
- if (message && message.trim().length > 0) {
236
+ if (message && message.trim().length > 0 && shouldUseKnowledgeSearch(message)) {
237
+ const { searchKnowledge } = require('./knowledge_base');
179
238
  const retrievedDocs = await searchKnowledge(message);
180
239
  if (retrievedDocs && retrievedDocs.length > 0) {
181
240
  let contextString = `\n\n[LOCAL KNOWLEDGE BASE - USE THIS CONTEXT TO ANSWER]\n`;
@@ -186,9 +245,56 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
186
245
  }
187
246
  }
188
247
 
189
- if (provider === 'ollama') {
190
- const axios = require('axios');
191
- return await handleOllamaChat(finalMessage, base64Image, base64Audio, config, axios);
248
+ const { getAvailableProviders } = require('../System/config_manager');
249
+ const availableProviders = getAvailableProviders(config);
250
+
251
+ // Ensure the requested provider is prioritized. If not available, fallback to the first available.
252
+ let providersToTry = [provider];
253
+ const alternates = availableProviders.filter(p => p !== provider);
254
+ providersToTry = providersToTry.concat(alternates);
255
+
256
+ for (let i = 0; i < providersToTry.length; i++) {
257
+ const currentProv = providersToTry[i];
258
+ try {
259
+ if (currentProv === 'ollama') {
260
+ return await handleOllamaChat(finalMessage, base64Image, base64Audio, config);
261
+ }
262
+ if (currentProv === 'anthropic') {
263
+ return await handleAnthropicChat(finalMessage, base64Image, config);
264
+ }
265
+ if (currentProv === 'openai') {
266
+ return await handleOpenAIChat(finalMessage, base64Image, config);
267
+ }
268
+ if (currentProv === 'local_openai') {
269
+ return await handleLocalOpenAIChat(finalMessage, base64Image, config);
270
+ }
271
+ if (currentProv === 'huggingface') {
272
+ return await handleHuggingFaceChat(finalMessage, base64Image, config);
273
+ }
274
+
275
+ return await handleGeminiChat(finalMessage, base64Image, base64Audio);
276
+ } catch (error) {
277
+ console.error(`[Fallback System] Provider '${currentProv}' failed:`, error.message);
278
+ if (i === providersToTry.length - 1) {
279
+ console.error("[Fallback System] All available providers failed.");
280
+ throw error; // No more providers to fallback to
281
+ }
282
+ console.log(`[Fallback System] Switching to next available provider: '${providersToTry[i+1]}'`);
283
+ // Continue the loop to try the next provider
284
+ }
285
+ }
286
+ } catch (globalError) {
287
+ console.error("handleChat error:", globalError);
288
+ throw globalError;
289
+ }
290
+ }
291
+
292
+ async function handleGeminiChat(finalMessage, base64Image, base64Audio) {
293
+ try {
294
+ // 1. Check cache first for text-only messages
295
+ if (finalMessage && !base64Image && !base64Audio) {
296
+ const cached = memoryStore.getCachedResponse(finalMessage);
297
+ if (cached) return cached;
192
298
  }
193
299
 
194
300
  const desiredModel = resolveGeminiModel();
@@ -272,7 +378,7 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
272
378
  }
273
379
  }
274
380
 
275
- // Finally, decode any remaining unicode escapes in the response text
381
+ // Decode any remaining unicode escapes in the response text
276
382
  if (parsedResult && typeof parsedResult.response === 'string') {
277
383
  parsedResult.response = decodeUnicode(parsedResult.response);
278
384
  }
@@ -280,6 +386,17 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
280
386
  // Attach timestamp to the result
281
387
  parsedResult.timestamp = now;
282
388
 
389
+ // Record interaction for long-term memory (non-blocking)
390
+ if (finalMessage && parsedResult.response) {
391
+ setImmediate(() => {
392
+ memoryStore.recordInteraction(finalMessage, parsedResult.response);
393
+ // Cache text-only responses
394
+ if (!base64Image && !base64Audio) {
395
+ memoryStore.cacheResponse(finalMessage, parsedResult);
396
+ }
397
+ });
398
+ }
399
+
283
400
  return parsedResult;
284
401
 
285
402
  } catch (error) {
@@ -288,12 +405,312 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
288
405
  }
289
406
  }
290
407
 
291
- async function handleOllamaChat(finalMessage, base64Image, base64Audio, config, axios) {
408
+ // ─────────────────────────────────────────────────────────────────────────────
409
+ // handleGeminiChatStream() — Streaming async generator (CLI only)
410
+ // Yields: { chunk: string } during streaming
411
+ // { done: true, parsed: object, timestamp: string } when complete
412
+ // ─────────────────────────────────────────────────────────────────────────────
413
+ async function* handleGeminiChatStream(finalMessage, base64Image, base64Audio) {
414
+ try {
415
+ // 1. Check cache first
416
+ if (finalMessage && !base64Image && !base64Audio) {
417
+ const cached = memoryStore.getCachedResponse(finalMessage);
418
+ if (cached) {
419
+ yield { chunk: `{"response":"${cached.response.replace(/"/g, '\\"')}", "action": {"type":"none"}}` };
420
+ yield { done: true, parsed: cached, timestamp: cached.timestamp || new Date().toISOString() };
421
+ return;
422
+ }
423
+ }
424
+
425
+ const desiredModel = resolveGeminiModel();
426
+ if (!chat || activeModel !== desiredModel) {
427
+ createChat(readChatHistory());
428
+ }
429
+
430
+ const parts = [];
431
+ if (finalMessage) {
432
+ parts.push({ text: finalMessage });
433
+ } else if (base64Audio && !base64Image) {
434
+ parts.push({ text: "Please listen to this voice command and respond in Thai with the appropriate JSON action if needed." });
435
+ } else if (!base64Image && !base64Audio) {
436
+ parts.push({ text: "Analyze this input." });
437
+ }
438
+ if (base64Image) {
439
+ const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
440
+ parts.push({ inlineData: { mimeType: "image/png", data: base64Data } });
441
+ }
442
+ if (base64Audio) {
443
+ let mimeType = "audio/webm";
444
+ const mimeMatch = base64Audio.match(/^data:(audio\/\w+);base64,/);
445
+ if (mimeMatch) mimeType = mimeMatch[1];
446
+ const base64Data = base64Audio.replace(/^data:audio\/\w+;base64,/, '');
447
+ parts.push({ inlineData: { mimeType, data: base64Data } });
448
+ }
449
+
450
+ const stream = await chat.sendMessageStream({ message: parts });
451
+ let fullText = '';
452
+
453
+ for await (const chunk of stream) {
454
+ let chunkText = '';
455
+ try {
456
+ chunkText = (typeof chunk.text === 'function') ? chunk.text() : (chunk.text || '');
457
+ } catch (_) {}
458
+ if (chunkText) {
459
+ fullText += chunkText;
460
+ yield { chunk: chunkText };
461
+ }
462
+ }
463
+
464
+ // Save history
465
+ const history = await chat.getHistory();
466
+ const now = new Date().toISOString();
467
+ if (history.length >= 2) {
468
+ const modelMsg = history[history.length - 1];
469
+ const userMsg = history[history.length - 2];
470
+ if (!modelMsg.timestamp) modelMsg.timestamp = now;
471
+ if (!userMsg.timestamp) userMsg.timestamp = now;
472
+ }
473
+ writeChatHistory(history);
474
+
475
+ // Parse complete JSON response
476
+ let parsedResult;
477
+ try {
478
+ parsedResult = JSON.parse(fullText);
479
+ } catch (_) {
480
+ const jsonMatch = fullText.match(/```json\n([\s\S]*?)\n```/) || fullText.match(/\{[\s\S]*\}/);
481
+ if (jsonMatch) {
482
+ parsedResult = JSON.parse(jsonMatch[jsonMatch.length > 1 ? 1 : 0]);
483
+ } else {
484
+ parsedResult = { response: fullText, action: { type: 'none', target: '' } };
485
+ }
486
+ }
487
+ if (parsedResult && typeof parsedResult.response === 'string') {
488
+ parsedResult.response = decodeUnicode(parsedResult.response);
489
+ }
490
+ parsedResult.timestamp = now;
491
+
492
+ // Record for long-term memory
493
+ if (finalMessage && parsedResult.response) {
494
+ setImmediate(() => {
495
+ memoryStore.recordInteraction(finalMessage, parsedResult.response);
496
+ // Cache text-only responses
497
+ if (!base64Image && !base64Audio) {
498
+ memoryStore.cacheResponse(finalMessage, parsedResult);
499
+ }
500
+ });
501
+ }
502
+
503
+ yield { done: true, parsed: parsedResult, timestamp: now };
504
+
505
+ } catch (error) {
506
+ console.error('[Stream] Gemini stream error:', error);
507
+ throw error;
508
+ }
509
+ }
510
+
511
+ async function handleAnthropicChat(finalMessage, base64Image, config) {
512
+ const history = readChatHistory() || [];
513
+ const apiKey = config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
514
+ if (!apiKey) return { response: "กรุณาใส่ Anthropic API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
515
+
516
+ const systemPrompt = buildSystemPrompt();
517
+
518
+ const messages = [];
519
+ for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
520
+ const role = msg.role === 'model' ? 'assistant' : 'user';
521
+ let text = Array.isArray(msg.parts) ? msg.parts.map(p => p.text || '').join('\n') : '';
522
+ if (text) messages.push({ role, content: text });
523
+ }
524
+
525
+ const content = [];
526
+ if (base64Image) {
527
+ const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
528
+ const mimeType = base64Image.match(/^data:(image\/\w+);base64,/)[1];
529
+ content.push({
530
+ type: "image",
531
+ source: { type: "base64", media_type: mimeType, data: base64Data }
532
+ });
533
+ }
534
+ content.push({ type: "text", text: finalMessage || "Analyze this." });
535
+ messages.push({ role: "user", content });
536
+
537
+ const response = await axios.post('https://api.anthropic.com/v1/messages', {
538
+ model: config.anthropicModel || 'claude-3-5-sonnet-latest',
539
+ max_tokens: 4096,
540
+ system: systemPrompt,
541
+ messages: messages
542
+ }, {
543
+ headers: {
544
+ 'x-api-key': apiKey,
545
+ 'anthropic-version': '2023-06-01',
546
+ 'content-type': 'application/json'
547
+ }
548
+ });
549
+
550
+ const outputText = response.data.content[0].text;
551
+ history.push({ role: 'user', parts: [{ text: finalMessage }] });
552
+ history.push({ role: 'model', parts: [{ text: outputText }] });
553
+ writeChatHistory(history.slice(-MAX_HISTORY_MESSAGES));
554
+
555
+ return parseAiResponse(outputText);
556
+ }
557
+
558
+ async function handleOpenAIChat(finalMessage, base64Image, config) {
559
+ const history = readChatHistory() || [];
560
+ const apiKey = config.openaiApiKey || process.env.OPENAI_API_KEY;
561
+ if (!apiKey) return { response: "กรุณาใส่ OpenAI API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
562
+
563
+ const systemPrompt = buildSystemPrompt();
564
+
565
+ const messages = [{ role: "system", content: systemPrompt }];
566
+ for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
567
+ const role = msg.role === 'model' ? 'assistant' : 'user';
568
+ let text = Array.isArray(msg.parts) ? msg.parts.map(p => p.text || '').join('\n') : '';
569
+ if (text) messages.push({ role, content: text });
570
+ }
571
+
572
+ const content = [{ type: "text", text: finalMessage || "Analyze this." }];
573
+ if (base64Image) {
574
+ content.push({
575
+ type: "image_url",
576
+ image_url: { url: base64Image }
577
+ });
578
+ }
579
+ messages.push({ role: "user", content });
580
+
581
+ const response = await axios.post('https://api.openai.com/v1/chat/completions', {
582
+ model: config.openaiModel || 'gpt-4o',
583
+ messages: messages,
584
+ response_format: { type: "json_object" }
585
+ }, {
586
+ headers: {
587
+ 'Authorization': `Bearer ${apiKey}`,
588
+ 'Content-Type': 'application/json'
589
+ }
590
+ });
591
+
592
+ const outputText = response.data.choices[0].message.content;
593
+ history.push({ role: 'user', parts: [{ text: finalMessage }] });
594
+ history.push({ role: 'model', parts: [{ text: outputText }] });
595
+ writeChatHistory(history.slice(-MAX_HISTORY_MESSAGES));
596
+
597
+ return parseAiResponse(outputText);
598
+ }
599
+
600
+ async function handleLocalOpenAIChat(finalMessage, base64Image, config) {
601
+ const history = readChatHistory() || [];
602
+ const apiKey = 'lm-studio';
603
+ const baseUrl = config.localApiBaseUrl || 'http://localhost:1234/v1';
604
+
605
+ const systemPrompt = buildSystemPrompt();
606
+
607
+ const messages = [{ role: "system", content: systemPrompt }];
608
+ for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
609
+ const role = msg.role === 'model' ? 'assistant' : 'user';
610
+ let text = Array.isArray(msg.parts) ? msg.parts.map(p => p.text || '').join('\n') : '';
611
+ if (text) messages.push({ role, content: text });
612
+ }
613
+
614
+ const content = [{ type: "text", text: finalMessage || "Analyze this." }];
615
+ if (base64Image) {
616
+ content.push({
617
+ type: "image_url",
618
+ image_url: { url: base64Image }
619
+ });
620
+ }
621
+ messages.push({ role: "user", content });
622
+
623
+ const response = await axios.post(`${baseUrl.replace(/\/$/, '')}/chat/completions`, {
624
+ model: config.localModelName || 'local-model',
625
+ messages: messages,
626
+ // response_format json_object is sometimes problematic on weak local models, but required by our prompt.
627
+ // We'll keep it as some local servers like LM Studio support it for specific models.
628
+ // If not supported, the system prompt usually coerces it anyway.
629
+ response_format: { type: "json_object" }
630
+ }, {
631
+ headers: {
632
+ 'Authorization': `Bearer ${apiKey}`,
633
+ 'Content-Type': 'application/json'
634
+ }
635
+ });
636
+
637
+ const outputText = response.data.choices[0].message.content;
638
+ history.push({ role: 'user', parts: [{ text: finalMessage }] });
639
+ history.push({ role: 'model', parts: [{ text: outputText }] });
640
+ writeChatHistory(history.slice(-MAX_HISTORY_MESSAGES));
641
+
642
+ return parseAiResponse(outputText);
643
+ }
644
+
645
+ async function handleHuggingFaceChat(finalMessage, base64Image, config) {
646
+ const history = readChatHistory() || [];
647
+ const apiKey = config.hfApiKey || process.env.HF_API_KEY;
648
+ if (!apiKey) return { response: "กรุณาใส่ Hugging Face API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
649
+
650
+ const modelId = config.hfModel || 'meta-llama/Meta-Llama-3-8B-Instruct';
651
+ const baseUrl = `https://api-inference.huggingface.co/models/${modelId}/v1/chat/completions`;
652
+
653
+ const systemPrompt = buildSystemPrompt();
654
+
655
+ const messages = [{ role: "system", content: systemPrompt }];
656
+ for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
657
+ const role = msg.role === 'model' ? 'assistant' : 'user';
658
+ let text = Array.isArray(msg.parts) ? msg.parts.map(p => p.text || '').join('\n') : '';
659
+ if (text) messages.push({ role, content: text });
660
+ }
661
+
662
+ const content = [{ type: "text", text: finalMessage || "Analyze this." }];
663
+ if (base64Image) {
664
+ content.push({
665
+ type: "image_url",
666
+ image_url: { url: base64Image }
667
+ });
668
+ }
669
+ messages.push({ role: "user", content });
670
+
671
+ const response = await axios.post(baseUrl, {
672
+ model: modelId,
673
+ messages: messages,
674
+ max_tokens: 4096
675
+ }, {
676
+ headers: {
677
+ 'Authorization': `Bearer ${apiKey}`,
678
+ 'Content-Type': 'application/json'
679
+ }
680
+ });
681
+
682
+ const outputText = response.data.choices[0].message.content;
683
+ history.push({ role: 'user', parts: [{ text: finalMessage }] });
684
+ history.push({ role: 'model', parts: [{ text: outputText }] });
685
+ writeChatHistory(history.slice(-MAX_HISTORY_MESSAGES));
686
+
687
+ return parseAiResponse(outputText);
688
+ }
689
+
690
+ function parseAiResponse(outputText) {
691
+ let parsedResult;
692
+ try {
693
+ parsedResult = JSON.parse(outputText);
694
+ } catch (e) {
695
+ const jsonMatch = outputText.match(/```json\n([\s\S]*?)\n```/) || outputText.match(/\{[\s\S]*\}/);
696
+ if (jsonMatch) {
697
+ parsedResult = JSON.parse(jsonMatch[jsonMatch.length > 1 ? 1 : 0]);
698
+ } else {
699
+ parsedResult = { response: outputText, action: { type: "none", target: "" } };
700
+ }
701
+ }
702
+ if (parsedResult && typeof parsedResult.response === 'string') {
703
+ parsedResult.response = decodeUnicode(parsedResult.response);
704
+ }
705
+ parsedResult.timestamp = new Date().toISOString();
706
+ return parsedResult;
707
+ }
708
+
709
+ async function handleOllamaChat(finalMessage, base64Image, base64Audio, config) {
292
710
  const history = readChatHistory() || [];
293
- pluginManager.loadPlugins();
294
711
 
295
712
  const ollamaMessages = [
296
- { role: 'system', content: systemInstruction + pluginManager.getPromptDescriptions() }
713
+ { role: 'system', content: buildSystemPrompt() }
297
714
  ];
298
715
 
299
716
  for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
@@ -320,7 +737,8 @@ async function handleOllamaChat(finalMessage, base64Image, base64Audio, config,
320
737
 
321
738
  ollamaMessages.push(userMessage);
322
739
 
323
- const response = await axios.post('http://localhost:11434/api/chat', {
740
+ const ollamaBaseUrl = (config.ollamaHost || 'http://localhost:11434').replace(/\/$/, '');
741
+ const response = await axios.post(`${ollamaBaseUrl}/api/chat`, {
324
742
  model: config.ollamaModel || 'llama3:latest',
325
743
  messages: ollamaMessages,
326
744
  format: 'json',
@@ -465,6 +883,7 @@ async function translateImageContent(base64Image) {
465
883
 
466
884
  module.exports = {
467
885
  handleChat,
886
+ handleGeminiChatStream,
468
887
  resetChat,
469
888
  getChatTranscript,
470
889
  translateImageContent,
@@ -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: