@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.
- package/GUIDE_TH.md +113 -0
- package/README.md +214 -142
- package/assets/CLI_Screen.png +0 -0
- package/docs/assets/CLI_Screen.png +0 -0
- package/docs/guide.html +632 -0
- package/docs/index.html +5 -4
- package/main.js +66 -894
- package/mint-cli-logic.js +15 -8
- package/mint-cli.js +305 -195
- package/package.json +12 -4
- package/src/AI_Brain/Gemini_API.js +77 -20
- package/src/AI_Brain/agent_orchestrator.js +6 -6
- package/src/AI_Brain/autonomous_brain.js +10 -0
- package/src/AI_Brain/behavior_memory.js +26 -5
- package/src/AI_Brain/headless_agent.js +4 -0
- package/src/AI_Brain/knowledge_base.js +61 -8
- package/src/AI_Brain/memory_store.js +55 -7
- package/src/Automation_Layer/file_operations.js +14 -3
- package/src/CLI/chat_router.js +21 -7
- package/src/CLI/chat_ui.js +264 -710
- package/src/CLI/code_agent.js +370 -124
- package/src/CLI/gmail_auth.js +210 -0
- package/src/CLI/list_features.js +5 -1
- package/src/CLI/onboarding.js +307 -55
- package/src/CLI/updater.js +208 -0
- package/src/Channels/brave_search_bridge.js +35 -0
- package/src/Channels/discord_bridge.js +68 -0
- package/src/Channels/google_search_bridge.js +38 -0
- package/src/Channels/line_bridge.js +60 -0
- package/src/Channels/slack_bridge.js +53 -0
- package/src/Channels/telegram_bridge.js +49 -0
- package/src/Channels/whatsapp_bridge.js +55 -0
- package/src/Command_Parser/parser.js +12 -1
- package/src/Plugins/gmail.js +251 -0
- package/src/Plugins/google_calendar.js +245 -19
- package/src/Plugins/notion.js +256 -0
- package/src/System/action_executor.js +129 -0
- package/src/System/bridge_manager.js +76 -0
- package/src/System/chat_history_manager.js +23 -5
- package/src/System/config_manager.js +41 -7
- package/src/System/custom_workflows.js +31 -2
- package/src/System/google_tts_urls.js +51 -0
- package/src/System/ipc_handlers.js +238 -0
- package/src/System/proactive_loop.js +137 -0
- package/src/System/safety_manager.js +165 -0
- package/src/System/screen_capture.js +175 -0
- package/src/System/task_manager.js +15 -5
- package/src/System/window_manager.js +210 -0
- package/src/UI/renderer.js +33 -7
- package/src/UI/settings.html +24 -0
- package/src/UI/settings.js +14 -4
- package/src/UI/styles.css +14 -1
- package/tests/action_executor_safety.test.js +67 -0
- package/tests/gmail.test.js +135 -0
- package/tests/gmail_auth.test.js +129 -0
- package/tests/google_calendar.test.js +113 -0
- package/tests/google_tts_urls.test.js +24 -0
- package/tests/notion.test.js +121 -0
- package/tests/provider_routing.test.js +17 -1
- package/tests/safety_manager.test.js +40 -0
- package/tests/updater.test.js +32 -0
package/src/CLI/code_agent.js
CHANGED
|
@@ -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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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.
|
|
25
|
-
3.
|
|
26
|
-
4.
|
|
27
|
-
5.
|
|
28
|
-
6.
|
|
29
|
-
7.
|
|
30
|
-
8.
|
|
31
|
-
9. When you are done, return "finish" with
|
|
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
|
|
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
|
-
- "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
'
|
|
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
|
|
501
|
-
const
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
594
|
-
].join('\n');
|
|
595
|
-
}
|
|
826
|
+
formattedToolResult
|
|
827
|
+
].join('\n'); }
|
|
596
828
|
|
|
597
|
-
// Check for Agent Collaboration (Review)
|
|
598
|
-
if (config.enableAgentCollaboration
|
|
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
|
-
|
|
896
|
+
getSupportedCodeProviderOrder,
|
|
897
|
+
findPaths,
|
|
898
|
+
listFiles,
|
|
899
|
+
searchCode,
|
|
900
|
+
walkDirectory
|
|
655
901
|
}
|
|
656
902
|
};
|