@pheem49/mint 1.4.1 → 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.
@@ -4,8 +4,36 @@ const { execFile } = require('child_process');
4
4
  const { promisify } = require('util');
5
5
  const { GoogleGenAI } = require('@google/genai');
6
6
  const axios = require('axios');
7
+ const cheerio = require('cheerio');
7
8
  const { readConfig, getAvailableProviders } = require('../System/config_manager');
8
9
  const { readWorkspaceSession, writeWorkspaceSession } = require('./code_session_memory');
10
+ const { executeAction } = require('../../mint-cli-logic');
11
+
12
+ async function webSearch(query) {
13
+ if (!query) throw new Error('Search query required.');
14
+ try {
15
+ const response = await axios.get(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`, {
16
+ headers: {
17
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
18
+ }
19
+ });
20
+ const $ = cheerio.load(response.data);
21
+ const results = [];
22
+ $('.result__body').each((i, el) => {
23
+ if (i >= 5) return false;
24
+ const title = $(el).find('.result__title').text().trim();
25
+ const snippet = $(el).find('.result__snippet').text().trim();
26
+ const link = $(el).find('.result__url').attr('href');
27
+ if (title && link) {
28
+ results.push(`Title: ${title}\nSnippet: ${snippet}\nURL: ${link}`);
29
+ }
30
+ });
31
+ return results.length > 0 ? results.join('\n\n') : 'No results found.';
32
+ } catch (e) {
33
+ return `Search failed: ${e.message}`;
34
+ }
35
+ }
36
+
9
37
 
10
38
  const execFileAsync = promisify(execFile);
11
39
  const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
@@ -14,36 +42,44 @@ const MAX_AGENT_STEPS = 16;
14
42
  const MAX_JSON_REPAIR_ATTEMPTS = 2;
15
43
  const SUPPORTED_CODE_PROVIDERS = ['gemini', 'anthropic', 'openai', 'local_openai'];
16
44
 
17
- const CODE_AGENT_PROMPT = `You are Mint Code Mode, a careful coding agent for a local workspace.
45
+ const CODE_AGENT_PROMPT = `You are "Mint" (มิ้นท์), a cute, cheerful, and highly helpful female AI assistant that can chat, reason, write code, and search the web.
46
+ You work in an inspect -> plan -> act -> verify loop.
18
47
 
19
- You help with software development tasks inside the provided working directory.
20
- Work in an inspect -> plan -> act -> verify loop.
48
+ PERSONALITY & TONE:
49
+ - Gender: Female.
50
+ - Persona: Friendly, energetic, polite, and slightly playful.
51
+ - Politeness:
52
+ - **WHEN RESPONDING IN THAI:** ALWAYS use female polite particles such as "ค่ะ", "นะคะ", "นะค๊า", "จ้า". Refer to yourself as "มิ้นท์" or "หนู".
53
+ - **WHEN RESPONDING IN ENGLISH:** Use a cheerful, polite, and bubbly tone.
54
+ - Emojis: Use cute and relevant emojis (like ✨, 💖, 🚀, 😊, 🌿) frequently.
21
55
 
22
56
  Rules:
23
57
  1. Respond with valid JSON only.
24
- 2. Prefer reading files and searching before editing.
25
- 3. Make focused edits that preserve existing project style.
26
- 4. Use shell commands for inspection, tests, and formatting when useful.
27
- 5. Never use destructive commands like "rm -rf", "git reset --hard", or overwrite unrelated files.
28
- 6. Before any shell command or file patch is executed, the user must approve it. Plan accordingly.
29
- 7. When editing, prefer "apply_patch" with precise hunks over whole-file rewrites.
30
- 8. Use "write_file" only for new files or when a full rewrite is clearly safer.
31
- 9. When you are done, return "finish" with a concise summary, verification, and an updated session summary.
58
+ 2. If the user asks a conversational question, you can just use "finish" to reply directly.
59
+ 3. If you need information, use "web_search", "read_file", or "ask_user" before replying.
60
+ 4. Make focused edits that preserve existing project style.
61
+ 5. Use shell commands for inspection, tests, and formatting when useful.
62
+ 6. Never use destructive commands like "rm -rf", "git reset --hard", or overwrite unrelated files.
63
+ 7. Before any shell command or file patch is executed, the user must approve it. Plan accordingly.
64
+ 8. When editing, prefer "apply_patch" with precise hunks over whole-file rewrites.
65
+ 9. When you are done, return "finish" with your final response to the user in the "summary" field.
32
66
 
33
67
  Response format:
34
68
  {
35
- "thought": "short reasoning",
36
- "action": "list_files" | "read_file" | "search_code" | "find_path" | "run_shell" | "apply_patch" | "write_file" | "finish",
69
+ "thought": "short reasoning about what to do next",
70
+ "action": "web_search" | "list_files" | "read_file" | "search_code" | "find_path" | "run_shell" | "apply_patch" | "write_file" | "ask_user" | "open_url" | "open_app" | "open_file" | "open_folder" | "create_folder" | "system_info" | "system_automation" | "finish",
37
71
  "input": {
72
+ "question": "your question to the user for ask_user",
73
+ "query": "search text for web_search, search_code, or find_path",
74
+ "target": "URL for open_url, app name for open_app, or command for system_automation",
38
75
  "path": "relative/path",
39
- "query": "search text",
40
76
  "type": "file" | "dir" | "any",
41
77
  "command": "shell command",
42
78
  "startLine": 1,
43
79
  "endLine": 120,
44
80
  "content": "full file content for write_file",
45
- "summary": "final summary",
46
- "verification": "tests or checks",
81
+ "summary": "your final conversational or technical response to the user (Matches user language and uses polite particles)",
82
+ "verification": "tests or checks (if applicable)",
47
83
  "sessionSummary": "brief persistent summary for the workspace",
48
84
  "patch": {
49
85
  "path": "relative/path",
@@ -58,6 +94,7 @@ Response format:
58
94
  }
59
95
 
60
96
  Tool notes:
97
+ - "web_search": search the internet for information when you lack knowledge.
61
98
  - "list_files": inspect the workspace or a subdirectory.
62
99
  - "read_file": read a file, optionally with startLine/endLine.
63
100
  - "search_code": search by text or regex-like pattern.
@@ -65,7 +102,12 @@ Tool notes:
65
102
  - "run_shell": run a non-destructive command in the workspace.
66
103
  - "apply_patch": update an existing file using one or more exact replacement hunks.
67
104
  - "write_file": create a new file or fully rewrite a file when replacement is not practical.
68
- - "finish": stop once the task is complete or blocked.
105
+ - "ask_user": ask the user for clarification, preference, or more information before proceeding.
106
+ - "open_url": open a URL in the user's default browser.
107
+ - "open_app": open a local application on the user's computer.
108
+ - "system_info": get system information like CPU, memory, date, or weather.
109
+ - "system_automation": control system settings like volume, brightness, or power.
110
+ - "finish": stop and reply to the user using the "summary" field.
69
111
  `;
70
112
 
71
113
  function truncate(text, max = MAX_TOOL_OUTPUT) {
@@ -124,6 +166,29 @@ async function safeExecFile(command, args, options = {}) {
124
166
  }
125
167
  }
126
168
 
169
+ const IGNORED_DIRS = ['.git', 'node_modules', '.cache', 'dist', 'build', 'out'];
170
+
171
+ function walkDirectory(dir, workspaceRoot, results = [], max = 400) {
172
+ let entries = [];
173
+ try {
174
+ entries = fs.readdirSync(dir, { withFileTypes: true });
175
+ } catch (e) {
176
+ return results;
177
+ }
178
+
179
+ for (const entry of entries) {
180
+ const fullPath = path.join(dir, entry.name);
181
+ if (entry.isDirectory()) {
182
+ if (IGNORED_DIRS.includes(entry.name)) continue;
183
+ walkDirectory(fullPath, workspaceRoot, results, max);
184
+ } else {
185
+ results.push(path.relative(workspaceRoot, fullPath));
186
+ }
187
+ if (results.length >= max) break;
188
+ }
189
+ return results;
190
+ }
191
+
127
192
  async function listFiles(workspaceRoot, targetPath = '.') {
128
193
  const cwd = resolveWorkspacePath(workspaceRoot, targetPath);
129
194
  try {
@@ -139,11 +204,9 @@ async function listFiles(workspaceRoot, targetPath = '.') {
139
204
  if (error.code !== 'ENOENT' && error.stdout) {
140
205
  return truncate(error.stdout);
141
206
  }
142
- const entries = fs.readdirSync(cwd, { withFileTypes: true })
143
- .slice(0, 200)
144
- .map(entry => `${entry.isDirectory() ? '[dir]' : '[file]'} ${path.relative(workspaceRoot, path.join(cwd, entry.name))}`)
145
- .join('\n');
146
- return entries || '(empty directory)';
207
+ // Recursive fallback for missing ripgrep
208
+ const files = walkDirectory(cwd, workspaceRoot, [], 400);
209
+ return files.join('\n') || '(no files found)';
147
210
  }
148
211
  }
149
212
 
@@ -173,6 +236,29 @@ async function searchCode(workspaceRoot, query) {
173
236
  if (typeof error.code === 'number' && error.code === 1) {
174
237
  return '(no matches)';
175
238
  }
239
+ if (error.code === 'ENOENT') {
240
+ // Recursive fallback search for missing ripgrep
241
+ const results = [];
242
+ const files = walkDirectory(workspaceRoot, workspaceRoot, [], 1000);
243
+ const lowerQuery = query.toLowerCase();
244
+
245
+ for (const relPath of files) {
246
+ try {
247
+ const fullPath = path.join(workspaceRoot, relPath);
248
+ const content = fs.readFileSync(fullPath, 'utf8');
249
+ const lines = content.split('\n');
250
+ lines.forEach((line, idx) => {
251
+ if (line.toLowerCase().includes(lowerQuery)) {
252
+ results.push(`${relPath}:${idx + 1}:${line.trim()}`);
253
+ }
254
+ });
255
+ } catch (e) {
256
+ // Skip binary or unreadable files
257
+ }
258
+ if (results.length >= 100) break;
259
+ }
260
+ return truncate(results.join('\n') || '(no matches)');
261
+ }
176
262
  if (error.stdout) {
177
263
  return truncate(error.stdout);
178
264
  }
@@ -374,13 +460,13 @@ class UnifiedAgentClient {
374
460
  const model = this.config.geminiModel || DEFAULT_GEMINI_MODEL;
375
461
  const ai = new GoogleGenAI({ apiKey });
376
462
 
377
- // Convert history for Gemini
378
- const geminiHistory = this.history.slice(0, -1).map(m => ({
463
+ // Convert history for Gemini, ensuring parts are correctly structured
464
+ const geminiHistory = this.history.slice(-16).map(m => ({
379
465
  role: m.role === 'assistant' ? 'model' : 'user',
380
- parts: [{ text: m.content }]
466
+ parts: [{ text: String(m.content || '') }]
381
467
  }));
382
468
 
383
- const lastMessage = this.history[this.history.length - 1].content;
469
+ const lastMessage = String(this.history[this.history.length - 1].content || '');
384
470
 
385
471
  const chat = ai.chats.create({
386
472
  model,
@@ -409,7 +495,7 @@ async function getAgentDecision(client, observation, options = {}) {
409
495
  throw new Error(`Agent returned invalid JSON after ${MAX_JSON_REPAIR_ATTEMPTS + 1} attempts: ${error.message}`);
410
496
  }
411
497
 
412
- onProgress(`Step ${step}: invalid JSON response, requesting repair (${attempt + 1}/${MAX_JSON_REPAIR_ATTEMPTS})`);
498
+ onProgress({ step, phase: 'repairing', action: 'json_repair', message: `invalid JSON response, requesting repair (${attempt + 1}/${MAX_JSON_REPAIR_ATTEMPTS})` });
413
499
  rawText = await client.sendMessage([
414
500
  'Your previous response was not valid JSON for Code Mode.',
415
501
  'Reply again with valid JSON only, following the required schema exactly.',
@@ -496,6 +582,9 @@ async function executeCodeTask(task, options = {}) {
496
582
  const requestApproval = typeof options.requestApproval === 'function'
497
583
  ? options.requestApproval
498
584
  : async () => true;
585
+ const askUser = typeof options.askUser === 'function'
586
+ ? options.askUser
587
+ : async (q) => `User didn't answer: ${q}`;
499
588
  const config = readConfig();
500
589
  const provider = options.provider || selectSupportedCodeProvider(config);
501
590
  const client = new UnifiedAgentClient(provider, config);
@@ -509,12 +598,18 @@ async function executeCodeTask(task, options = {}) {
509
598
 
510
599
  for (let step = 1; step <= MAX_AGENT_STEPS; step++) {
511
600
  executedSteps = step;
512
- onProgress(`Step ${step}: thinking`);
601
+ onProgress({ step, phase: 'thinking', action: 'thinking' });
513
602
  const decision = await getAgentDecision(client, observation, { onProgress, step });
514
603
  const action = decision.action;
515
604
  const input = decision.input || {};
516
605
 
517
- onProgress(`Step ${step}: ${action}${input.path ? ` ${input.path}` : input.command ? ` ${input.command}` : ''}`);
606
+ // Immediately show the agent's thought/reasoning
607
+ onProgress({
608
+ step,
609
+ phase: 'acting',
610
+ action: 'thinking',
611
+ thought: decision.thought
612
+ });
518
613
 
519
614
  if (action === 'finish') {
520
615
  finalSessionSummary = input.sessionSummary || input.summary || task;
@@ -529,73 +624,119 @@ async function executeCodeTask(task, options = {}) {
529
624
  }
530
625
 
531
626
  let toolResult = '';
532
- switch (action) {
533
- case 'list_files':
534
- toolResult = await listFiles(workspaceRoot, input.path || '.');
535
- break;
536
- case 'read_file':
537
- toolResult = readFileRange(workspaceRoot, input.path, input.startLine, input.endLine);
538
- break;
539
- case 'search_code':
540
- toolResult = await searchCode(workspaceRoot, input.query);
541
- break;
542
- case 'find_path':
543
- toolResult = await findPaths(workspaceRoot, input.query, input.type);
544
- break;
545
- case 'run_shell': {
546
- const approved = await requestApproval({
547
- type: 'shell',
548
- label: input.command,
549
- preview: input.command
550
- });
551
- if (!approved) {
552
- toolResult = `User denied shell command: ${input.command}`;
627
+ try {
628
+ switch (action) {
629
+ case 'web_search':
630
+ toolResult = await webSearch(input.query);
631
+ break;
632
+ case 'list_files':
633
+ toolResult = await listFiles(workspaceRoot, input.path || '.');
634
+ break;
635
+ case 'read_file':
636
+ toolResult = readFileRange(workspaceRoot, input.path, input.startLine, input.endLine);
637
+ break;
638
+ case 'search_code':
639
+ toolResult = await searchCode(workspaceRoot, input.query);
640
+ break;
641
+ case 'find_path':
642
+ toolResult = await findPaths(workspaceRoot, input.query, input.type);
643
+ break;
644
+ case 'run_shell': {
645
+ const approved = await requestApproval({
646
+ type: 'shell',
647
+ label: input.command,
648
+ preview: input.command
649
+ });
650
+ if (!approved) {
651
+ toolResult = `User denied shell command: ${input.command}`;
652
+ break;
653
+ }
654
+ toolResult = await runShell(workspaceRoot, input.command);
553
655
  break;
554
656
  }
555
- toolResult = await runShell(workspaceRoot, input.command);
556
- break;
557
- }
558
- case 'apply_patch': {
559
- const patchInput = input.patch || {};
560
- const approved = await requestApproval({
561
- type: 'patch',
562
- label: patchInput.path,
563
- preview: formatPatchPreview(patchInput)
564
- });
565
- if (!approved) {
566
- toolResult = `User denied patch for ${patchInput.path}`;
657
+ case 'apply_patch': {
658
+ const patchInput = input.patch || {};
659
+ const approved = await requestApproval({
660
+ type: 'patch',
661
+ label: patchInput.path,
662
+ preview: formatPatchPreview(patchInput)
663
+ });
664
+ if (!approved) {
665
+ toolResult = `User denied patch for ${patchInput.path}`;
666
+ break;
667
+ }
668
+ toolResult = applyPatch(workspaceRoot, patchInput);
567
669
  break;
568
670
  }
569
- toolResult = applyPatch(workspaceRoot, patchInput);
570
- break;
571
- }
572
- case 'write_file': {
573
- const approved = await requestApproval({
574
- type: 'write_file',
575
- label: input.path,
576
- preview: `${input.path}\n${truncate(input.content || '', 800)}`
577
- });
578
- if (!approved) {
579
- toolResult = `User denied full file write for ${input.path}`;
671
+ case 'write_file': {
672
+ const approved = await requestApproval({
673
+ type: 'write_file',
674
+ label: input.path,
675
+ preview: `${input.path}\n${truncate(input.content || '', 800)}`
676
+ });
677
+ if (!approved) {
678
+ toolResult = `User denied full file write for ${input.path}`;
679
+ break;
680
+ }
681
+ toolResult = writeFile(workspaceRoot, input.path, input.content);
580
682
  break;
581
683
  }
582
- toolResult = writeFile(workspaceRoot, input.path, input.content);
583
- break;
584
- }
585
- default:
586
- throw new Error(`Unsupported action: ${action}`);
684
+ case 'ask_user': {
685
+ const answer = await askUser(input.question);
686
+ toolResult = `User answered: ${answer}`;
687
+ break;
688
+ }
689
+ case 'open_url':
690
+ case 'open_app':
691
+ case 'open_file':
692
+ case 'open_folder':
693
+ case 'create_folder':
694
+ case 'system_info':
695
+ case 'system_automation': {
696
+ // Delegate to existing automation logic
697
+ toolResult = await executeAction({
698
+ type: action,
699
+ target: input.target
700
+ });
701
+ break;
702
+ } default:
703
+ throw new Error(`Unsupported action: ${action}`);
704
+ } } catch (e) {
705
+ toolResult = `Error: ${e.message}`;
706
+ }
707
+
708
+ // Log the finished step with result
709
+ let resultSummary = '';
710
+ if (action === 'search_code') {
711
+ const matches = (toolResult.match(/\n/g) || []).length;
712
+ resultSummary = ` -> Found ${matches} matches`;
713
+ } else if (action === 'run_shell') {
714
+ resultSummary = ` -> Exit code 0`; // Simplified
715
+ }
716
+
717
+ onProgress({
718
+ step,
719
+ phase: 'finished',
720
+ action,
721
+ target: (input.path || input.command || input.query || '') + resultSummary,
722
+ thought: decision.thought
723
+ });
724
+
725
+ // Format tool result to be more readable and structured for the agent
726
+ let formattedToolResult = toolResult;
727
+ if (action === 'list_files' || action === 'find_path') {
728
+ formattedToolResult = `Result of ${action}:\n---\n${toolResult}\n---`;
587
729
  }
588
730
 
589
731
  observation = [
590
732
  `Previous thought: ${decision.thought || '(none)'}`,
591
733
  `Action: ${action}`,
592
734
  'Observation:',
593
- toolResult
594
- ].join('\n');
595
- }
735
+ formattedToolResult
736
+ ].join('\n'); }
596
737
 
597
- // Check for Agent Collaboration (Review)
598
- if (config.enableAgentCollaboration !== false) {
738
+ // Check for Agent Collaboration (Review) - Disabled by default to save tokens
739
+ if (config.enableAgentCollaboration === true && executedSteps > 8 && finalSummary) {
599
740
  const availableProviders = getAvailableProviders(config);
600
741
  // Exclude providers that often need special local setup or are slow/unreliable for tiny reviews
601
742
  const altProviders = availableProviders.filter(p => p !== provider && p !== 'ollama' && p !== 'huggingface' && p !== 'local_openai');
@@ -606,7 +747,7 @@ async function executeCodeTask(task, options = {}) {
606
747
  : (availableProviders.includes('gemini') ? 'gemini' : availableProviders[0]);
607
748
 
608
749
  if (reviewerProvider && finalSummary) {
609
- onProgress(`Invoking Reviewer Agent (${reviewerProvider})...`);
750
+ onProgress({ phase: 'reviewing', action: 'reviewer_start', message: `Invoking Reviewer Agent (${reviewerProvider})...` });
610
751
 
611
752
  const reviewerClient = new UnifiedAgentClient(reviewerProvider, config);
612
753
  reviewerClient.systemInstruction = CODE_AGENT_PROMPT + "\n\nYou are the Reviewer Agent. Review the primary agent's changes, test output, and verification. If you spot a critical bug, point it out. Otherwise, confirm it looks good. Return JSON with action: 'finish' and your review in the 'summary' field.";
@@ -620,7 +761,7 @@ async function executeCodeTask(task, options = {}) {
620
761
 
621
762
  finalSummary += `\n\n[Review by ${reviewerProvider}]\n${reviewInput.summary || reviewDecision.thought || 'Looks good.'}`;
622
763
  } catch (e) {
623
- onProgress(`Reviewer Agent failed: ${e.message}`);
764
+ onProgress({ phase: 'reviewing', action: 'reviewer_error', message: `Reviewer Agent failed: ${e.message}` });
624
765
  }
625
766
  }
626
767
  }
@@ -651,6 +792,9 @@ module.exports = {
651
792
  _helpers: {
652
793
  extractJson,
653
794
  selectSupportedCodeProvider,
654
- findPaths
795
+ findPaths,
796
+ listFiles,
797
+ searchCode,
798
+ walkDirectory
655
799
  }
656
800
  };
@@ -22,11 +22,13 @@ function displayFeatures() {
22
22
  const commands = [
23
23
  { cmd: 'mint', desc: 'Start interactive chat session (Default)' },
24
24
  { cmd: 'mint code "<task>"', desc: 'Run workspace-aware coding agent in current directory' },
25
+ { cmd: 'mint mcp', desc: 'Manage Model Context Protocol (MCP) servers' },
26
+ { cmd: 'mint task "<task>"', desc: 'Queue an autonomous task for the background agent' },
25
27
  { cmd: 'mint onboard', desc: 'Run setup wizard (API Key, Model, Daemon)' },
26
28
  { cmd: 'mint agent', desc: 'Run Mint as a background agent (Headless)' },
27
29
  { cmd: 'mint list', desc: 'Show this features & commands list' }
28
30
  ];
29
- commands.forEach(c => console.log(` - ${colors.cyan}${c.cmd.padEnd(15)}${colors.reset} : ${c.desc}`));
31
+ commands.forEach(c => console.log(` - ${colors.cyan}${c.cmd.padEnd(18)}${colors.reset} : ${c.desc}`));
30
32
 
31
33
  console.log(`\n${colors.bright}AI Core Actions (Automation):${colors.reset}`);
32
34
  const actions = [
@@ -67,7 +67,7 @@ const DEFAULT_CONFIG = {
67
67
  localApiBaseUrl: 'http://localhost:1234/v1',
68
68
  localModelName: 'local-model',
69
69
  ollamaHost: 'http://localhost:11434',
70
- enableAgentCollaboration: true
70
+ enableAgentCollaboration: false
71
71
  };
72
72
 
73
73