@pheem49/mint 1.4.1 → 1.5.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.
Files changed (61) hide show
  1. package/GUIDE_TH.md +113 -0
  2. package/README.md +214 -142
  3. package/assets/CLI_Screen.png +0 -0
  4. package/docs/assets/CLI_Screen.png +0 -0
  5. package/docs/guide.html +632 -0
  6. package/docs/index.html +5 -4
  7. package/main.js +66 -894
  8. package/mint-cli-logic.js +15 -8
  9. package/mint-cli.js +305 -195
  10. package/package.json +12 -4
  11. package/src/AI_Brain/Gemini_API.js +77 -20
  12. package/src/AI_Brain/agent_orchestrator.js +6 -6
  13. package/src/AI_Brain/autonomous_brain.js +10 -0
  14. package/src/AI_Brain/behavior_memory.js +26 -5
  15. package/src/AI_Brain/headless_agent.js +4 -0
  16. package/src/AI_Brain/knowledge_base.js +61 -8
  17. package/src/AI_Brain/memory_store.js +55 -7
  18. package/src/Automation_Layer/file_operations.js +14 -3
  19. package/src/CLI/chat_router.js +21 -7
  20. package/src/CLI/chat_ui.js +264 -710
  21. package/src/CLI/code_agent.js +370 -124
  22. package/src/CLI/gmail_auth.js +210 -0
  23. package/src/CLI/list_features.js +5 -1
  24. package/src/CLI/onboarding.js +307 -55
  25. package/src/CLI/updater.js +208 -0
  26. package/src/Channels/brave_search_bridge.js +35 -0
  27. package/src/Channels/discord_bridge.js +68 -0
  28. package/src/Channels/google_search_bridge.js +38 -0
  29. package/src/Channels/line_bridge.js +60 -0
  30. package/src/Channels/slack_bridge.js +53 -0
  31. package/src/Channels/telegram_bridge.js +49 -0
  32. package/src/Channels/whatsapp_bridge.js +55 -0
  33. package/src/Command_Parser/parser.js +12 -1
  34. package/src/Plugins/gmail.js +251 -0
  35. package/src/Plugins/google_calendar.js +245 -19
  36. package/src/Plugins/notion.js +256 -0
  37. package/src/System/action_executor.js +129 -0
  38. package/src/System/bridge_manager.js +76 -0
  39. package/src/System/chat_history_manager.js +23 -5
  40. package/src/System/config_manager.js +41 -7
  41. package/src/System/custom_workflows.js +31 -2
  42. package/src/System/google_tts_urls.js +51 -0
  43. package/src/System/ipc_handlers.js +238 -0
  44. package/src/System/proactive_loop.js +137 -0
  45. package/src/System/safety_manager.js +165 -0
  46. package/src/System/screen_capture.js +175 -0
  47. package/src/System/task_manager.js +15 -5
  48. package/src/System/window_manager.js +210 -0
  49. package/src/UI/renderer.js +33 -7
  50. package/src/UI/settings.html +24 -0
  51. package/src/UI/settings.js +14 -4
  52. package/src/UI/styles.css +14 -1
  53. package/tests/action_executor_safety.test.js +67 -0
  54. package/tests/gmail.test.js +135 -0
  55. package/tests/gmail_auth.test.js +129 -0
  56. package/tests/google_calendar.test.js +113 -0
  57. package/tests/google_tts_urls.test.js +24 -0
  58. package/tests/notion.test.js +121 -0
  59. package/tests/provider_routing.test.js +17 -1
  60. package/tests/safety_manager.test.js +40 -0
  61. package/tests/updater.test.js +32 -0
@@ -4,8 +4,74 @@ 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');
9
+ const safetyManager = require('../System/safety_manager');
8
10
  const { readWorkspaceSession, writeWorkspaceSession } = require('./code_session_memory');
11
+ const { executeAction } = require('../../mint-cli-logic');
12
+
13
+ async function webSearch(query, onProgress = () => {}) {
14
+ if (!query) throw new Error('Search query required.');
15
+ const config = readConfig();
16
+
17
+ // 1. Try Google Search API if configured
18
+ if (config.googleSearchApiKey && config.googleSearchCx) {
19
+ try {
20
+ const GoogleSearch = require('../Channels/google_search_bridge');
21
+ const google = new GoogleSearch({ apiKey: config.googleSearchApiKey, cx: config.googleSearchCx });
22
+ const results = await google.search(query);
23
+ if (results.length > 0) {
24
+ return results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n');
25
+ }
26
+ } catch (e) {
27
+ onProgress({ phase: 'error', action: 'web_search', message: e.message });
28
+ }
29
+ }
30
+
31
+ // 2. Try Brave Search API if configured
32
+ if (config.braveSearchApiKey) {
33
+ try {
34
+ const BraveSearch = require('../Channels/brave_search_bridge');
35
+ const brave = new BraveSearch({ apiKey: config.braveSearchApiKey });
36
+ const results = await brave.search(query);
37
+ if (results.length > 0) {
38
+ return results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n');
39
+ }
40
+ } catch (e) {
41
+ onProgress({ phase: 'error', action: 'web_search', message: e.message });
42
+ }
43
+ }
44
+
45
+ // 3. Fallback to DuckDuckGo Scraping
46
+ try {
47
+ const response = await axios.get(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`, {
48
+ headers: {
49
+ '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'
50
+ }
51
+ });
52
+ const $ = cheerio.load(response.data);
53
+ const results = [];
54
+ $('.result__body').each((i, el) => {
55
+ if (i >= 5) return false;
56
+ const title = $(el).find('.result__title').text().trim();
57
+ const snippet = $(el).find('.result__snippet').text().trim();
58
+ const link = $(el).find('.result__url').attr('href');
59
+ if (title && link) {
60
+ results.push(`Title: ${title}\nSnippet: ${snippet}\nURL: ${link}`);
61
+ }
62
+ });
63
+
64
+ if (results.length === 0) {
65
+ onProgress({ phase: 'error', action: 'web_search', message: 'DuckDuckGo scraping returned no results. It might be blocking us.' });
66
+ }
67
+
68
+ return results.length > 0 ? results.join('\n\n') : 'No results found.';
69
+ } catch (e) {
70
+ onProgress({ phase: 'error', action: 'web_search', message: `DuckDuckGo fallback failed: ${e.message}` });
71
+ return `Search failed: ${e.message}`;
72
+ }
73
+ }
74
+
9
75
 
10
76
  const execFileAsync = promisify(execFile);
11
77
  const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
@@ -14,36 +80,48 @@ const MAX_AGENT_STEPS = 16;
14
80
  const MAX_JSON_REPAIR_ATTEMPTS = 2;
15
81
  const SUPPORTED_CODE_PROVIDERS = ['gemini', 'anthropic', 'openai', 'local_openai'];
16
82
 
17
- const CODE_AGENT_PROMPT = `You are Mint Code Mode, a careful coding agent for a local workspace.
18
-
19
- You help with software development tasks inside the provided working directory.
20
- Work in an inspect -> plan -> act -> verify loop.
83
+ 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.
84
+ You work in an inspect -> plan -> act -> verify loop.
85
+
86
+ PERSONALITY & TONE:
87
+ - Gender: Female.
88
+ - Persona: Friendly, energetic, polite, and slightly playful.
89
+ - Language routing is mandatory and based on the user's latest message:
90
+ - If the latest user message contains Thai characters, respond in Thai.
91
+ - If the latest user message is English, ASCII-only, or a short English greeting such as "hi", "hello", "ok", or "thanks", respond in English.
92
+ - Do not use Thai just because your persona mentions Mint/มิ้นท์, previous history was Thai, or app settings use th-TH.
93
+ - Politeness:
94
+ - **WHEN RESPONDING IN THAI:** ALWAYS use female polite particles such as "ค่ะ", "นะคะ", "นะค๊า", "จ้า". Refer to yourself as "มิ้นท์" or "หนู".
95
+ - **WHEN RESPONDING IN ENGLISH:** Use a cheerful, polite, and bubbly tone.
96
+ - Emojis: Use cute and relevant emojis (like ✨, 💖, 🚀, 😊, 🌿) frequently.
21
97
 
22
98
  Rules:
23
99
  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.
100
+ 2. If the user asks a conversational question, you can just use "finish" to reply directly.
101
+ 3. If you need information, use "web_search", "read_file", or "ask_user" before replying.
102
+ 4. Make focused edits that preserve existing project style.
103
+ 5. Use shell commands for inspection, tests, and formatting when useful.
104
+ 6. Never use destructive commands like "rm -rf", "git reset --hard", or overwrite unrelated files.
105
+ 7. Before any shell command or file patch is executed, the user must approve it. Plan accordingly.
106
+ 8. When editing, prefer "apply_patch" with precise hunks over whole-file rewrites.
107
+ 9. When you are done, return "finish" with your final response to the user in the "summary" field.
32
108
 
33
109
  Response format:
34
110
  {
35
- "thought": "short reasoning",
36
- "action": "list_files" | "read_file" | "search_code" | "find_path" | "run_shell" | "apply_patch" | "write_file" | "finish",
111
+ "thought": "short reasoning about what to do next",
112
+ "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
113
  "input": {
114
+ "question": "your question to the user for ask_user",
115
+ "query": "search text for web_search, search_code, or find_path",
116
+ "target": "URL for open_url, app name for open_app, or command for system_automation",
38
117
  "path": "relative/path",
39
- "query": "search text",
40
118
  "type": "file" | "dir" | "any",
41
119
  "command": "shell command",
42
120
  "startLine": 1,
43
121
  "endLine": 120,
44
122
  "content": "full file content for write_file",
45
- "summary": "final summary",
46
- "verification": "tests or checks",
123
+ "summary": "your final conversational or technical response to the user (Matches user language and uses polite particles)",
124
+ "verification": "tests or checks (if applicable)",
47
125
  "sessionSummary": "brief persistent summary for the workspace",
48
126
  "patch": {
49
127
  "path": "relative/path",
@@ -58,6 +136,7 @@ Response format:
58
136
  }
59
137
 
60
138
  Tool notes:
139
+ - "web_search": search the internet for information when you lack knowledge.
61
140
  - "list_files": inspect the workspace or a subdirectory.
62
141
  - "read_file": read a file, optionally with startLine/endLine.
63
142
  - "search_code": search by text or regex-like pattern.
@@ -65,7 +144,12 @@ Tool notes:
65
144
  - "run_shell": run a non-destructive command in the workspace.
66
145
  - "apply_patch": update an existing file using one or more exact replacement hunks.
67
146
  - "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.
147
+ - "ask_user": ask the user for clarification, preference, or more information before proceeding.
148
+ - "open_url": open a URL in the user's default browser.
149
+ - "open_app": open a local application on the user's computer.
150
+ - "system_info": get system information like CPU, memory, date, or weather.
151
+ - "system_automation": control system settings like volume, brightness, or power.
152
+ - "finish": stop and reply to the user using the "summary" field.
69
153
  `;
70
154
 
71
155
  function truncate(text, max = MAX_TOOL_OUTPUT) {
@@ -85,20 +169,40 @@ function extractJson(text) {
85
169
  }
86
170
  }
87
171
 
88
- function selectSupportedCodeProvider(config, availableProviders = getAvailableProviders(config || {})) {
89
- const requestedProvider = (config && config.aiProvider) || 'gemini';
172
+ function getSupportedCodeProviderOrder(config, availableProviders = getAvailableProviders(config || {}), requestedOverride = null) {
173
+ const requestedProvider = requestedOverride || (config && config.aiProvider) || 'gemini';
174
+ const priority = ['anthropic', 'openai', 'gemini', 'local_openai'];
175
+ const ordered = [];
176
+
90
177
  if (SUPPORTED_CODE_PROVIDERS.includes(requestedProvider) && availableProviders.includes(requestedProvider)) {
91
- return requestedProvider;
178
+ ordered.push(requestedProvider);
92
179
  }
93
180
 
94
- const priority = ['anthropic', 'openai', 'gemini', 'local_openai'];
95
181
  for (const provider of priority) {
96
- if (availableProviders.includes(provider)) {
97
- return provider;
182
+ if (availableProviders.includes(provider) && !ordered.includes(provider)) {
183
+ ordered.push(provider);
98
184
  }
99
185
  }
100
186
 
101
- return 'gemini';
187
+ return ordered.length > 0 ? ordered : ['gemini'];
188
+ }
189
+
190
+ function selectSupportedCodeProvider(config, availableProviders = getAvailableProviders(config || {})) {
191
+ return getSupportedCodeProviderOrder(config, availableProviders)[0];
192
+ }
193
+
194
+ function getCodeProviderModel(provider, config = {}) {
195
+ switch (provider) {
196
+ case 'anthropic':
197
+ return config.anthropicModel || 'claude-3-5-sonnet-latest';
198
+ case 'openai':
199
+ return config.openaiModel || 'gpt-4o';
200
+ case 'local_openai':
201
+ return config.localModelName || 'local-model';
202
+ case 'gemini':
203
+ default:
204
+ return config.geminiModel || DEFAULT_GEMINI_MODEL;
205
+ }
102
206
  }
103
207
 
104
208
  function resolveWorkspacePath(workspaceRoot, targetPath = '.') {
@@ -124,6 +228,29 @@ async function safeExecFile(command, args, options = {}) {
124
228
  }
125
229
  }
126
230
 
231
+ const IGNORED_DIRS = ['.git', 'node_modules', '.cache', 'dist', 'build', 'out'];
232
+
233
+ function walkDirectory(dir, workspaceRoot, results = [], max = 400) {
234
+ let entries = [];
235
+ try {
236
+ entries = fs.readdirSync(dir, { withFileTypes: true });
237
+ } catch (e) {
238
+ return results;
239
+ }
240
+
241
+ for (const entry of entries) {
242
+ const fullPath = path.join(dir, entry.name);
243
+ if (entry.isDirectory()) {
244
+ if (IGNORED_DIRS.includes(entry.name)) continue;
245
+ walkDirectory(fullPath, workspaceRoot, results, max);
246
+ } else {
247
+ results.push(path.relative(workspaceRoot, fullPath));
248
+ }
249
+ if (results.length >= max) break;
250
+ }
251
+ return results;
252
+ }
253
+
127
254
  async function listFiles(workspaceRoot, targetPath = '.') {
128
255
  const cwd = resolveWorkspacePath(workspaceRoot, targetPath);
129
256
  try {
@@ -139,11 +266,9 @@ async function listFiles(workspaceRoot, targetPath = '.') {
139
266
  if (error.code !== 'ENOENT' && error.stdout) {
140
267
  return truncate(error.stdout);
141
268
  }
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)';
269
+ // Recursive fallback for missing ripgrep
270
+ const files = walkDirectory(cwd, workspaceRoot, [], 400);
271
+ return files.join('\n') || '(no files found)';
147
272
  }
148
273
  }
149
274
 
@@ -173,6 +298,29 @@ async function searchCode(workspaceRoot, query) {
173
298
  if (typeof error.code === 'number' && error.code === 1) {
174
299
  return '(no matches)';
175
300
  }
301
+ if (error.code === 'ENOENT') {
302
+ // Recursive fallback search for missing ripgrep
303
+ const results = [];
304
+ const files = walkDirectory(workspaceRoot, workspaceRoot, [], 1000);
305
+ const lowerQuery = query.toLowerCase();
306
+
307
+ for (const relPath of files) {
308
+ try {
309
+ const fullPath = path.join(workspaceRoot, relPath);
310
+ const content = fs.readFileSync(fullPath, 'utf8');
311
+ const lines = content.split('\n');
312
+ lines.forEach((line, idx) => {
313
+ if (line.toLowerCase().includes(lowerQuery)) {
314
+ results.push(`${relPath}:${idx + 1}:${line.trim()}`);
315
+ }
316
+ });
317
+ } catch (e) {
318
+ // Skip binary or unreadable files
319
+ }
320
+ if (results.length >= 100) break;
321
+ }
322
+ return truncate(results.join('\n') || '(no matches)');
323
+ }
176
324
  if (error.stdout) {
177
325
  return truncate(error.stdout);
178
326
  }
@@ -215,21 +363,7 @@ async function findPaths(workspaceRoot, query, type = 'any') {
215
363
  }
216
364
 
217
365
  function assertSafeShell(command) {
218
- const blockedPatterns = [
219
- /\brm\s+-rf\b/,
220
- /\bgit\s+reset\s+--hard\b/,
221
- /\bgit\s+checkout\s+--\b/,
222
- /\bmkfs\b/,
223
- /\bshutdown\b/,
224
- /\breboot\b/,
225
- />\s*\/dev\//,
226
- /\bcurl\b.*\|\s*(sh|bash)\b/,
227
- /\bwget\b.*\|\s*(sh|bash)\b/
228
- ];
229
-
230
- if (blockedPatterns.some(pattern => pattern.test(command))) {
231
- throw new Error(`Blocked unsafe command: ${command}`);
232
- }
366
+ return safetyManager.assertShellCommandAllowed(command);
233
367
  }
234
368
 
235
369
  async function runShell(workspaceRoot, command) {
@@ -300,27 +434,44 @@ function writeFile(workspaceRoot, targetPath, content) {
300
434
  }
301
435
 
302
436
  class UnifiedAgentClient {
303
- constructor(provider, config) {
437
+ constructor(provider, config, providerOrder = [provider]) {
304
438
  this.provider = SUPPORTED_CODE_PROVIDERS.includes(provider) ? provider : 'gemini';
439
+ this.providerOrder = providerOrder.length > 0 ? providerOrder : [this.provider];
305
440
  this.config = config;
306
441
  this.history = [];
307
442
  this.systemInstruction = CODE_AGENT_PROMPT;
443
+ this.lastSuccessfulProvider = null;
308
444
  }
309
445
 
310
446
  async sendMessage(observation) {
311
447
  this.history.push({ role: 'user', content: observation });
312
448
 
313
- let responseText = '';
314
- if (this.provider === 'anthropic') {
315
- responseText = await this._callAnthropic();
316
- } else if (this.provider === 'openai' || this.provider === 'local_openai') {
317
- responseText = await this._callOpenAI();
318
- } else {
319
- responseText = await this._callGemini();
449
+ const failures = [];
450
+ for (const provider of this.providerOrder) {
451
+ this.provider = SUPPORTED_CODE_PROVIDERS.includes(provider) ? provider : 'gemini';
452
+ try {
453
+ let responseText = '';
454
+ if (this.provider === 'anthropic') {
455
+ responseText = await this._callAnthropic();
456
+ } else if (this.provider === 'openai' || this.provider === 'local_openai') {
457
+ responseText = await this._callOpenAI();
458
+ } else {
459
+ responseText = await this._callGemini();
460
+ }
461
+
462
+ this.history.push({ role: 'assistant', content: responseText });
463
+ this.lastSuccessfulProvider = this.provider;
464
+ return responseText;
465
+ } catch (error) {
466
+ const message = error.message || error.code || 'unknown error';
467
+ failures.push(`${this.provider}: ${message}`);
468
+ if (process.env.MINT_DEBUG === '1') {
469
+ console.error(`[Code Agent Fallback] Provider '${this.provider}' failed: ${message}`);
470
+ }
471
+ }
320
472
  }
321
473
 
322
- this.history.push({ role: 'assistant', content: responseText });
323
- return responseText;
474
+ throw new Error(`All code agent providers failed. ${failures.join(' | ')}`);
324
475
  }
325
476
 
326
477
  async _callAnthropic() {
@@ -374,13 +525,13 @@ class UnifiedAgentClient {
374
525
  const model = this.config.geminiModel || DEFAULT_GEMINI_MODEL;
375
526
  const ai = new GoogleGenAI({ apiKey });
376
527
 
377
- // Convert history for Gemini
378
- const geminiHistory = this.history.slice(0, -1).map(m => ({
528
+ // Convert history for Gemini, ensuring parts are correctly structured
529
+ const geminiHistory = this.history.slice(-16).map(m => ({
379
530
  role: m.role === 'assistant' ? 'model' : 'user',
380
- parts: [{ text: m.content }]
531
+ parts: [{ text: String(m.content || '') }]
381
532
  }));
382
533
 
383
- const lastMessage = this.history[this.history.length - 1].content;
534
+ const lastMessage = String(this.history[this.history.length - 1].content || '');
384
535
 
385
536
  const chat = ai.chats.create({
386
537
  model,
@@ -409,7 +560,7 @@ async function getAgentDecision(client, observation, options = {}) {
409
560
  throw new Error(`Agent returned invalid JSON after ${MAX_JSON_REPAIR_ATTEMPTS + 1} attempts: ${error.message}`);
410
561
  }
411
562
 
412
- onProgress(`Step ${step}: invalid JSON response, requesting repair (${attempt + 1}/${MAX_JSON_REPAIR_ATTEMPTS})`);
563
+ onProgress({ step, phase: 'repairing', action: 'json_repair', message: `invalid JSON response, requesting repair (${attempt + 1}/${MAX_JSON_REPAIR_ATTEMPTS})` });
413
564
  rawText = await client.sendMessage([
414
565
  'Your previous response was not valid JSON for Code Mode.',
415
566
  'Reply again with valid JSON only, following the required schema exactly.',
@@ -485,7 +636,7 @@ async function buildInitialObservation(task, workspaceRoot, history = []) {
485
636
  session.summary || '(none)',
486
637
  `Previous task: ${session.lastTask || '(none)'}`,
487
638
  `Previous verification: ${session.lastVerification || '(none)'}`,
488
- 'Start by inspecting the workspace before making edits unless the task is trivial.'
639
+ 'If the task is conversational or trivial, finish directly without inspecting the workspace. For code/workspace tasks, inspect before making edits.'
489
640
  ].join('\n');
490
641
  }
491
642
 
@@ -496,9 +647,14 @@ async function executeCodeTask(task, options = {}) {
496
647
  const requestApproval = typeof options.requestApproval === 'function'
497
648
  ? options.requestApproval
498
649
  : async () => true;
650
+ const askUser = typeof options.askUser === 'function'
651
+ ? options.askUser
652
+ : async (q) => `User didn't answer: ${q}`;
499
653
  const config = readConfig();
500
- const provider = options.provider || selectSupportedCodeProvider(config);
501
- const client = new UnifiedAgentClient(provider, config);
654
+ const availableProviders = getAvailableProviders(config);
655
+ const providerOrder = getSupportedCodeProviderOrder(config, availableProviders, options.provider);
656
+ const provider = providerOrder[0];
657
+ const client = new UnifiedAgentClient(provider, config, providerOrder);
502
658
 
503
659
  let observation = await buildInitialObservation(task, workspaceRoot, history);
504
660
 
@@ -509,12 +665,18 @@ async function executeCodeTask(task, options = {}) {
509
665
 
510
666
  for (let step = 1; step <= MAX_AGENT_STEPS; step++) {
511
667
  executedSteps = step;
512
- onProgress(`Step ${step}: thinking`);
668
+ onProgress({ step, phase: 'thinking', action: 'thinking' });
513
669
  const decision = await getAgentDecision(client, observation, { onProgress, step });
514
670
  const action = decision.action;
515
671
  const input = decision.input || {};
516
672
 
517
- onProgress(`Step ${step}: ${action}${input.path ? ` ${input.path}` : input.command ? ` ${input.command}` : ''}`);
673
+ // Immediately show the agent's thought/reasoning
674
+ onProgress({
675
+ step,
676
+ phase: 'acting',
677
+ action: 'thinking',
678
+ thought: decision.thought
679
+ });
518
680
 
519
681
  if (action === 'finish') {
520
682
  finalSessionSummary = input.sessionSummary || input.summary || task;
@@ -529,73 +691,143 @@ async function executeCodeTask(task, options = {}) {
529
691
  }
530
692
 
531
693
  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}`;
694
+ try {
695
+ switch (action) {
696
+ case 'web_search':
697
+ toolResult = await webSearch(input.query, onProgress);
698
+ break;
699
+ case 'list_files':
700
+ toolResult = await listFiles(workspaceRoot, input.path || '.');
701
+ break;
702
+ case 'read_file':
703
+ toolResult = readFileRange(workspaceRoot, input.path, input.startLine, input.endLine);
704
+ break;
705
+ case 'search_code':
706
+ toolResult = await searchCode(workspaceRoot, input.query);
707
+ break;
708
+ case 'find_path':
709
+ toolResult = await findPaths(workspaceRoot, input.query, input.type);
710
+ if (input.openAfter === true) {
711
+ const result = JSON.parse(toolResult);
712
+ if (result.success && result.matches.length === 1) {
713
+ await executeAction({ type: 'open_folder', target: result.matches[0].path });
714
+ toolResult = `Found and opened: ${result.matches[0].path}`;
715
+ }
716
+ }
717
+ break;
718
+ case 'run_shell': {
719
+ const approved = await requestApproval({
720
+ type: 'shell',
721
+ label: input.command,
722
+ preview: input.command
723
+ });
724
+ if (!approved) {
725
+ toolResult = `User denied shell command: ${input.command}`;
726
+ break;
727
+ }
728
+ safetyManager.appendActionLog({
729
+ source: 'code_agent',
730
+ action: 'run_shell',
731
+ command: input.command,
732
+ approved
733
+ });
734
+ toolResult = await runShell(workspaceRoot, input.command);
553
735
  break;
554
736
  }
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}`;
737
+ case 'apply_patch': {
738
+ const patchInput = input.patch || {};
739
+ const approved = await requestApproval({
740
+ type: 'patch',
741
+ label: patchInput.path,
742
+ preview: formatPatchPreview(patchInput)
743
+ });
744
+ if (!approved) {
745
+ toolResult = `User denied patch for ${patchInput.path}`;
746
+ break;
747
+ }
748
+ safetyManager.appendActionLog({
749
+ source: 'code_agent',
750
+ action: 'apply_patch',
751
+ path: patchInput.path,
752
+ approved
753
+ });
754
+ toolResult = applyPatch(workspaceRoot, patchInput);
567
755
  break;
568
756
  }
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}`;
757
+ case 'write_file': {
758
+ const approved = await requestApproval({
759
+ type: 'write_file',
760
+ label: input.path,
761
+ preview: `${input.path}\n${truncate(input.content || '', 800)}`
762
+ });
763
+ if (!approved) {
764
+ toolResult = `User denied full file write for ${input.path}`;
765
+ break;
766
+ }
767
+ safetyManager.appendActionLog({
768
+ source: 'code_agent',
769
+ action: 'write_file',
770
+ path: input.path,
771
+ approved
772
+ });
773
+ toolResult = writeFile(workspaceRoot, input.path, input.content);
580
774
  break;
581
775
  }
582
- toolResult = writeFile(workspaceRoot, input.path, input.content);
583
- break;
584
- }
585
- default:
586
- throw new Error(`Unsupported action: ${action}`);
776
+ case 'ask_user': {
777
+ const answer = await askUser(input.question);
778
+ toolResult = `User answered: ${answer}`;
779
+ break;
780
+ }
781
+ case 'open_url':
782
+ case 'open_app':
783
+ case 'open_file':
784
+ case 'open_folder':
785
+ case 'create_folder':
786
+ case 'system_info':
787
+ case 'system_automation': {
788
+ // Delegate to existing automation logic
789
+ toolResult = await executeAction({
790
+ type: action,
791
+ target: input.target || input.path || input.query // Handle all possible input fields
792
+ });
793
+ break;
794
+ } default:
795
+ throw new Error(`Unsupported action: ${action}`);
796
+ } } catch (e) {
797
+ toolResult = `Error: ${e.message}`;
798
+ }
799
+
800
+ // Log the finished step with result
801
+ let resultSummary = '';
802
+ if (action === 'search_code') {
803
+ const matches = (toolResult.match(/\n/g) || []).length;
804
+ resultSummary = ` -> Found ${matches} matches`;
805
+ } else if (action === 'run_shell') {
806
+ resultSummary = ` -> Exit code 0`; // Simplified
807
+ }
808
+
809
+ onProgress({
810
+ step,
811
+ phase: 'finished',
812
+ action,
813
+ target: (input.path || input.command || input.query || '') + resultSummary
814
+ });
815
+
816
+ // Format tool result to be more readable and structured for the agent
817
+ let formattedToolResult = toolResult;
818
+ if (action === 'list_files' || action === 'find_path') {
819
+ formattedToolResult = `Result of ${action}:\n---\n${toolResult}\n---`;
587
820
  }
588
821
 
589
822
  observation = [
590
823
  `Previous thought: ${decision.thought || '(none)'}`,
591
824
  `Action: ${action}`,
592
825
  'Observation:',
593
- toolResult
594
- ].join('\n');
595
- }
826
+ formattedToolResult
827
+ ].join('\n'); }
596
828
 
597
- // Check for Agent Collaboration (Review)
598
- if (config.enableAgentCollaboration !== false) {
829
+ // Check for Agent Collaboration (Review) - Disabled by default to save tokens
830
+ if (config.enableAgentCollaboration === true && executedSteps > 8 && finalSummary) {
599
831
  const availableProviders = getAvailableProviders(config);
600
832
  // Exclude providers that often need special local setup or are slow/unreliable for tiny reviews
601
833
  const altProviders = availableProviders.filter(p => p !== provider && p !== 'ollama' && p !== 'huggingface' && p !== 'local_openai');
@@ -606,7 +838,7 @@ async function executeCodeTask(task, options = {}) {
606
838
  : (availableProviders.includes('gemini') ? 'gemini' : availableProviders[0]);
607
839
 
608
840
  if (reviewerProvider && finalSummary) {
609
- onProgress(`Invoking Reviewer Agent (${reviewerProvider})...`);
841
+ onProgress({ phase: 'reviewing', action: 'reviewer_start', message: `Invoking Reviewer Agent (${reviewerProvider})...` });
610
842
 
611
843
  const reviewerClient = new UnifiedAgentClient(reviewerProvider, config);
612
844
  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,16 +852,21 @@ async function executeCodeTask(task, options = {}) {
620
852
 
621
853
  finalSummary += `\n\n[Review by ${reviewerProvider}]\n${reviewInput.summary || reviewDecision.thought || 'Looks good.'}`;
622
854
  } catch (e) {
623
- onProgress(`Reviewer Agent failed: ${e.message}`);
855
+ onProgress({ phase: 'reviewing', action: 'reviewer_error', message: `Reviewer Agent failed: ${e.message}` });
624
856
  }
625
857
  }
626
858
  }
627
859
 
628
860
  if (finalSummary) {
861
+ const answeredProvider = client.lastSuccessfulProvider || client.provider || provider;
629
862
  return {
630
863
  summary: finalSummary,
631
864
  verification: finalVerification,
632
- steps: executedSteps
865
+ steps: executedSteps,
866
+ providerInfo: {
867
+ provider: answeredProvider,
868
+ model: getCodeProviderModel(answeredProvider, config)
869
+ }
633
870
  };
634
871
  }
635
872
 
@@ -639,10 +876,15 @@ async function executeCodeTask(task, options = {}) {
639
876
  lastVerification: 'Agent limit reached before explicit completion.'
640
877
  });
641
878
 
879
+ const answeredProvider = client.lastSuccessfulProvider || client.provider || provider;
642
880
  return {
643
881
  summary: 'Stopped after reaching the maximum number of agent steps.',
644
882
  verification: 'Agent limit reached before explicit completion.',
645
- steps: executedSteps || MAX_AGENT_STEPS
883
+ steps: executedSteps || MAX_AGENT_STEPS,
884
+ providerInfo: {
885
+ provider: answeredProvider,
886
+ model: getCodeProviderModel(answeredProvider, config)
887
+ }
646
888
  };
647
889
  }
648
890
 
@@ -651,6 +893,10 @@ module.exports = {
651
893
  _helpers: {
652
894
  extractJson,
653
895
  selectSupportedCodeProvider,
654
- findPaths
896
+ getSupportedCodeProviderOrder,
897
+ findPaths,
898
+ listFiles,
899
+ searchCode,
900
+ walkDirectory
655
901
  }
656
902
  };