@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.
@@ -4,43 +4,82 @@ 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';
12
40
  const MAX_TOOL_OUTPUT = 12000;
13
41
  const MAX_AGENT_STEPS = 16;
42
+ const MAX_JSON_REPAIR_ATTEMPTS = 2;
43
+ const SUPPORTED_CODE_PROVIDERS = ['gemini', 'anthropic', 'openai', 'local_openai'];
14
44
 
15
- 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.
16
47
 
17
- You help with software development tasks inside the provided working directory.
18
- 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.
19
55
 
20
56
  Rules:
21
57
  1. Respond with valid JSON only.
22
- 2. Prefer reading files and searching before editing.
23
- 3. Make focused edits that preserve existing project style.
24
- 4. Use shell commands for inspection, tests, and formatting when useful.
25
- 5. Never use destructive commands like "rm -rf", "git reset --hard", or overwrite unrelated files.
26
- 6. Before any shell command or file patch is executed, the user must approve it. Plan accordingly.
27
- 7. When editing, prefer "apply_patch" with precise hunks over whole-file rewrites.
28
- 8. Use "write_file" only for new files or when a full rewrite is clearly safer.
29
- 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.
30
66
 
31
67
  Response format:
32
68
  {
33
- "thought": "short reasoning",
34
- "action": "list_files" | "read_file" | "search_code" | "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",
35
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",
36
75
  "path": "relative/path",
37
- "query": "search text",
76
+ "type": "file" | "dir" | "any",
38
77
  "command": "shell command",
39
78
  "startLine": 1,
40
79
  "endLine": 120,
41
80
  "content": "full file content for write_file",
42
- "summary": "final summary",
43
- "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)",
44
83
  "sessionSummary": "brief persistent summary for the workspace",
45
84
  "patch": {
46
85
  "path": "relative/path",
@@ -55,13 +94,20 @@ Response format:
55
94
  }
56
95
 
57
96
  Tool notes:
97
+ - "web_search": search the internet for information when you lack knowledge.
58
98
  - "list_files": inspect the workspace or a subdirectory.
59
99
  - "read_file": read a file, optionally with startLine/endLine.
60
100
  - "search_code": search by text or regex-like pattern.
101
+ - "find_path": find files or directories by path/name when the user is looking for a folder, filename, or location.
61
102
  - "run_shell": run a non-destructive command in the workspace.
62
103
  - "apply_patch": update an existing file using one or more exact replacement hunks.
63
104
  - "write_file": create a new file or fully rewrite a file when replacement is not practical.
64
- - "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.
65
111
  `;
66
112
 
67
113
  function truncate(text, max = MAX_TOOL_OUTPUT) {
@@ -81,6 +127,22 @@ function extractJson(text) {
81
127
  }
82
128
  }
83
129
 
130
+ function selectSupportedCodeProvider(config, availableProviders = getAvailableProviders(config || {})) {
131
+ const requestedProvider = (config && config.aiProvider) || 'gemini';
132
+ if (SUPPORTED_CODE_PROVIDERS.includes(requestedProvider) && availableProviders.includes(requestedProvider)) {
133
+ return requestedProvider;
134
+ }
135
+
136
+ const priority = ['anthropic', 'openai', 'gemini', 'local_openai'];
137
+ for (const provider of priority) {
138
+ if (availableProviders.includes(provider)) {
139
+ return provider;
140
+ }
141
+ }
142
+
143
+ return 'gemini';
144
+ }
145
+
84
146
  function resolveWorkspacePath(workspaceRoot, targetPath = '.') {
85
147
  const resolved = path.resolve(workspaceRoot, targetPath);
86
148
  const relative = path.relative(workspaceRoot, resolved);
@@ -104,6 +166,29 @@ async function safeExecFile(command, args, options = {}) {
104
166
  }
105
167
  }
106
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
+
107
192
  async function listFiles(workspaceRoot, targetPath = '.') {
108
193
  const cwd = resolveWorkspacePath(workspaceRoot, targetPath);
109
194
  try {
@@ -119,11 +204,9 @@ async function listFiles(workspaceRoot, targetPath = '.') {
119
204
  if (error.code !== 'ENOENT' && error.stdout) {
120
205
  return truncate(error.stdout);
121
206
  }
122
- const entries = fs.readdirSync(cwd, { withFileTypes: true })
123
- .slice(0, 200)
124
- .map(entry => `${entry.isDirectory() ? '[dir]' : '[file]'} ${path.relative(workspaceRoot, path.join(cwd, entry.name))}`)
125
- .join('\n');
126
- 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)';
127
210
  }
128
211
  }
129
212
 
@@ -153,6 +236,29 @@ async function searchCode(workspaceRoot, query) {
153
236
  if (typeof error.code === 'number' && error.code === 1) {
154
237
  return '(no matches)';
155
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
+ }
156
262
  if (error.stdout) {
157
263
  return truncate(error.stdout);
158
264
  }
@@ -160,6 +266,40 @@ async function searchCode(workspaceRoot, query) {
160
266
  }
161
267
  }
162
268
 
269
+ async function findPaths(workspaceRoot, query, type = 'any') {
270
+ if (!query || !query.trim()) {
271
+ throw new Error('Path search query is required.');
272
+ }
273
+
274
+ const normalizedType = ['file', 'dir', 'any'].includes(type) ? type : 'any';
275
+ const loweredQuery = query.trim().toLowerCase();
276
+ const results = [];
277
+
278
+ function visit(currentPath) {
279
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true });
280
+ for (const entry of entries) {
281
+ const absoluteEntryPath = path.join(currentPath, entry.name);
282
+ const relativeEntryPath = path.relative(workspaceRoot, absoluteEntryPath) || '.';
283
+ const entryType = entry.isDirectory() ? 'dir' : 'file';
284
+ const matchesType = normalizedType === 'any' || normalizedType === entryType;
285
+ const matchesQuery = entry.name.toLowerCase().includes(loweredQuery) || relativeEntryPath.toLowerCase().includes(loweredQuery);
286
+
287
+ if (matchesType && matchesQuery) {
288
+ results.push(`${entryType === 'dir' ? '[dir]' : '[file]'} ${relativeEntryPath}`);
289
+ if (results.length >= 200) return;
290
+ }
291
+
292
+ if (entry.isDirectory() && results.length < 200) {
293
+ visit(absoluteEntryPath);
294
+ if (results.length >= 200) return;
295
+ }
296
+ }
297
+ }
298
+
299
+ visit(workspaceRoot);
300
+ return results.length > 0 ? results.join('\n') : '(no matching paths)';
301
+ }
302
+
163
303
  function assertSafeShell(command) {
164
304
  const blockedPatterns = [
165
305
  /\brm\s+-rf\b/,
@@ -247,7 +387,7 @@ function writeFile(workspaceRoot, targetPath, content) {
247
387
 
248
388
  class UnifiedAgentClient {
249
389
  constructor(provider, config) {
250
- this.provider = provider;
390
+ this.provider = SUPPORTED_CODE_PROVIDERS.includes(provider) ? provider : 'gemini';
251
391
  this.config = config;
252
392
  this.history = [];
253
393
  this.systemInstruction = CODE_AGENT_PROMPT;
@@ -320,13 +460,13 @@ class UnifiedAgentClient {
320
460
  const model = this.config.geminiModel || DEFAULT_GEMINI_MODEL;
321
461
  const ai = new GoogleGenAI({ apiKey });
322
462
 
323
- // Convert history for Gemini
324
- 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 => ({
325
465
  role: m.role === 'assistant' ? 'model' : 'user',
326
- parts: [{ text: m.content }]
466
+ parts: [{ text: String(m.content || '') }]
327
467
  }));
328
468
 
329
- const lastMessage = this.history[this.history.length - 1].content;
469
+ const lastMessage = String(this.history[this.history.length - 1].content || '');
330
470
 
331
471
  const chat = ai.chats.create({
332
472
  model,
@@ -342,6 +482,29 @@ class UnifiedAgentClient {
342
482
  }
343
483
  }
344
484
 
485
+ async function getAgentDecision(client, observation, options = {}) {
486
+ const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
487
+ const step = options.step || 0;
488
+
489
+ let rawText = await client.sendMessage(observation);
490
+ for (let attempt = 0; attempt <= MAX_JSON_REPAIR_ATTEMPTS; attempt++) {
491
+ try {
492
+ return extractJson(rawText);
493
+ } catch (error) {
494
+ if (attempt === MAX_JSON_REPAIR_ATTEMPTS) {
495
+ throw new Error(`Agent returned invalid JSON after ${MAX_JSON_REPAIR_ATTEMPTS + 1} attempts: ${error.message}`);
496
+ }
497
+
498
+ onProgress({ step, phase: 'repairing', action: 'json_repair', message: `invalid JSON response, requesting repair (${attempt + 1}/${MAX_JSON_REPAIR_ATTEMPTS})` });
499
+ rawText = await client.sendMessage([
500
+ 'Your previous response was not valid JSON for Code Mode.',
501
+ 'Reply again with valid JSON only, following the required schema exactly.',
502
+ `Previous response:\n${truncate(rawText, 4000)}`
503
+ ].join('\n'));
504
+ }
505
+ }
506
+ }
507
+
345
508
  function detectPackageManager(workspaceRoot) {
346
509
  if (fs.existsSync(path.join(workspaceRoot, 'package-lock.json'))) return 'npm';
347
510
  if (fs.existsSync(path.join(workspaceRoot, 'pnpm-lock.yaml'))) return 'pnpm';
@@ -384,12 +547,17 @@ async function getGitContext(workspaceRoot) {
384
547
  return { isRepo: true, branch, status, diffSummary };
385
548
  }
386
549
 
387
- async function buildInitialObservation(task, workspaceRoot) {
550
+ async function buildInitialObservation(task, workspaceRoot, history = []) {
388
551
  const session = readWorkspaceSession(workspaceRoot);
389
552
  const gitContext = await getGitContext(workspaceRoot);
390
553
  const testCommands = detectTestCommands(workspaceRoot);
391
554
 
555
+ const contextStr = history.length > 0
556
+ ? `Recent Context:\n${history.slice(-10).map(m => `${m.sender}: ${m.text}`).join('\n')}\n`
557
+ : '';
558
+
392
559
  return [
560
+ contextStr,
393
561
  `Task: ${task}`,
394
562
  `Workspace: ${workspaceRoot}`,
395
563
  `Git branch: ${gitContext.branch}`,
@@ -409,28 +577,39 @@ async function buildInitialObservation(task, workspaceRoot) {
409
577
 
410
578
  async function executeCodeTask(task, options = {}) {
411
579
  const workspaceRoot = path.resolve(options.cwd || process.cwd());
580
+ const history = options.history || [];
412
581
  const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
413
582
  const requestApproval = typeof options.requestApproval === 'function'
414
583
  ? options.requestApproval
415
584
  : async () => true;
585
+ const askUser = typeof options.askUser === 'function'
586
+ ? options.askUser
587
+ : async (q) => `User didn't answer: ${q}`;
416
588
  const config = readConfig();
417
- const provider = options.provider || 'gemini';
589
+ const provider = options.provider || selectSupportedCodeProvider(config);
418
590
  const client = new UnifiedAgentClient(provider, config);
419
591
 
420
- let observation = await buildInitialObservation(task, workspaceRoot);
592
+ let observation = await buildInitialObservation(task, workspaceRoot, history);
421
593
 
422
594
  let finalSummary = '';
423
595
  let finalVerification = '';
424
596
  let finalSessionSummary = '';
597
+ let executedSteps = 0;
425
598
 
426
599
  for (let step = 1; step <= MAX_AGENT_STEPS; step++) {
427
- onProgress(`Step ${step}: thinking`);
428
- const text = await client.sendMessage(observation);
429
- const decision = extractJson(text);
600
+ executedSteps = step;
601
+ onProgress({ step, phase: 'thinking', action: 'thinking' });
602
+ const decision = await getAgentDecision(client, observation, { onProgress, step });
430
603
  const action = decision.action;
431
604
  const input = decision.input || {};
432
605
 
433
- 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
+ });
434
613
 
435
614
  if (action === 'finish') {
436
615
  finalSessionSummary = input.sessionSummary || input.summary || task;
@@ -445,75 +624,130 @@ async function executeCodeTask(task, options = {}) {
445
624
  }
446
625
 
447
626
  let toolResult = '';
448
- switch (action) {
449
- case 'list_files':
450
- toolResult = await listFiles(workspaceRoot, input.path || '.');
451
- break;
452
- case 'read_file':
453
- toolResult = readFileRange(workspaceRoot, input.path, input.startLine, input.endLine);
454
- break;
455
- case 'search_code':
456
- toolResult = await searchCode(workspaceRoot, input.query);
457
- break;
458
- case 'run_shell': {
459
- const approved = await requestApproval({
460
- type: 'shell',
461
- label: input.command,
462
- preview: input.command
463
- });
464
- if (!approved) {
465
- 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);
466
655
  break;
467
656
  }
468
- toolResult = await runShell(workspaceRoot, input.command);
469
- break;
470
- }
471
- case 'apply_patch': {
472
- const patchInput = input.patch || {};
473
- const approved = await requestApproval({
474
- type: 'patch',
475
- label: patchInput.path,
476
- preview: formatPatchPreview(patchInput)
477
- });
478
- if (!approved) {
479
- 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);
480
669
  break;
481
670
  }
482
- toolResult = applyPatch(workspaceRoot, patchInput);
483
- break;
484
- }
485
- case 'write_file': {
486
- const approved = await requestApproval({
487
- type: 'write_file',
488
- label: input.path,
489
- preview: `${input.path}\n${truncate(input.content || '', 800)}`
490
- });
491
- if (!approved) {
492
- 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);
493
682
  break;
494
683
  }
495
- toolResult = writeFile(workspaceRoot, input.path, input.content);
496
- break;
497
- }
498
- default:
499
- 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---`;
500
729
  }
501
730
 
502
731
  observation = [
503
732
  `Previous thought: ${decision.thought || '(none)'}`,
504
733
  `Action: ${action}`,
505
734
  'Observation:',
506
- toolResult
507
- ].join('\n');
508
- }
735
+ formattedToolResult
736
+ ].join('\n'); }
509
737
 
510
- // Check for Agent Collaboration (Review)
511
- if (config.enableAgentCollaboration !== false) {
738
+ // Check for Agent Collaboration (Review) - Disabled by default to save tokens
739
+ if (config.enableAgentCollaboration === true && executedSteps > 8 && finalSummary) {
512
740
  const availableProviders = getAvailableProviders(config);
513
- const altProviders = availableProviders.filter(p => p !== provider && p !== 'ollama' && p !== 'huggingface');
514
- if (altProviders.length > 0 && finalSummary) {
515
- const reviewerProvider = altProviders[0];
516
- onProgress(`Invoking Reviewer Agent (${reviewerProvider})...`);
741
+ // Exclude providers that often need special local setup or are slow/unreliable for tiny reviews
742
+ const altProviders = availableProviders.filter(p => p !== provider && p !== 'ollama' && p !== 'huggingface' && p !== 'local_openai');
743
+
744
+ // Fallback to provider itself if no other good ones exist, or pick the best available
745
+ const reviewerProvider = altProviders.length > 0
746
+ ? altProviders[0]
747
+ : (availableProviders.includes('gemini') ? 'gemini' : availableProviders[0]);
748
+
749
+ if (reviewerProvider && finalSummary) {
750
+ onProgress({ phase: 'reviewing', action: 'reviewer_start', message: `Invoking Reviewer Agent (${reviewerProvider})...` });
517
751
 
518
752
  const reviewerClient = new UnifiedAgentClient(reviewerProvider, config);
519
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.";
@@ -527,7 +761,7 @@ async function executeCodeTask(task, options = {}) {
527
761
 
528
762
  finalSummary += `\n\n[Review by ${reviewerProvider}]\n${reviewInput.summary || reviewDecision.thought || 'Looks good.'}`;
529
763
  } catch (e) {
530
- onProgress(`Reviewer Agent failed: ${e.message}`);
764
+ onProgress({ phase: 'reviewing', action: 'reviewer_error', message: `Reviewer Agent failed: ${e.message}` });
531
765
  }
532
766
  }
533
767
  }
@@ -536,7 +770,7 @@ async function executeCodeTask(task, options = {}) {
536
770
  return {
537
771
  summary: finalSummary,
538
772
  verification: finalVerification,
539
- steps: MAX_AGENT_STEPS
773
+ steps: executedSteps
540
774
  };
541
775
  }
542
776
 
@@ -549,8 +783,18 @@ async function executeCodeTask(task, options = {}) {
549
783
  return {
550
784
  summary: 'Stopped after reaching the maximum number of agent steps.',
551
785
  verification: 'Agent limit reached before explicit completion.',
552
- steps: MAX_AGENT_STEPS
786
+ steps: executedSteps || MAX_AGENT_STEPS
553
787
  };
554
788
  }
555
789
 
556
- module.exports = { executeCodeTask };
790
+ module.exports = {
791
+ executeCodeTask,
792
+ _helpers: {
793
+ extractJson,
794
+ selectSupportedCodeProvider,
795
+ findPaths,
796
+ listFiles,
797
+ searchCode,
798
+ walkDirectory
799
+ }
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 = [