@pheem49/mint 1.4.0 → 1.4.2

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/mint-cli.js CHANGED
@@ -1,7 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  require('dotenv').config({ quiet: true });
3
+ // Suppress experimental SQLite warning
4
+ const originalEmit = process.emit;
5
+ process.emit = function (name, data, ...args) {
6
+ if (name === 'warning' && typeof data === 'object' && data.name === 'ExperimentalWarning' && data.message.includes('SQLite')) {
7
+ return false;
8
+ }
9
+ return originalEmit.apply(process, [name, data, ...args]);
10
+ };
3
11
  const { Command } = require('commander');
4
- const { handleChat, handleGeminiChatStream, resetChat, refreshApiKeyFromConfig } = require('./src/AI_Brain/Gemini_API');
12
+ const { handleChat, handleGeminiChatStream, resetChat, refreshApiKeyFromConfig, getChatTranscript } = require('./src/AI_Brain/Gemini_API');
5
13
  const agentOrchestrator = require('./src/AI_Brain/agent_orchestrator');
6
14
  const workspaceManager = require('./src/CLI/workspace_manager');
7
15
  const systemMonitor = require('./src/Plugins/system_monitor');
@@ -37,10 +45,42 @@ const colors = {
37
45
  yellow: "\x1b[33m"
38
46
  };
39
47
 
48
+ function formatProgress(info) {
49
+ if (typeof info === 'string') return `${colors.gray}[Mint Code] ${info}${colors.reset}`;
50
+
51
+ const { step, phase, action, target, message } = info;
52
+
53
+ if (action === 'ask_user') {
54
+ return `\n${colors.mint}✓${colors.reset} ${colors.bright}Ask User${colors.reset}\n${colors.gray} ${target || message || ''}${colors.reset}`;
55
+ }
56
+
57
+ let icon = `${colors.mint}✓${colors.reset}`;
58
+ let label = action || phase;
59
+ let color = colors.reset;
60
+
61
+ switch (action) {
62
+ case 'thinking':
63
+ return `\n${colors.yellow}* ${colors.bright}Thinking${colors.reset}`;
64
+ case 'web_search': label = 'WebSearch'; break;
65
+ case 'list_files':
66
+ case 'find_path': label = 'Explored'; break;
67
+ case 'read_file': label = 'ReadFile'; break;
68
+ case 'search_code': label = 'SearchText'; break;
69
+ case 'apply_patch':
70
+ case 'write_file': label = 'Edited'; break;
71
+ case 'run_shell': label = 'Ran command'; break;
72
+ case 'json_repair': icon = '*'; label = 'Repairing JSON'; break;
73
+ case 'reviewer_start': label = 'Reviewing'; break;
74
+ }
75
+
76
+ const content = target || message || '';
77
+ return ` ${icon} ${colors.bright}${label}${colors.reset} ${color}${content}${colors.reset}`;
78
+ }
79
+
40
80
  const program = new Command();
41
81
 
42
82
  program
43
- .name('mint-ai')
83
+ .name('mint')
44
84
  .description('Mint - Your Personal AI Assistant CLI')
45
85
  .version(pkg.version);
46
86
 
@@ -99,6 +139,79 @@ program
99
139
  console.log(`${colors.gray}You will receive a notification when it's done.${colors.reset}\n`);
100
140
  });
101
141
 
142
+ program
143
+ .command('mcp')
144
+ .description('Manage MCP (Model Context Protocol) servers')
145
+ .addCommand(new Command('add')
146
+ .description('Add a new MCP server')
147
+ .argument('<name>', 'Server name')
148
+ .argument('<command>', 'Command to run (e.g. npx)')
149
+ .option('-a, --args <args...>', 'Command arguments')
150
+ .option('-e, --env <env...>', 'Environment variables (KEY=VALUE)')
151
+ .action((name, command, options) => {
152
+ const config = readConfig();
153
+ const mcpServers = config.mcpServers || {};
154
+
155
+ const env = {};
156
+ if (options.env) {
157
+ options.env.forEach(kv => {
158
+ const [k, v] = kv.split('=');
159
+ if (k && v) env[k] = v;
160
+ });
161
+ }
162
+
163
+ mcpServers[name] = {
164
+ command,
165
+ args: options.args || [],
166
+ env
167
+ };
168
+
169
+ config.mcpServers = mcpServers;
170
+ writeConfig(config);
171
+ console.log(`\n${colors.mint}✓${colors.reset} MCP server "${name}" added successfully.`);
172
+ })
173
+ )
174
+ .addCommand(new Command('remove')
175
+ .description('Remove an MCP server')
176
+ .argument('<name>', 'Server name')
177
+ .action((name) => {
178
+ const config = readConfig();
179
+ if (config.mcpServers && config.mcpServers[name]) {
180
+ delete config.mcpServers[name];
181
+ writeConfig(config);
182
+ console.log(`\n${colors.mint}✓${colors.reset} MCP server "${name}" removed.`);
183
+ } else {
184
+ console.log(`\n${colors.pink}✗${colors.reset} MCP server "${name}" not found.`);
185
+ }
186
+ })
187
+ )
188
+ .addCommand(new Command('list')
189
+ .description('List configured MCP servers')
190
+ .action(() => {
191
+ const config = readConfig();
192
+ const servers = Object.keys(config.mcpServers || {});
193
+ if (servers.length === 0) {
194
+ console.log(`\n${colors.gray}No MCP servers configured.${colors.reset}`);
195
+ } else {
196
+ console.log(`\n${colors.bright}Configured MCP Servers:${colors.reset}`);
197
+ servers.forEach(name => {
198
+ const s = config.mcpServers[name];
199
+ console.log(`${colors.mint}• ${colors.bright}${name}${colors.reset}`);
200
+ console.log(` ${colors.gray}Command:${colors.reset} ${s.command} ${(s.args || []).join(' ')}`);
201
+ });
202
+ }
203
+ })
204
+ )
205
+ .addCommand(new Command('clear')
206
+ .description('Remove all MCP servers')
207
+ .action(() => {
208
+ const config = readConfig();
209
+ config.mcpServers = {};
210
+ writeConfig(config);
211
+ console.log(`\n${colors.mint}✓${colors.reset} All MCP servers cleared.`);
212
+ })
213
+ );
214
+
102
215
  program
103
216
  .command('code')
104
217
  .description('Run Mint in workspace-aware coding mode for the current project')
@@ -110,8 +223,8 @@ program
110
223
  try {
111
224
  const result = await executeCodeTask(task, {
112
225
  cwd: process.cwd(),
113
- onProgress: (message) => {
114
- console.log(`${colors.gray}[Mint Code] ${message}${colors.reset}`);
226
+ onProgress: (info) => {
227
+ console.log(formatProgress(info));
115
228
  },
116
229
  requestApproval: requestCodeApproval
117
230
  });
@@ -133,7 +246,7 @@ program.parse(process.argv);
133
246
  */
134
247
  async function startInteractiveChat(initialMessage = null) {
135
248
  let lastResponseText = "";
136
- const { screen, appendMessage, streamMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode } = createChatUI({
249
+ const { screen, appendMessage, streamMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode, appendCodeStep, updateWorkspace, askUser } = createChatUI({
137
250
  onSubmit: async (text) => {
138
251
  if (text.startsWith('/')) {
139
252
  if (text.startsWith('/agent')) {
@@ -145,7 +258,7 @@ async function startInteractiveChat(initialMessage = null) {
145
258
  if (success) {
146
259
  const agent = agentOrchestrator.getCurrentAgent();
147
260
  appendMessage('system', `Switched to Agent: ${agent.icon} ${agent.name}`);
148
- updateStatusModel(null, agent.name); // Pass name to status bar
261
+ updateStatusModel(agent.name); // Pass name to status bar
149
262
  resetChat(); // Reset to apply new system prompt
150
263
  } else {
151
264
  appendMessage('error', `Agent "${args[1]}" not found. Try /agent list`);
@@ -192,9 +305,25 @@ async function startInteractiveChat(initialMessage = null) {
192
305
  } else {
193
306
  appendMessage('error', `Workspace "${name}" not found.`);
194
307
  }
308
+ } else if (subCmd === 'use' || subCmd === 'switch') {
309
+ const name = args[2];
310
+ const all = workspaceManager.listWorkspaces();
311
+ if (all[name]) {
312
+ const newPath = all[name].path;
313
+ try {
314
+ process.chdir(newPath);
315
+ updateWorkspace(newPath);
316
+ appendMessage('system', `✓ Switched to workspace "${name}" at ${newPath}`);
317
+ resetChat();
318
+ } catch (e) {
319
+ appendMessage('error', `Failed to change directory: ${e.message}`);
320
+ }
321
+ } else {
322
+ appendMessage('error', `Workspace "${name}" not found. Try /workspace list`);
323
+ }
195
324
  } else {
196
325
  const ws = workspaceManager.getWorkspaceByPath(process.cwd());
197
- appendMessage('system', ws ? `Current Workspace: ${ws.name}\nPath: ${ws.path}` : "Not currently in a registered workspace.\nUsage: /workspace <add|list|remove>");
326
+ appendMessage('system', ws ? `Current Workspace: ${ws.name}\nPath: ${ws.path}` : `Not currently in a registered workspace.\nActive Path: ${process.cwd()}\nUsage: /workspace <add|use|list|remove>`);
198
327
  }
199
328
  return;
200
329
  }
@@ -211,27 +340,15 @@ async function startInteractiveChat(initialMessage = null) {
211
340
  // Other slash commands
212
341
  const fakeRl = { close: () => { } };
213
342
  appendMessage('user', text);
214
- await handleSlashCommandUI(text, appendMessage, updateStatusModel, copyLastResponse, setThinking, requestApproval, setMode);
343
+ await handleSlashCommandUI(text, appendMessage, updateStatusModel, copyLastResponse, setThinking, requestApproval, setMode, appendCodeStep, updateWorkspace);
215
344
  return;
216
345
  }
217
346
  }
218
347
  appendMessage('user', text);
219
348
 
220
- const routeDecision = await detectCodeIntent(text, process.cwd());
221
- if (routeDecision.route === 'code') {
222
- appendMessage('system', `Router: entering Code Mode. ${routeDecision.reason}`);
223
- await runChatRoutedTask(text, {
224
- appendMessage,
225
- setThinking,
226
- requestApproval,
227
- setMode
228
- });
229
- return;
230
- }
231
-
232
- setMode('Chat');
349
+ const transcript = await getChatTranscript();
350
+ if (setMode) setMode('Agent');
233
351
 
234
- // Start thinking timer
235
352
  let seconds = 0;
236
353
  setThinking(true, seconds);
237
354
  const timer = setInterval(() => {
@@ -241,96 +358,31 @@ async function startInteractiveChat(initialMessage = null) {
241
358
 
242
359
  try {
243
360
  const config = require('./src/System/config_manager').readConfig();
244
- const provider = config.aiProvider || 'gemini';
245
- const currentAgent = agentOrchestrator.getCurrentAgent();
246
- updateStatusModel(null, currentAgent.name);
247
- if (provider === 'gemini') {
248
- // ── Streaming path (Gemini only) ──────────────────────────────────
249
- // Gemini returns JSON so we buffer all chunks and progressively
250
- // extract the "response" field as more of the JSON arrives.
251
- clearInterval(timer);
252
-
253
- let jsonBuffer = '';
254
- let finalParsed = null;
255
- let streamer = null;
256
- let displayedChars = 0; // chars of response text already sent to TUI
257
-
258
- try {
259
- for await (const event of handleGeminiChatStream(text)) {
260
- if (event.chunk) {
261
- jsonBuffer += event.chunk;
262
-
263
- // Progressively extract readable text from the growing JSON buffer
264
- const match = jsonBuffer.match(/"response"\s*:\s*"((?:[^"\\]|\\.)*)"/s);
265
- if (match) {
266
- const fullText = match[1]
267
- .replace(/\\n/g, '\n')
268
- .replace(/\\"/g, '"')
269
- .replace(/\\\\/g, '\\');
270
- const newChars = fullText.slice(displayedChars);
271
- if (newChars.length > 0) {
272
- if (!streamer) {
273
- setThinking(false);
274
- streamer = streamMessage('assistant');
275
- }
276
- streamer.appendChunk(newChars);
277
- displayedChars = fullText.length;
278
- }
279
- }
280
- } else if (event.done) {
281
- finalParsed = event.parsed;
282
- // Flush any remaining response text not yet displayed
283
- if (finalParsed && finalParsed.response) {
284
- const remaining = finalParsed.response.slice(displayedChars);
285
- if (!streamer) {
286
- setThinking(false);
287
- streamer = streamMessage('assistant');
288
- }
289
- if (remaining) streamer.appendChunk(remaining);
290
- }
291
- if (streamer) {
292
- streamer.finalize(event.timestamp);
293
- } else {
294
- setThinking(false);
295
- appendMessage('assistant',
296
- finalParsed ? finalParsed.response : '',
297
- event.timestamp);
298
- }
299
- }
300
- }
301
- } catch (streamErr) {
302
- setThinking(false);
303
- appendMessage('error', streamErr.message);
304
- return;
305
- }
361
+ const availableProviders = require('./src/System/config_manager').getAvailableProviders(config);
362
+ const preferredProvider = require('./src/CLI/code_agent')._helpers.selectSupportedCodeProvider(config, availableProviders);
306
363
 
307
- // Execute Actions from the final parsed response
308
- if (finalParsed) {
309
- const { executeAction } = require('./mint-cli-logic');
310
- if (finalParsed.action && finalParsed.action.type !== 'none') {
311
- const result = await executeAction(finalParsed.action);
312
- if (result) appendMessage('system', `Action: ${result}`);
313
- }
364
+ const result = await executeCodeTask(text, {
365
+ cwd: process.cwd(),
366
+ requestApproval,
367
+ askUser,
368
+ provider: preferredProvider,
369
+ history: transcript,
370
+ onProgress: (info) => {
371
+ if (appendCodeStep) appendCodeStep(info);
314
372
  }
373
+ });
374
+
375
+ clearInterval(timer);
376
+ setThinking(false);
377
+ lastResponseText = result.summary;
378
+ appendMessage('assistant', result.summary);
315
379
 
316
- } else {
317
- // ── Non-streaming fallback (Ollama, Anthropic, OpenAI, etc.) ──
318
- const response = await handleChat(text);
319
- clearInterval(timer);
320
- setThinking(false);
321
- lastResponseText = response.response;
322
- appendMessage('assistant', response.response, response.timestamp);
323
-
324
- const { executeAction } = require('./mint-cli-logic');
325
- if (response.action && response.action.type !== 'none') {
326
- const result = await executeAction(response.action);
327
- if (result) appendMessage('system', `Action: ${result}`);
328
- }
329
- }
330
380
  } catch (err) {
331
381
  clearInterval(timer);
332
382
  setThinking(false);
333
383
  appendMessage('error', err.message);
384
+ } finally {
385
+ if (setMode) setMode('Chat');
334
386
  }
335
387
  },
336
388
  onExit: () => {
@@ -346,30 +398,43 @@ async function startInteractiveChat(initialMessage = null) {
346
398
  // Handle initial message if passed via CLI arg
347
399
  if (initialMessage) {
348
400
  appendMessage('user', initialMessage);
349
- const routeDecision = await detectCodeIntent(initialMessage, process.cwd());
350
- if (routeDecision.route === 'code') {
351
- appendMessage('system', `Router: entering Code Mode. ${routeDecision.reason}`);
352
- await runChatRoutedTask(initialMessage, {
353
- appendMessage,
354
- setThinking,
401
+ const transcript = await getChatTranscript();
402
+ if (setMode) setMode('Agent');
403
+
404
+ let seconds = 0;
405
+ setThinking(true, seconds);
406
+ const timer = setInterval(() => {
407
+ seconds++;
408
+ setThinking(true, seconds);
409
+ }, 1000);
410
+
411
+ try {
412
+ const config = require('./src/System/config_manager').readConfig();
413
+ const availableProviders = require('./src/System/config_manager').getAvailableProviders(config);
414
+ const preferredProvider = require('./src/CLI/code_agent')._helpers.selectSupportedCodeProvider(config, availableProviders);
415
+
416
+ const result = await executeCodeTask(initialMessage, {
417
+ cwd: process.cwd(),
355
418
  requestApproval,
356
- setMode
419
+ askUser,
420
+ provider: preferredProvider,
421
+ history: transcript,
422
+ onProgress: (info) => {
423
+ if (appendCodeStep) appendCodeStep(info);
424
+ }
357
425
  });
358
- } else {
359
- setMode('Chat');
360
- let seconds = 0;
361
- setThinking(true, seconds);
362
- const timer = setInterval(() => { seconds++; setThinking(true, seconds); }, 1000);
363
- try {
364
- const response = await handleChat(initialMessage);
365
- clearInterval(timer);
366
- setThinking(false);
367
- appendMessage('assistant', response.response, response.timestamp);
368
- } catch (err) {
369
- clearInterval(timer);
370
- setThinking(false);
371
- appendMessage('error', err.message);
372
- }
426
+
427
+ clearInterval(timer);
428
+ setThinking(false);
429
+ lastResponseText = result.summary;
430
+ appendMessage('assistant', result.summary);
431
+
432
+ } catch (err) {
433
+ clearInterval(timer);
434
+ setThinking(false);
435
+ appendMessage('error', err.message);
436
+ } finally {
437
+ if (setMode) setMode('Chat');
373
438
  }
374
439
  }
375
440
  }
@@ -377,7 +442,7 @@ async function startInteractiveChat(initialMessage = null) {
377
442
  /**
378
443
  * Handles slash commands within the TUI context
379
444
  */
380
- async function handleSlashCommandUI(input, appendMessage, updateStatusModel, copyLastResponse, setThinking, requestApproval, setMode) {
445
+ async function handleSlashCommandUI(input, appendMessage, updateStatusModel, copyLastResponse, setThinking, requestApproval, setMode, appendCodeStep, updateWorkspace) {
381
446
  const parts = input.split(' ');
382
447
  const command = parts[0].toLowerCase();
383
448
  const args = parts.slice(1);
@@ -387,16 +452,36 @@ async function handleSlashCommandUI(input, appendMessage, updateStatusModel, cop
387
452
  case '/?':
388
453
  appendMessage('system', [
389
454
  'Mint Slash Commands:',
390
- ' /code <task> — Force workspace Code Mode',
391
- ' /models [name] List or switch Gemini models',
392
- ' /config Show current configuration',
393
- ' /copy Copy last response to clipboard',
394
- ' /clear Clear conversation history',
395
- ' /reset Reset conversation history',
396
- ' /exitExit Mint'
455
+ ' /code <task> — Force workspace Code Mode',
456
+ ' /cd <path> Change current working directory',
457
+ ' /models [name] List or switch Gemini models',
458
+ ' /config Show current configuration',
459
+ ' /copy Copy last response to clipboard',
460
+ ' /clear Clear conversation history',
461
+ ' /resetReset conversation history',
462
+ ' /exit — Exit Mint'
397
463
  ].join('\n'));
398
464
  break;
399
465
 
466
+ case '/cd':
467
+ if (args.length === 0) {
468
+ appendMessage('system', `Current Directory: ${process.cwd()}`);
469
+ break;
470
+ }
471
+ try {
472
+ const newPath = path.resolve(process.cwd(), args[0]);
473
+ if (fs.existsSync(newPath) && fs.lstatSync(newPath).isDirectory()) {
474
+ process.chdir(newPath);
475
+ if (updateWorkspace) updateWorkspace(newPath);
476
+ appendMessage('system', `✓ Directory changed to: ${newPath}`);
477
+ } else {
478
+ appendMessage('error', `Directory not found: ${newPath}`);
479
+ }
480
+ } catch (err) {
481
+ appendMessage('error', `Error: ${err.message}`);
482
+ }
483
+ break;
484
+
400
485
  case '/model':
401
486
  case '/models':
402
487
  const config = readConfig();
@@ -454,8 +539,9 @@ async function handleSlashCommandUI(input, appendMessage, updateStatusModel, cop
454
539
  await runChatRoutedTask(`/code ${args.join(' ')}`, {
455
540
  appendMessage,
456
541
  setThinking,
457
- requestApproval,
458
- setMode
542
+ appendCodeStep,
543
+ setMode,
544
+ history: await getChatTranscript()
459
545
  });
460
546
  break;
461
547
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pheem49/mint",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
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": {
@@ -58,11 +58,13 @@ Always respond exactly with valid JSON containing NO MARKDOWN FORMATTING (do not
58
58
  {
59
59
  "response": "Your conversational reply here (Matches user language).",
60
60
  "action": {
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",
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",
62
62
 
63
63
  "pluginName": "only if type is plugin",
64
64
  "server": "only if type is mcp_tool (server name)",
65
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,
66
68
  "x": 0-1000, // required for mouse_click and mouse_move
67
69
  "y": 0-1000, // required for mouse_click and mouse_move
68
70
  "button": 1 | 2 | 3, // optional for mouse_click, 1=left, 2=middle, 3=right
@@ -86,6 +88,12 @@ Output: { "response": "สวัสดีค่ะ! หนูชื่อมิ
86
88
  Input: "Create a folder named Projects"
87
89
  Output: { "response": "Sure thing! I'm creating a folder named 'Projects' for you right now.", "action": { "type": "create_folder", "target": "Projects" } }
88
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
+
89
97
  Input: "วันนี้วันที่เท่าไร" or "What date is today?" or "today's date" or "วันเวลา"
90
98
  Output: { "response": "แป๊บนึงนะคะ มิ้นท์จะดูให้ค่า", "action": { "type": "system_info", "target": "" } }
91
99
 
@@ -165,6 +173,13 @@ function resolveGeminiModel() {
165
173
  }
166
174
  }
167
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
+
168
183
  // Chat session — maintains conversation history within the session
169
184
  let chat = null;
170
185
  let activeModel = resolveGeminiModel();
@@ -214,21 +229,6 @@ function shouldUseKnowledgeSearch(message) {
214
229
  async function handleChat(message, base64Image = null, base64Audio = null) {
215
230
  try {
216
231
  const config = readConfig();
217
- const provider = config.aiProvider || 'gemini';
218
-
219
- // Ensure API Key is loaded and Client is initialized before every chat
220
- const currentKey = resolveApiKey();
221
- if (!currentKey) {
222
- return {
223
- response: "I couldn't find your Gemini API Key. Please run 'mint onboard' to set it up!",
224
- action: { type: "none", target: "" }
225
- };
226
- }
227
-
228
- if (!ai || activeApiKey !== currentKey) {
229
- initAiClient();
230
- createChat(readChatHistory());
231
- }
232
232
 
233
233
  let finalMessage = message;
234
234
 
@@ -245,13 +245,7 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
245
245
  }
246
246
  }
247
247
 
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);
248
+ const providersToTry = getProviderAttemptOrder(config);
255
249
 
256
250
  for (let i = 0; i < providersToTry.length; i++) {
257
251
  const currentProv = providersToTry[i];
@@ -272,6 +266,23 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
272
266
  return await handleHuggingFaceChat(finalMessage, base64Image, config);
273
267
  }
274
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
+
275
286
  return await handleGeminiChat(finalMessage, base64Image, base64Audio);
276
287
  } catch (error) {
277
288
  console.error(`[Fallback System] Provider '${currentProv}' failed:`, error.message);
@@ -887,5 +898,8 @@ module.exports = {
887
898
  resetChat,
888
899
  getChatTranscript,
889
900
  translateImageContent,
890
- refreshApiKeyFromConfig
901
+ refreshApiKeyFromConfig,
902
+ _helpers: {
903
+ getProviderAttemptOrder
904
+ }
891
905
  };
@@ -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
 
@@ -1,7 +1,4 @@
1
1
  const { GoogleGenAI } = require('@google/genai');
2
- const path = require('path');
3
- const fs = require('fs');
4
- const { app } = require('electron');
5
2
  const { readConfig } = require('../System/config_manager');
6
3
 
7
4
  // ============================================================
@@ -76,11 +73,8 @@ function resolveGeminiModel() {
76
73
 
77
74
  function getMinSuggestionIntervalMs() {
78
75
  try {
79
- const CONFIG_PATH = path.join(app.getPath('userData'), 'mint-config.json');
80
- if (fs.existsSync(CONFIG_PATH)) {
81
- const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
82
- return (cfg.proactiveCooldown || 120) * 1000;
83
- }
76
+ const cfg = readConfig();
77
+ return (cfg.proactiveCooldown || 120) * 1000;
84
78
  } catch {
85
79
  // ignore
86
80
  }