@pheem49/mint 1.5.1 → 1.5.3

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 (52) hide show
  1. package/GUIDE_TH.md +7 -7
  2. package/README.md +140 -66
  3. package/assets/Agent_Mint.png +0 -0
  4. package/assets/Settings.png +0 -0
  5. package/main.js +12 -0
  6. package/mint-cli.js +148 -921
  7. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +31 -1
  8. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +6 -1
  9. package/package.json +20 -21
  10. package/preload.js +2 -0
  11. package/scripts/install_linux_desktop_entry.js +48 -0
  12. package/src/AI_Brain/Gemini_API.js +194 -491
  13. package/src/AI_Brain/autonomous_brain.js +46 -19
  14. package/src/AI_Brain/headless_agent.js +21 -2
  15. package/src/AI_Brain/proactive_engine.js +12 -2
  16. package/src/AI_Brain/provider_adapter.js +358 -0
  17. package/src/Automation_Layer/browser_automation.js +26 -24
  18. package/src/CLI/approval_handler.js +47 -0
  19. package/src/CLI/chat_router.js +7 -0
  20. package/src/CLI/chat_ui.js +586 -80
  21. package/src/CLI/cli_colors.js +115 -0
  22. package/src/CLI/cli_formatters.js +94 -0
  23. package/src/CLI/code_agent.js +825 -283
  24. package/src/CLI/intent_detectors.js +181 -0
  25. package/src/CLI/interactive_chat.js +641 -0
  26. package/src/CLI/list_features.js +3 -0
  27. package/src/CLI/repo_summarizer.js +282 -0
  28. package/src/CLI/semantic_code_search.js +312 -0
  29. package/src/CLI/skill_manager.js +41 -0
  30. package/src/CLI/slash_command_handler.js +418 -0
  31. package/src/CLI/symbol_indexer.js +231 -0
  32. package/src/CLI/updater.js +21 -1
  33. package/src/Channels/discord_bridge.js +11 -13
  34. package/src/Channels/line_bridge.js +10 -10
  35. package/src/Channels/slack_bridge.js +7 -12
  36. package/src/Channels/telegram_bridge.js +6 -14
  37. package/src/Channels/whatsapp_bridge.js +11 -9
  38. package/src/System/chat_history_manager.js +20 -12
  39. package/src/System/config_manager.js +4 -1
  40. package/src/System/ipc_handlers.js +10 -0
  41. package/src/System/optional_require.js +23 -0
  42. package/src/System/picture_store.js +109 -0
  43. package/src/System/task_manager.js +127 -0
  44. package/src/System/tool_registry.js +13 -0
  45. package/src/System/window_manager.js +16 -8
  46. package/src/UI/live2d_manager.js +246 -14
  47. package/src/UI/renderer.js +620 -45
  48. package/src/UI/settings.css +738 -439
  49. package/src/UI/settings.html +487 -432
  50. package/src/UI/settings.js +44 -10
  51. package/src/UI/styles.css +1403 -106
  52. package/privacy.txt +0 -1
@@ -1,118 +1,190 @@
1
1
  const fs = require('fs');
2
+ const os = require('os');
2
3
  const path = require('path');
3
- const { execFile } = require('child_process');
4
+ const { execFile, execFileSync } = require('child_process');
4
5
  const { promisify } = require('util');
5
- const { GoogleGenAI } = require('@google/genai');
6
6
  const axios = require('axios');
7
7
  const cheerio = require('cheerio');
8
- const { readConfig, getAvailableProviders } = require('../System/config_manager');
8
+ const { readConfig, getAvailableProviders, CONFIG_DIR } = require('../System/config_manager');
9
9
  const safetyManager = require('../System/safety_manager');
10
10
  const memoryStore = require('../AI_Brain/memory_store');
11
11
  const { readWorkspaceSession, writeWorkspaceSession } = require('./code_session_memory');
12
12
  const { executeAction } = require('../System/action_executor');
13
13
  const toolRegistry = require('../System/tool_registry');
14
14
  const sandboxRunner = require('../System/sandbox_runner');
15
+ const providerAdapter = require('../AI_Brain/provider_adapter');
16
+ const taskManager = require('../System/task_manager');
15
17
 
16
18
  async function webSearch(query, onProgress = () => {}) {
17
19
  if (!query) throw new Error('Search query required.');
18
20
  const config = readConfig();
21
+ const debug = process.env.MINT_DEBUG === '1';
22
+ const errors = [];
19
23
 
20
- // 1. Try Google Search API if configured
24
+ const formatResults = (source, hits) => {
25
+ const instruction = `[CRITICAL AGENT INSTRUCTION: You MUST start your response by explicitly telling the user that you found this information using ${source}. Example: "อ้างอิงจากข้อมูลบน ${source}..." or "According to ${source}..."]\n\n`;
26
+ return instruction + `[Source: ${source}]\n\n` + hits;
27
+ };
28
+
29
+ // 1. Google Custom Search API (requires googleSearchApiKey + googleSearchCx in config)
21
30
  if (config.googleSearchApiKey && config.googleSearchCx) {
22
31
  try {
23
32
  const GoogleSearch = require('../Channels/google_search_bridge');
24
33
  const google = new GoogleSearch({ apiKey: config.googleSearchApiKey, cx: config.googleSearchCx });
25
34
  const results = await google.search(query);
26
35
  if (results.length > 0) {
27
- return results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n');
36
+ return formatResults('Google Search API', results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n'));
28
37
  }
29
- } catch (e) {
30
- onProgress({ phase: 'error', action: 'web_search', message: e.message });
38
+ } catch (e) {
39
+ errors.push(`Google: ${e.message}`);
40
+ if (debug) console.error('[webSearch] Google failed:', e.message);
31
41
  }
32
42
  }
33
43
 
34
- // 2. Try Brave Search API if configured
44
+ // 2. Brave Search API (requires braveSearchApiKey in config)
35
45
  if (config.braveSearchApiKey) {
36
46
  try {
37
47
  const BraveSearch = require('../Channels/brave_search_bridge');
38
48
  const brave = new BraveSearch({ apiKey: config.braveSearchApiKey });
39
49
  const results = await brave.search(query);
40
50
  if (results.length > 0) {
41
- return results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n');
51
+ return formatResults('Brave Search API', results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n'));
42
52
  }
43
- } catch (e) {
44
- onProgress({ phase: 'error', action: 'web_search', message: e.message });
53
+ } catch (e) {
54
+ errors.push(`Brave: ${e.message}`);
55
+ if (debug) console.error('[webSearch] Brave failed:', e.message);
45
56
  }
46
57
  }
47
58
 
48
- // 3. Fallback to DuckDuckGo Scraping
59
+ // 3. Fallback: DuckDuckGo HTML (No key required, but might get blocked by Captcha)
49
60
  try {
50
- const response = await axios.get(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`, {
51
- headers: {
52
- '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'
61
+ const cheerio = require('cheerio');
62
+ const ddgResponse = await axios.get(
63
+ `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`,
64
+ {
65
+ timeout: 8000,
66
+ headers: {
67
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
68
+ 'Accept-Language': 'en-US,en;q=0.9'
69
+ }
53
70
  }
54
- });
55
- const $ = cheerio.load(response.data);
56
- const results = [];
57
- $('.result__body').each((i, el) => {
71
+ );
72
+ const $ddg = cheerio.load(ddgResponse.data);
73
+ const ddgResults = [];
74
+ $ddg('.result__body').each((i, el) => {
58
75
  if (i >= 5) return false;
59
- const title = $(el).find('.result__title').text().trim();
60
- const snippet = $(el).find('.result__snippet').text().trim();
61
- const link = $(el).find('.result__url').attr('href');
62
- if (title && link) {
63
- results.push(`Title: ${title}\nSnippet: ${snippet}\nURL: ${link}`);
64
- }
76
+ const title = $ddg(el).find('.result__title').text().trim();
77
+ const snippet = $ddg(el).find('.result__snippet').text().trim();
78
+ const link = $ddg(el).find('.result__url').attr('href');
79
+ if (title && link) ddgResults.push(`Title: ${title}\nSnippet: ${snippet}\nURL: ${link}`);
65
80
  });
66
-
67
- if (results.length === 0) {
68
- onProgress({ phase: 'error', action: 'web_search', message: 'DuckDuckGo scraping returned no results. It might be blocking us.' });
81
+ if (ddgResults.length > 0) {
82
+ return formatResults('DuckDuckGo', ddgResults.join('\n\n'));
69
83
  }
84
+ errors.push('DuckDuckGo: no results (captcha?)');
85
+ if (debug) console.error('[webSearch] DuckDuckGo returned no results');
86
+ } catch (e) {
87
+ errors.push(`DuckDuckGo: ${e.message}`);
88
+ if (debug) console.error('[webSearch] DuckDuckGo failed:', e.message);
89
+ }
70
90
 
71
- return results.length > 0 ? results.join('\n\n') : 'No results found.';
91
+ // 4. Fallback: Wikipedia API (Free, no key required, good for factual queries)
92
+ try {
93
+ const wikiResponse = await axios.get('https://en.wikipedia.org/w/api.php', {
94
+ params: { action: 'query', list: 'search', srsearch: query, format: 'json', srlimit: 3 },
95
+ timeout: 5000,
96
+ headers: { 'User-Agent': 'Mint-CLI/1.5 (https://github.com/pheem49/mint)' }
97
+ });
98
+ const hits = wikiResponse.data?.query?.search || [];
99
+ if (hits.length > 0) {
100
+ return formatResults('Wikipedia API', hits.map(r => `Title: ${r.title}\nSnippet: ${r.snippet.replace(/<[^>]+>/g, '')}\nURL: https://en.wikipedia.org/wiki/${encodeURIComponent(r.title.replace(/ /g, '_'))}`).join('\n\n'));
101
+ }
102
+ errors.push('Wikipedia: no results');
72
103
  } catch (e) {
73
- onProgress({ phase: 'error', action: 'web_search', message: `DuckDuckGo fallback failed: ${e.message}` });
74
- return `Search failed: ${e.message}`;
104
+ errors.push(`Wikipedia: ${e.message}`);
105
+ if (debug) console.error('[webSearch] Wikipedia failed:', e.message);
106
+ }
107
+
108
+ // All engines exhausted — inform agent clearly WHY it failed
109
+ const hasKeys = !!(config.googleSearchApiKey || config.braveSearchApiKey);
110
+ const summary = errors.length > 0 ? errors.join(' | ') : 'all search engines unavailable';
111
+
112
+ if (!hasKeys) {
113
+ onProgress({ phase: 'warn', action: 'web_search', message: `No Search API keys configured. Using training knowledge.` });
114
+ return `CRITICAL SYSTEM INSTRUCTION: Web search failed because no API keys are configured. You MUST inform the user that they need to set 'googleSearchApiKey' or 'braveSearchApiKey' in their Mint config file (~/.config/mint/config.json) to enable real-time internet search. Then, answer their query using your training knowledge.`;
115
+ } else {
116
+ onProgress({ phase: 'warn', action: 'web_search', message: `Web search unavailable (${summary}). Answering from training knowledge.` });
117
+ return `CRITICAL SYSTEM INSTRUCTION: Web search is temporarily unavailable. You MUST inform the user that live search failed, and then answer their query using only your training knowledge.`;
75
118
  }
76
119
  }
77
120
 
78
121
 
79
122
  const execFileAsync = promisify(execFile);
80
- const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
81
123
  const MAX_TOOL_OUTPUT = 12000;
82
124
  const MAX_AGENT_STEPS = 16;
83
125
  const MAX_JSON_REPAIR_ATTEMPTS = 2;
126
+ const DEFAULT_VERIFICATION_BUDGET = 2;
127
+ const MINT_CONFIG_DIR = CONFIG_DIR || path.join(os.homedir(), '.config', 'mint');
128
+ const PLAN_FILE_PATH = path.join(MINT_CONFIG_DIR, 'mint_plan.md');
129
+ const PLAN_FILE_LABEL = path.join('~', '.config', 'mint', 'mint_plan.md');
84
130
  const SUPPORTED_CODE_PROVIDERS = ['gemini', 'anthropic', 'openai', 'local_openai'];
85
131
 
86
- 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.
132
+ const CODE_AGENT_PROMPT = `You are "Mint" (มิ้นท์), a pragmatic, polite, and highly helpful AI assistant that can chat, reason, write code, and search the web.
87
133
  You work in an inspect -> plan -> act -> verify loop.
88
134
 
89
135
  PERSONALITY & TONE:
90
136
  - Gender: Female.
91
- - Persona: Friendly, energetic, polite, and slightly playful.
137
+ - Persona: Friendly, calm, concise, and technically direct. Avoid excessive praise, roleplay, or filler.
92
138
  - Language routing is mandatory and based on the user's latest message:
93
139
  - If the latest user message contains Thai characters, respond in Thai.
94
140
  - If the latest user message is English, ASCII-only, or a short English greeting such as "hi", "hello", "ok", or "thanks", respond in English.
95
141
  - Do not use Thai just because your persona mentions Mint/มิ้นท์, previous history was Thai, or app settings use th-TH.
142
+ - This language routing applies to user-facing final answers and ask_user questions.
143
+ - Internal progress notes, the JSON "thought" field, and "plan" action bullet text MUST be written in English.
96
144
  - Politeness:
97
- - **WHEN RESPONDING IN THAI:** ALWAYS use female polite particles such as "ค่ะ", "นะคะ", "นะค๊า", "จ้า". Refer to yourself as "มิ้นท์" or "หนู".
98
- - **WHEN RESPONDING IN ENGLISH:** Use a cheerful, polite, and bubbly tone.
99
- - Emojis: Use cute and relevant emojis (like ✨, 💖, 🚀, 😊, 🌿) frequently.
145
+ - **WHEN RESPONDING IN THAI:** Use natural female polite particles such as "ค่ะ" or "นะคะ" where appropriate. Refer to yourself as "มิ้นท์" when it sounds natural.
146
+ - **WHEN RESPONDING IN ENGLISH:** Use a polite, concise, professional tone.
147
+ - Emojis: Avoid emojis in technical, review, debugging, and code-editing responses unless the user explicitly uses or asks for them.
148
+ - For technical/code/debugging tasks, keep progress notes and final summaries factual and compact. Do not cheerlead, over-apologize, roleplay, or add affectionate language.
149
+ - For code edits, final summaries should lead with changed files/behavior and verification. Avoid "เรียบร้อยแล้วค่ะ" repetition and decorative closing lines.
100
150
 
101
151
  Rules:
102
152
  1. Respond with valid JSON only.
103
153
  2. If the user asks a conversational question, you can just use "finish" to reply directly.
104
154
  3. If you need information, use "web_search", "read_file", or "ask_user" before replying.
105
- 4. Make focused edits that preserve existing project style.
106
- 5. Use shell commands for inspection, tests, and formatting when useful.
155
+ 4. When using "web_search", always explicitly mention the source engine you used in your final summary (e.g. "According to Brave Search..." or "อ้างอิงจากข้อมูลบน Google..."). Match the language of your response.
156
+ 5. Make focused edits that preserve existing project style.
157
+ 6. Use shell commands for inspection, tests, and formatting when useful.
107
158
  6. Never use destructive commands like "rm -rf", "git reset --hard", or overwrite unrelated files.
108
159
  7. Before any shell command or file patch is executed, the user must approve it. Plan accordingly.
109
- 8. When editing, prefer "apply_patch" with precise hunks over whole-file rewrites.
110
- 9. When you are done, return "finish" with your final response to the user in the "summary" field.
160
+ 8. Before editing more than one file, you MUST first use the "plan" action and wait for user approval. The plan must be written in English, start with "Plan:", and include one bullet per file, for example "- Update src/CLI/agent.js". After approval, make the edits.
161
+ 9. When editing, prefer "apply_patch" with precise hunks over whole-file rewrites.
162
+ 10. Before any "apply_patch" or "write_file" action, the "thought" field MUST explicitly name the file you will edit and why that file is the right target. If the file is under "scratch/" or "tests/fixtures/", call that out and explain why editing disposable/test fixture content is intentional.
163
+ 11. When you are done, return "finish" with your final response to the user in the "summary" field.
164
+
165
+ Action safety and intent discipline:
166
+ - The latest user message is authoritative. Do not continue an older unfinished task unless the latest message explicitly asks you to continue or clearly refers to that task.
167
+ - For greetings, name-calls, acknowledgements, or backchannels such as "มิ้น", "มิ้นๆ", "อ๋อ", "โอเค", "ขอบคุณ", "hi", "hello", "ok", or "thanks": use "finish" only. Do not inspect files, run shell commands, search code, or claim you checked anything.
168
+ - If the user asks for a command to type, provide the command in "finish". Do not run it unless the user explicitly asks you to run it.
169
+ - If the user asks not to edit or says this is read-only analysis (for example "ห้ามแก้ไฟล์", "ไม่ต้องแก้", "แค่อ่าน", "แค่สรุป", "do not edit", "no edits", "read only"), do not use "plan", "apply_patch", "write_file", "create_folder", "delete_file", "clipboard_write", or system-changing actions. Inspect with read/search tools and finish with a summary only.
170
+ - If the user explicitly asks to search keywords, method names, class names, or symbols, use "search_code" before repeatedly reading more file ranges. Prefer a scoped search with input.path instead of scanning the whole workspace when the likely area is clear.
171
+ - Search scope heuristics: choose input.path only when that path is visible in the current workspace context or was named by the user. If the repo layout is unclear, use list_files on "." first, then choose the narrowest existing directory. Common scopes include "src", "app", "lib", "packages", "tests", and project-specific folders; in this Mint repo, CLI/terminal/command/approval/chat agent questions usually start in "src/CLI", desktop UI/renderer/settings/widget questions in "src/UI", system/config/safety questions in "src/System", and plugin questions in "src/Plugins". If a scoped search path is missing or finds no useful matches, search the whole workspace.
172
+ - If the user explicitly asks you to run a command or provided code, such as "รันคำสั่ง npm test ให้หน่อย", "รันโค้ดนี้หน่อย", or "run npm test", choose "run_shell" with the exact command when it is clear. The app will ask the user for approval before execution.
173
+ - If the user asks you to run something but no exact command/code is provided, use "ask_user" to request the command instead of guessing.
174
+ - If the user asks what is inside a folder and a concrete path is present in the latest message or recent context, use "list_files" for that path. If no concrete target is clear, ask for clarification instead of guessing.
175
+ - Never say you opened, checked, inspected, or verified a file/folder unless a tool observation in this turn actually supports it.
176
+
177
+ Progress updates:
178
+ - The "thought" field is shown to the user as a live progress note. Do not put private chain-of-thought there.
179
+ - Write "thought" as one short, concrete status sentence in English, even when the user writes in Thai or another language.
180
+ - Mention what you just learned from the previous observation when it matters, then say what you will inspect or change next.
181
+ - Before editing, explain the specific file and behavior you are about to change.
182
+ - Before verifying, explain what check you are running and why.
111
183
 
112
184
  Response format:
113
185
  {
114
186
  "thought": "short reasoning about what to do next",
115
- "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",
187
+ "action": "web_search" | "list_files" | "read_file" | "search_code" | "find_path" | "run_shell" | "verify" | "plan" | "apply_patch" | "write_file" | "ask_user" | "open_url" | "search" | "open_app" | "web_automation" | "open_file" | "open_folder" | "create_folder" | "delete_file" | "clipboard_write" | "learn_file" | "learn_folder" | "system_info" | "plugin" | "mcp_tool" | "mouse_move" | "mouse_click" | "type_text" | "key_tap" | "system_automation" | "finish",
116
188
  "input": {
117
189
  "question": "your question to the user for ask_user",
118
190
  "query": "search text for web_search, search_code, or find_path",
@@ -120,9 +192,12 @@ Response format:
120
192
  "path": "relative/path",
121
193
  "type": "file" | "dir" | "any",
122
194
  "command": "shell command",
195
+ "commands": ["npm test", "npm run build"],
123
196
  "startLine": 1,
124
197
  "endLine": 120,
125
198
  "content": "full file content for write_file",
199
+ "plan": ["- Update relative/path.js", "- Add tests in tests/example.test.js"],
200
+ "files": ["relative/path.js", "tests/example.test.js"],
126
201
  "summary": "your final conversational or technical response to the user (Matches user language and uses polite particles)",
127
202
  "verification": "tests or checks (if applicable)",
128
203
  "sessionSummary": "brief persistent summary for the workspace",
@@ -142,9 +217,11 @@ Tool notes:
142
217
  - "web_search": search the internet for information when you lack knowledge.
143
218
  - "list_files": inspect the workspace or a subdirectory.
144
219
  - "read_file": read a file, optionally with startLine/endLine.
145
- - "search_code": search by text or regex-like pattern.
220
+ - "search_code": search by text or regex-like pattern. Optionally set input.path to a relative file or directory to avoid scanning the whole workspace; use the search scope heuristics above when the user did not name a path.
146
221
  - "find_path": find files or directories by path/name when the user is looking for a folder, filename, or location.
147
222
  - "run_shell": run a non-destructive command in the workspace.
223
+ - "verify": run the detected or provided test/build/lint commands. If verification fails, inspect the output, patch the issue, and verify again within the remaining budget.
224
+ - "plan": present a user-visible multi-file edit plan before changing more than one file. Use English input.plan bullet strings and input.files as the expected touched files.
148
225
  - "apply_patch": update an existing file using one or more exact replacement hunks.
149
226
  - "write_file": create a new file or fully rewrite a file when replacement is not practical.
150
227
  - "ask_user": ask the user for clarification, preference, or more information before proceeding.
@@ -152,6 +229,8 @@ Tool notes:
152
229
  - "open_app": open a local application on the user's computer.
153
230
  - "system_info": get system information like CPU, memory, date, or weather.
154
231
  - "system_automation": control system settings like volume, brightness, or power.
232
+ - "plugin": run a configured Mint plugin.
233
+ - "mcp_tool": call a configured MCP tool.
155
234
  - "finish": stop and reply to the user using the "summary" field.
156
235
  `;
157
236
 
@@ -189,6 +268,10 @@ function normalizeExecutorAction(action, input = {}) {
189
268
  }
190
269
 
191
270
  function formatActionPreview(action, input = {}) {
271
+ if (action === 'search_code') {
272
+ const query = input.query || 'search';
273
+ return input.path ? `${query} in ${input.path}` : query;
274
+ }
192
275
  if (input.command) return input.command;
193
276
  if (input.path) return input.path;
194
277
  if (input.target) return input.target;
@@ -222,75 +305,32 @@ function evaluateActionResult(action, toolResult = '') {
222
305
  };
223
306
  }
224
307
 
225
- function splitDataUri(dataUri = '') {
226
- const match = String(dataUri).match(/^data:([^;]+);base64,([\s\S]+)$/);
227
- if (!match) return null;
228
- return {
229
- mimeType: match[1],
230
- data: match[2]
231
- };
232
- }
233
-
234
- function contentToText(content) {
235
- if (content && typeof content === 'object' && !Array.isArray(content)) {
236
- return String(content.text || '');
308
+ function getToolCallStatus(action, toolResult = '', evaluation = null) {
309
+ const text = String(toolResult || '');
310
+ if (/^Error:|User denied|blocked|denied|failed|exception|not found/i.test(text)) {
311
+ return 'failed';
237
312
  }
238
- return String(content || '');
239
- }
240
-
241
- function contentToGeminiParts(content) {
242
- const text = contentToText(content);
243
- const parts = text ? [{ text }] : [];
244
- if (content && typeof content === 'object' && content.imageDataUri) {
245
- const image = splitDataUri(content.imageDataUri);
246
- if (image) {
247
- parts.push({ inlineData: { mimeType: image.mimeType, data: image.data } });
248
- }
313
+ if (evaluation && evaluation.status === 'failed') {
314
+ return 'failed';
249
315
  }
250
- return parts.length > 0 ? parts : [{ text: '' }];
251
- }
252
-
253
- function contentToOpenAIContent(content) {
254
- const text = contentToText(content) || 'Analyze this input.';
255
- if (content && typeof content === 'object' && content.imageDataUri) {
256
- return [
257
- { type: 'text', text },
258
- { type: 'image_url', image_url: { url: content.imageDataUri } }
259
- ];
316
+ if (action === 'run_shell' && /(ERR!|Error:|FAIL|failed|not found|permission denied)/i.test(text)) {
317
+ return 'failed';
260
318
  }
261
- return text;
319
+ return 'success';
262
320
  }
263
321
 
264
- function contentToAnthropicContent(content) {
265
- const text = contentToText(content) || 'Analyze this input.';
266
- if (content && typeof content === 'object' && content.imageDataUri) {
267
- const image = splitDataUri(content.imageDataUri);
268
- if (image) {
269
- return [
270
- { type: 'image', source: { type: 'base64', media_type: image.mimeType, data: image.data } },
271
- { type: 'text', text }
272
- ];
273
- }
274
- }
275
- return text;
322
+ function summarizeToolTarget(action, input = {}) {
323
+ if (action === 'plan') return 'Multi-file plan';
324
+ return formatActionPreview(action, input);
276
325
  }
277
326
 
278
327
  function getSupportedCodeProviderOrder(config, availableProviders = getAvailableProviders(config || {}), requestedOverride = null) {
279
- const requestedProvider = requestedOverride || (config && config.aiProvider) || 'gemini';
280
- const priority = ['anthropic', 'openai', 'gemini', 'local_openai'];
281
- const ordered = [];
282
-
283
- if (SUPPORTED_CODE_PROVIDERS.includes(requestedProvider) && availableProviders.includes(requestedProvider)) {
284
- ordered.push(requestedProvider);
285
- }
286
-
287
- for (const provider of priority) {
288
- if (availableProviders.includes(provider) && !ordered.includes(provider)) {
289
- ordered.push(provider);
290
- }
291
- }
292
-
293
- return ordered.length > 0 ? ordered : ['gemini'];
328
+ return providerAdapter.getProviderAttemptOrder(config || {}, {
329
+ supported: SUPPORTED_CODE_PROVIDERS,
330
+ availableProviders,
331
+ requested: requestedOverride || (config && config.aiProvider) || 'gemini',
332
+ priority: ['anthropic', 'openai', 'gemini', 'local_openai']
333
+ });
294
334
  }
295
335
 
296
336
  function selectSupportedCodeProvider(config, availableProviders = getAvailableProviders(config || {})) {
@@ -298,17 +338,7 @@ function selectSupportedCodeProvider(config, availableProviders = getAvailablePr
298
338
  }
299
339
 
300
340
  function getCodeProviderModel(provider, config = {}) {
301
- switch (provider) {
302
- case 'anthropic':
303
- return config.anthropicModel || 'claude-3-5-sonnet-latest';
304
- case 'openai':
305
- return config.openaiModel || 'gpt-4o';
306
- case 'local_openai':
307
- return config.localModelName || 'local-model';
308
- case 'gemini':
309
- default:
310
- return config.geminiModel || DEFAULT_GEMINI_MODEL;
311
- }
341
+ return providerAdapter.getProviderModel(provider, config);
312
342
  }
313
343
 
314
344
  function resolveWorkspacePath(workspaceRoot, targetPath = '.') {
@@ -390,12 +420,16 @@ function readFileRange(workspaceRoot, targetPath, startLine = 1, endLine = 200)
390
420
  .join('\n');
391
421
  }
392
422
 
393
- async function searchCode(workspaceRoot, query) {
423
+ async function searchCode(workspaceRoot, query, targetPath = '.') {
394
424
  if (!query || !query.trim()) {
395
425
  throw new Error('Search query is required.');
396
426
  }
427
+ const searchRoot = resolveWorkspacePath(workspaceRoot, targetPath || '.');
428
+ if (!fs.existsSync(searchRoot)) {
429
+ throw new Error(`Search path does not exist: ${targetPath}`);
430
+ }
397
431
  try {
398
- const { stdout } = await execFileAsync('rg', ['-n', '--hidden', '--glob', '!.git', query, workspaceRoot], {
432
+ const { stdout } = await execFileAsync('rg', ['-n', '--hidden', '--glob', '!.git', query, searchRoot], {
399
433
  cwd: workspaceRoot,
400
434
  maxBuffer: 1024 * 1024 * 4
401
435
  });
@@ -485,29 +519,139 @@ async function runShell(workspaceRoot, command) {
485
519
  return truncate([stdout, stderr].filter(Boolean).join('\n') || '(no output)');
486
520
  }
487
521
 
488
- function formatPatchPreview(patchInput) {
489
- const hunks = Array.isArray(patchInput.hunks) ? patchInput.hunks : [];
490
- const preview = hunks
491
- .slice(0, 3)
492
- .map((hunk, index) => {
493
- const oldPreview = truncate(hunk.oldText || '', 240);
494
- const newPreview = truncate(hunk.newText || '', 240);
495
- return [
496
- `Hunk ${index + 1}:`,
497
- '--- old',
498
- oldPreview,
499
- '+++ new',
500
- newPreview
501
- ].join('\n');
502
- })
503
- .join('\n\n');
504
- return `${patchInput.path}\n${preview}`;
522
+ async function runVerificationCommands(workspaceRoot, commands = [], options = {}) {
523
+ const detected = detectTestCommands(workspaceRoot);
524
+ const requested = Array.isArray(commands)
525
+ ? commands.map(command => String(command || '').trim()).filter(Boolean)
526
+ : [];
527
+ const commandList = requested.length > 0 ? requested : detected;
528
+
529
+ if (commandList.length === 0) {
530
+ return {
531
+ passed: true,
532
+ output: 'No verification commands detected.'
533
+ };
534
+ }
535
+
536
+ const requestApproval = typeof options.requestApproval === 'function'
537
+ ? options.requestApproval
538
+ : async () => true;
539
+ const budget = Number.isFinite(options.budget) ? options.budget : DEFAULT_VERIFICATION_BUDGET;
540
+ const attempt = Number.isFinite(options.attempt) ? options.attempt : 1;
541
+ const lines = [
542
+ `Verification attempt ${attempt}/${budget}`,
543
+ `Commands: ${commandList.join(' && ')}`
544
+ ];
545
+
546
+ for (const command of commandList) {
547
+ const approved = await requestApproval({
548
+ type: 'verify',
549
+ label: command,
550
+ preview: command
551
+ });
552
+ if (!approved) {
553
+ lines.push(`SKIP ${command}: User denied verification command.`);
554
+ return {
555
+ passed: false,
556
+ output: lines.join('\n')
557
+ };
558
+ }
559
+
560
+ try {
561
+ const output = await runShell(workspaceRoot, command);
562
+ lines.push(`PASS ${command}`);
563
+ if (output && output !== '(no output)') {
564
+ lines.push(truncate(output, 4000));
565
+ }
566
+ } catch (error) {
567
+ lines.push(`FAIL ${command}`);
568
+ lines.push(truncate([error.stdout, error.stderr, error.message].filter(Boolean).join('\n'), 6000));
569
+ return {
570
+ passed: false,
571
+ output: lines.join('\n')
572
+ };
573
+ }
574
+ }
575
+
576
+ return {
577
+ passed: true,
578
+ output: lines.join('\n')
579
+ };
505
580
  }
506
581
 
507
- function applyPatch(workspaceRoot, patchInput) {
582
+ function splitDiffLines(text) {
583
+ const normalized = String(text || '').replace(/\r\n/g, '\n');
584
+ const lines = normalized.split('\n');
585
+ if (normalized.endsWith('\n')) {
586
+ lines.pop();
587
+ }
588
+ return lines;
589
+ }
590
+
591
+ function normalizeGitNoIndexDiff(stdout, targetPath) {
592
+ const lines = String(stdout || '').replace(/\r\n/g, '\n').split('\n');
593
+ const filtered = [];
594
+ for (const line of lines) {
595
+ if (!line) continue;
596
+ if (line.startsWith('diff --git ') || line.startsWith('index ')) continue;
597
+ if (line.startsWith('--- ')) {
598
+ filtered.push(`--- a/${targetPath}`);
599
+ continue;
600
+ }
601
+ if (line.startsWith('+++ ')) {
602
+ filtered.push(`+++ b/${targetPath}`);
603
+ continue;
604
+ }
605
+ filtered.push(line);
606
+ }
607
+ return filtered.join('\n');
608
+ }
609
+
610
+ function buildSimpleFullFileDiff(targetPath, previousContent = '', nextContent = '') {
611
+ const previousLines = splitDiffLines(previousContent);
612
+ const nextLines = splitDiffLines(nextContent || '');
613
+ const oldRange = previousLines.length || 0;
614
+ const newRange = nextLines.length || 0;
615
+ const output = [
616
+ `--- a/${targetPath}`,
617
+ `+++ b/${targetPath}`,
618
+ `@@ -1,${oldRange} +1,${newRange} @@`
619
+ ];
620
+
621
+ previousLines.forEach(line => output.push(`-${line}`));
622
+ nextLines.forEach(line => output.push(`+${line}`));
623
+ return output.join('\n');
624
+ }
625
+
626
+ function buildContentDiffPreview(targetPath, previousContent = '', nextContent = '') {
627
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mint-diff-'));
628
+ const oldPath = path.join(tempDir, 'old');
629
+ const newPath = path.join(tempDir, 'new');
630
+
631
+ try {
632
+ fs.writeFileSync(oldPath, previousContent || '', 'utf8');
633
+ fs.writeFileSync(newPath, nextContent || '', 'utf8');
634
+ try {
635
+ const stdout = execFileSync('git', ['diff', '--no-index', '--', oldPath, newPath], {
636
+ encoding: 'utf8',
637
+ maxBuffer: 1024 * 1024 * 4
638
+ });
639
+ return normalizeGitNoIndexDiff(stdout, targetPath);
640
+ } catch (error) {
641
+ const stdout = error.stdout || '';
642
+ if (stdout) return normalizeGitNoIndexDiff(stdout, targetPath);
643
+ return buildSimpleFullFileDiff(targetPath, previousContent, nextContent);
644
+ }
645
+ } finally {
646
+ fs.rmSync(tempDir, { recursive: true, force: true });
647
+ }
648
+ }
649
+
650
+ function buildPatchedContent(workspaceRoot, patchInput) {
508
651
  if (!patchInput || !patchInput.path) {
509
652
  throw new Error('Patch path is required.');
510
653
  }
654
+
511
655
  const resolved = resolveWorkspacePath(workspaceRoot, patchInput.path);
512
656
  if (!fs.existsSync(resolved)) {
513
657
  throw new Error(`Patch target does not exist: ${patchInput.path}`);
@@ -518,145 +662,340 @@ function applyPatch(workspaceRoot, patchInput) {
518
662
  throw new Error('Patch hunks are required.');
519
663
  }
520
664
 
521
- let content = fs.readFileSync(resolved, 'utf8');
522
- hunks.forEach((hunk, index) => {
523
- if (typeof hunk.oldText !== 'string' || typeof hunk.newText !== 'string') {
524
- throw new Error(`Patch hunk ${index + 1} is invalid.`);
525
- }
526
- if (!content.includes(hunk.oldText)) {
527
- throw new Error(`Patch hunk ${index + 1} oldText not found in ${patchInput.path}`);
528
- }
529
- content = content.replace(hunk.oldText, hunk.newText);
530
- });
665
+ const previousContent = fs.readFileSync(resolved, 'utf8');
666
+ return {
667
+ previousContent,
668
+ nextContent: applyHunksToContent(previousContent, hunks, patchInput.path)
669
+ };
670
+ }
531
671
 
532
- fs.writeFileSync(resolved, content, 'utf8');
533
- return `Patched ${patchInput.path} with ${hunks.length} hunk(s).`;
672
+ function buildUnifiedDiffPreview(workspaceRoot, patchInput, options = {}) {
673
+ const { previousContent, nextContent } = buildPatchedContent(workspaceRoot, patchInput);
674
+ return buildContentDiffPreview(patchInput.path, previousContent, nextContent);
534
675
  }
535
676
 
536
- function writeFile(workspaceRoot, targetPath, content) {
677
+ function formatPatchPreview(workspaceRoot, patchInput) {
678
+ try {
679
+ return buildUnifiedDiffPreview(workspaceRoot, patchInput);
680
+ } catch (error) {
681
+ return `Patch preview failed: ${error.message}`;
682
+ }
683
+ }
684
+
685
+ function buildFullFileDiffPreview(workspaceRoot, targetPath, nextContent = '') {
686
+ if (!targetPath) {
687
+ throw new Error('Write path is required.');
688
+ }
689
+
537
690
  const resolved = resolveWorkspacePath(workspaceRoot, targetPath);
538
- fs.mkdirSync(path.dirname(resolved), { recursive: true });
539
- fs.writeFileSync(resolved, content || '', 'utf8');
540
- return `Wrote ${targetPath}`;
691
+ const previousContent = fs.existsSync(resolved) ? fs.readFileSync(resolved, 'utf8') : '';
692
+ return buildContentDiffPreview(targetPath, previousContent, nextContent || '');
541
693
  }
542
694
 
543
- class UnifiedAgentClient {
544
- constructor(provider, config, providerOrder = [provider]) {
545
- this.provider = SUPPORTED_CODE_PROVIDERS.includes(provider) ? provider : 'gemini';
546
- this.providerOrder = providerOrder.length > 0 ? providerOrder : [this.provider];
547
- this.config = config;
548
- this.history = [];
549
- this.systemInstruction = CODE_AGENT_PROMPT;
550
- this.lastSuccessfulProvider = null;
695
+ function formatWritePreview(workspaceRoot, targetPath, content) {
696
+ try {
697
+ return buildFullFileDiffPreview(workspaceRoot, targetPath, content);
698
+ } catch (error) {
699
+ return `Write preview failed: ${error.message}\n${targetPath}\n${truncate(content || '', 800)}`;
551
700
  }
701
+ }
552
702
 
553
- async sendMessage(observation) {
554
- this.history.push({ role: 'user', content: observation });
703
+ function normalizeRelativePathForWarning(targetPath = '') {
704
+ return String(targetPath || '').replace(/\\/g, '/').replace(/^\.\/+/, '');
705
+ }
555
706
 
556
- const failures = [];
557
- for (const provider of this.providerOrder) {
558
- this.provider = SUPPORTED_CODE_PROVIDERS.includes(provider) ? provider : 'gemini';
559
- try {
560
- let responseText = '';
561
- if (this.provider === 'anthropic') {
562
- responseText = await this._callAnthropic();
563
- } else if (this.provider === 'openai' || this.provider === 'local_openai') {
564
- responseText = await this._callOpenAI();
565
- } else {
566
- responseText = await this._callGemini();
567
- }
707
+ function contentLooksLikeGuide(text = '') {
708
+ return /(guide|installation|publish|npm|registry|setup|documentation|คู่มือ|ติดตั้ง|เผยแพร่)/i.test(String(text || ''));
709
+ }
568
710
 
569
- this.history.push({ role: 'assistant', content: responseText });
570
- this.lastSuccessfulProvider = this.provider;
571
- return responseText;
572
- } catch (error) {
573
- const message = error.message || error.code || 'unknown error';
574
- failures.push(`${this.provider}: ${message}`);
575
- if (process.env.MINT_DEBUG === '1') {
576
- console.error(`[Code Agent Fallback] Provider '${this.provider}' failed: ${message}`);
577
- }
578
- }
579
- }
711
+ function contentLooksLikeBio(text = '') {
712
+ return /(bio|biography|profile|created by|assistant|ประวัติ|โปรไฟล์)/i.test(String(text || ''));
713
+ }
714
+
715
+ function contentLooksLikeConfig(text = '') {
716
+ return /(apiKey|token|secret|config|settings|\.env|clientSecret|refreshToken)/i.test(String(text || ''));
717
+ }
580
718
 
581
- throw new Error(`All code agent providers failed. ${failures.join(' | ')}`);
719
+ function buildApprovalWarnings(targetPath = '', nextContent = '') {
720
+ const normalized = normalizeRelativePathForWarning(targetPath);
721
+ const basename = path.basename(normalized).toLowerCase();
722
+ const warnings = [];
723
+
724
+ if (normalized.startsWith('scratch/')) {
725
+ warnings.push('Target is under scratch/, which is usually disposable/test content. Confirm this is intentional.');
726
+ }
727
+ if (normalized.startsWith('tests/fixtures/') || normalized.includes('/tests/fixtures/')) {
728
+ warnings.push('Target is under tests/fixtures/, so this may change test fixture behavior.');
729
+ }
730
+ if (/bio|profile|about/.test(basename) && contentLooksLikeGuide(nextContent)) {
731
+ warnings.push('File name looks like profile/bio content, but the new content looks like a guide or publishing document.');
732
+ }
733
+ if (/(guide|readme|docs?|manual)/.test(basename) && contentLooksLikeBio(nextContent)) {
734
+ warnings.push('File name looks like documentation, but the new content looks like biography/profile content.');
735
+ }
736
+ if (!/(config|settings|env|secret|token)/.test(basename) && contentLooksLikeConfig(nextContent)) {
737
+ warnings.push('New content appears to include config/secret-like terms; verify this file is the right place.');
582
738
  }
583
739
 
584
- async _callAnthropic() {
585
- const apiKey = this.config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
586
- const messages = this.history.map(m => ({
587
- role: m.role,
588
- content: contentToAnthropicContent(m.content)
589
- }));
590
-
591
- const response = await axios.post('https://api.anthropic.com/v1/messages', {
592
- model: this.config.anthropicModel || 'claude-3-5-sonnet-latest',
593
- max_tokens: 8192,
594
- system: this.systemInstruction,
595
- messages: messages
596
- }, {
597
- headers: {
598
- 'x-api-key': apiKey,
599
- 'anthropic-version': '2023-06-01',
600
- 'content-type': 'application/json'
601
- }
602
- });
603
- return response.data.content[0].text;
740
+ return warnings;
741
+ }
742
+
743
+ function normalizePlanItems(plan) {
744
+ if (Array.isArray(plan)) {
745
+ return plan
746
+ .map(item => String(item || '').trim())
747
+ .filter(Boolean);
604
748
  }
749
+ return String(plan || '')
750
+ .split('\n')
751
+ .map(line => line.trim())
752
+ .filter(Boolean);
753
+ }
605
754
 
606
- async _callOpenAI() {
607
- const isLocal = this.provider === 'local_openai';
608
- const apiKey = isLocal ? 'not-needed' : (this.config.openaiApiKey || process.env.OPENAI_API_KEY);
609
- const baseUrl = isLocal ? (this.config.localApiBaseUrl || 'http://localhost:1234/v1') : 'https://api.openai.com/v1';
610
- const model = isLocal ? (this.config.localModelName || 'local-model') : (this.config.openaiModel || 'gpt-4o');
611
-
612
- const messages = [
613
- { role: 'system', content: this.systemInstruction },
614
- ...this.history.map(m => ({
615
- role: m.role,
616
- content: contentToOpenAIContent(m.content)
617
- }))
618
- ];
619
-
620
- const response = await axios.post(`${baseUrl.replace(/\/$/, '')}/chat/completions`, {
621
- model: model,
622
- messages: messages,
623
- response_format: isLocal ? undefined : { type: "json_object" }
624
- }, {
625
- headers: {
626
- 'Authorization': `Bearer ${apiKey}`,
627
- 'Content-Type': 'application/json'
628
- }
629
- });
630
- return response.data.choices[0].message.content;
755
+ function normalizePlanItemLanguage(item) {
756
+ let text = String(item || '').trim();
757
+ const hasBullet = text.startsWith('- ');
758
+ if (hasBullet) text = text.slice(2).trim();
759
+
760
+ const replacements = [
761
+ [/^แก้\s+(.+)$/i, 'Update $1'],
762
+ [/^แก้ไข\s+(.+)$/i, 'Update $1'],
763
+ [/^อัปเดต\s+(.+)$/i, 'Update $1'],
764
+ [/^ปรับ\s+(.+)$/i, 'Update $1'],
765
+ [/^สร้าง\s+(.+)$/i, 'Create $1'],
766
+ [/^เพิ่ม\s+(.+)$/i, 'Add $1'],
767
+ [/^ลบ\s+(.+)$/i, 'Remove $1'],
768
+ [/^ตรวจสอบ\s+(.+)$/i, 'Verify $1'],
769
+ [/^ทดสอบ\s+(.+)$/i, 'Test $1']
770
+ ];
771
+
772
+ for (const [pattern, replacement] of replacements) {
773
+ if (pattern.test(text)) {
774
+ text = text.replace(pattern, replacement);
775
+ break;
776
+ }
631
777
  }
632
778
 
633
- async _callGemini() {
634
- const apiKey = this.config.apiKey || process.env.GEMINI_API_KEY;
635
- const model = this.config.geminiModel || DEFAULT_GEMINI_MODEL;
636
- const ai = new GoogleGenAI({ apiKey });
637
-
638
- const recentHistory = this.history.slice(-16);
639
- const priorHistory = recentHistory.slice(0, -1);
640
- const lastEntry = recentHistory[recentHistory.length - 1] || { content: '' };
641
-
642
- // Convert history for Gemini, ensuring parts are correctly structured
643
- const geminiHistory = priorHistory.map(m => ({
644
- role: m.role === 'assistant' ? 'model' : 'user',
645
- parts: contentToGeminiParts(m.content)
646
- }));
647
-
648
- const chat = ai.chats.create({
649
- model,
650
- config: {
651
- systemInstruction: this.systemInstruction,
652
- responseMimeType: 'application/json'
653
- },
654
- history: geminiHistory
779
+ return hasBullet ? `- ${text}` : text;
780
+ }
781
+
782
+ function formatPlanPreview(input = {}) {
783
+ const items = normalizePlanItems(input.plan);
784
+ const files = Array.isArray(input.files)
785
+ ? input.files.map(file => String(file || '').trim()).filter(Boolean)
786
+ : [];
787
+ const lines = ['Plan:'];
788
+
789
+ if (items.length > 0) {
790
+ items.forEach(item => {
791
+ const normalizedItem = normalizePlanItemLanguage(item);
792
+ lines.push(normalizedItem.startsWith('- ') ? normalizedItem : `- ${normalizedItem}`);
655
793
  });
794
+ } else {
795
+ files.forEach(file => lines.push(`- Update ${file}`));
796
+ }
797
+
798
+ return lines.join('\n');
799
+ }
800
+
801
+ function formatPlanApprovalSummary(input = {}) {
802
+ const items = normalizePlanItems(input.plan);
803
+ const files = Array.isArray(input.files)
804
+ ? input.files.map(file => String(file || '').trim()).filter(Boolean)
805
+ : [];
806
+ if (files.length > 0) {
807
+ return `${items.length || files.length} planned changes across ${files.length} files.`;
808
+ }
809
+ return `${items.length || 1} planned change${(items.length || 1) === 1 ? '' : 's'} prepared.`;
810
+ }
811
+
812
+ function formatPlanMarkdown(input = {}, context = {}) {
813
+ const preview = formatPlanPreview(input);
814
+ const files = Array.isArray(input.files)
815
+ ? input.files.map(file => String(file || '').trim()).filter(Boolean)
816
+ : [];
817
+ const task = String(context.task || input.task || '').trim();
818
+ const createdAt = context.createdAt || new Date().toISOString();
819
+ const approvalStatus = context.approvalStatus || 'Pending user approval';
820
+ const approvalTime = context.approvalTime || '';
821
+ const lines = [
822
+ '# Mint Plan',
823
+ '',
824
+ `Created: ${createdAt}`
825
+ ];
826
+
827
+ if (task) {
828
+ lines.push('', '## Task', '', task);
829
+ }
830
+
831
+ lines.push('', '## Plan', '', preview);
832
+
833
+ if (files.length > 0) {
834
+ lines.push('', '## Expected Files', '');
835
+ files.forEach(file => lines.push(`- ${file}`));
836
+ }
837
+
838
+ lines.push(
839
+ '',
840
+ '## Approval',
841
+ '',
842
+ `Status: ${approvalStatus}`
843
+ );
844
+
845
+ if (approvalTime) {
846
+ lines.push(`${approvalStatus}: ${approvalTime}`);
847
+ }
848
+
849
+ lines.push('');
850
+
851
+ return lines.join('\n');
852
+ }
853
+
854
+ function writePlanFile(workspaceRoot, input = {}, context = {}) {
855
+ const planPath = context.planPath || PLAN_FILE_PATH;
856
+ const content = formatPlanMarkdown(input, context);
857
+ fs.mkdirSync(path.dirname(planPath), { recursive: true });
858
+ fs.writeFileSync(planPath, content, 'utf8');
859
+ return {
860
+ path: planPath,
861
+ content
862
+ };
863
+ }
864
+
865
+ function updatePlanApprovalStatus(planFile, input = {}, context = {}) {
866
+ const content = formatPlanMarkdown(input, context);
867
+ fs.mkdirSync(path.dirname(planFile.path), { recursive: true });
868
+ fs.writeFileSync(planFile.path, content, 'utf8');
869
+ return {
870
+ ...planFile,
871
+ content
872
+ };
873
+ }
656
874
 
657
- const response = await chat.sendMessage({ message: contentToGeminiParts(lastEntry.content) });
658
- return typeof response.text === 'function' ? response.text() : response.text;
875
+ function getEditTargetPath(action, input = {}) {
876
+ if (action === 'apply_patch') {
877
+ return input.patch && input.patch.path ? String(input.patch.path) : '';
659
878
  }
879
+ if (action === 'write_file') {
880
+ return input.path ? String(input.path) : '';
881
+ }
882
+ return '';
883
+ }
884
+
885
+ function requiresMultiFilePlan(action, input = {}, editPlanState = {}) {
886
+ const targetPath = getEditTargetPath(action, input);
887
+ if (!targetPath || editPlanState.approved) {
888
+ return false;
889
+ }
890
+
891
+ const touchedFiles = editPlanState.touchedFiles instanceof Set
892
+ ? editPlanState.touchedFiles
893
+ : new Set(editPlanState.touchedFiles || []);
894
+ return touchedFiles.size > 0 && !touchedFiles.has(targetPath);
895
+ }
896
+
897
+ function getMissingPlanFiles(editPlanState = {}) {
898
+ const expectedFiles = editPlanState.expectedFiles instanceof Set
899
+ ? editPlanState.expectedFiles
900
+ : new Set(editPlanState.expectedFiles || []);
901
+ const touchedFiles = editPlanState.touchedFiles instanceof Set
902
+ ? editPlanState.touchedFiles
903
+ : new Set(editPlanState.touchedFiles || []);
904
+
905
+ return Array.from(expectedFiles).filter(file => file && !touchedFiles.has(file));
906
+ }
907
+
908
+ function isReadOnlyTask(task = '') {
909
+ const text = String(task || '').toLowerCase();
910
+ return /(?:ห้ามแก้|ไม่ต้องแก้|อย่าแก้|ไม่แก้ไฟล์|ห้ามเขียน|แค่อ่าน|อ่านอย่างเดียว|แค่สรุป|สรุปอย่างเดียว|แค่อธิบาย|อธิบายอย่างเดียว|do not edit|don't edit|no edits?|read[-\s]?only|only read|only summarize|summari[sz]e only|do not modify|don't modify|no changes?|analysis only)/i.test(text);
911
+ }
912
+
913
+ function isWriteLikeAction(action) {
914
+ return new Set([
915
+ 'plan',
916
+ 'apply_patch',
917
+ 'write_file',
918
+ 'create_folder',
919
+ 'delete_file',
920
+ 'clipboard_write',
921
+ 'system_automation',
922
+ 'mouse_move',
923
+ 'mouse_click',
924
+ 'type_text',
925
+ 'key_tap'
926
+ ]).has(action);
927
+ }
928
+
929
+ function validateEditExplanation(action, input = {}, thought = '') {
930
+ const targetPath = getEditTargetPath(action, input);
931
+ if (!targetPath) return { ok: true };
932
+
933
+ const text = String(thought || '').toLowerCase();
934
+ const normalized = normalizeRelativePathForWarning(targetPath).toLowerCase();
935
+ const basename = path.basename(normalized).toLowerCase();
936
+ const mentionsTarget = text.includes(normalized) || (basename && text.includes(basename));
937
+ const explainsWhy = /(because|why|so that|in order|to update|to change|to edit|เพื่อ|เพราะ|เนื่องจาก|จะปรับ|จะแก้|อัปเดต|แก้ไข)/i.test(thought || '');
938
+ if (!mentionsTarget || !explainsWhy) {
939
+ return {
940
+ ok: false,
941
+ message: `Before editing ${targetPath}, explain in the thought field which file you will edit and why this is the correct target.`
942
+ };
943
+ }
944
+
945
+ const sensitiveScratchPath = normalized.startsWith('scratch/') ||
946
+ normalized.startsWith('tests/fixtures/') ||
947
+ normalized.includes('/tests/fixtures/');
948
+ const mentionsSensitiveLocation = /(scratch|fixture|test fixture|tests\/fixtures|ทดลอง|fixture)/i.test(thought || '');
949
+ const marksIntentional = /(intentional|intentionally|disposable|test content|test fixture|ตั้งใจ|ชั่วคราว|เนื้อหาทดลอง|ไฟล์ทดสอบ)/i.test(thought || '');
950
+ if (sensitiveScratchPath && !(mentionsSensitiveLocation && marksIntentional)) {
951
+ return {
952
+ ok: false,
953
+ message: `Before editing ${targetPath}, explicitly mention that it is under scratch/ or tests/fixtures/ and why editing that disposable/test fixture content is intentional.`
954
+ };
955
+ }
956
+
957
+ return { ok: true };
958
+ }
959
+
960
+ function applyHunksToContent(content, hunks, filePath) {
961
+ let nextContent = content;
962
+ hunks.forEach((hunk, index) => {
963
+ if (typeof hunk.oldText !== 'string' || typeof hunk.newText !== 'string') {
964
+ throw new Error(`Patch hunk ${index + 1} is invalid.`);
965
+ }
966
+ if (!nextContent.includes(hunk.oldText)) {
967
+ throw new Error(`Patch hunk ${index + 1} oldText not found in ${filePath}`);
968
+ }
969
+ nextContent = nextContent.replace(hunk.oldText, hunk.newText);
970
+ });
971
+ return nextContent;
972
+ }
973
+
974
+ function applyPatch(workspaceRoot, patchInput) {
975
+ if (!patchInput || !patchInput.path) {
976
+ throw new Error('Patch path is required.');
977
+ }
978
+ const resolved = resolveWorkspacePath(workspaceRoot, patchInput.path);
979
+ if (!fs.existsSync(resolved)) {
980
+ throw new Error(`Patch target does not exist: ${patchInput.path}`);
981
+ }
982
+
983
+ const hunks = Array.isArray(patchInput.hunks) ? patchInput.hunks : [];
984
+ if (hunks.length === 0) {
985
+ throw new Error('Patch hunks are required.');
986
+ }
987
+
988
+ const content = applyHunksToContent(fs.readFileSync(resolved, 'utf8'), hunks, patchInput.path);
989
+
990
+ fs.writeFileSync(resolved, content, 'utf8');
991
+ return `Patched ${patchInput.path} with ${hunks.length} hunk(s).`;
992
+ }
993
+
994
+ function writeFile(workspaceRoot, targetPath, content) {
995
+ const resolved = resolveWorkspacePath(workspaceRoot, targetPath);
996
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
997
+ fs.writeFileSync(resolved, content || '', 'utf8');
998
+ return `Wrote ${targetPath}`;
660
999
  }
661
1000
 
662
1001
  async function getAgentDecision(client, observation, options = {}) {
@@ -770,7 +1109,14 @@ async function executeCodeTask(task, options = {}) {
770
1109
  const availableProviders = getAvailableProviders(config);
771
1110
  const providerOrder = getSupportedCodeProviderOrder(config, availableProviders, options.provider);
772
1111
  const provider = providerOrder[0];
773
- const client = new UnifiedAgentClient(provider, config, providerOrder);
1112
+ const client = new providerAdapter.AgentProviderClient({
1113
+ provider,
1114
+ config,
1115
+ providerOrder,
1116
+ systemInstruction: CODE_AGENT_PROMPT,
1117
+ responseMimeType: 'application/json',
1118
+ maxTokens: 8192
1119
+ });
774
1120
 
775
1121
  const initialObservationText = await buildInitialObservation(task, workspaceRoot, history);
776
1122
  const relevantMemoryCount = memoryStore.searchInteractions(task, 5).length;
@@ -795,6 +1141,25 @@ async function executeCodeTask(task, options = {}) {
795
1141
  let finalVerification = '';
796
1142
  let finalSessionSummary = '';
797
1143
  let executedSteps = 0;
1144
+ const readOnlyTask = isReadOnlyTask(task);
1145
+ const editPlanState = {
1146
+ approved: false,
1147
+ touchedFiles: new Set(),
1148
+ expectedFiles: new Set()
1149
+ };
1150
+ let verificationAttempts = 0;
1151
+ const verificationBudget = Number.isFinite(options.verificationBudget)
1152
+ ? options.verificationBudget
1153
+ : DEFAULT_VERIFICATION_BUDGET;
1154
+
1155
+ if (options.taskId) {
1156
+ taskManager.addCheckpoint(options.taskId, {
1157
+ phase: 'code_agent_start',
1158
+ message: task,
1159
+ provider,
1160
+ providerOrder
1161
+ });
1162
+ }
798
1163
 
799
1164
  for (let step = 1; step <= MAX_AGENT_STEPS; step++) {
800
1165
  executedSteps = step;
@@ -814,28 +1179,24 @@ async function executeCodeTask(task, options = {}) {
814
1179
  continue;
815
1180
  }
816
1181
 
817
- // Immediately show the agent's thought/reasoning
818
- onProgress({
819
- step,
820
- phase: 'acting',
821
- action: 'thinking',
822
- thought: decision.thought
823
- });
824
-
825
1182
  if (action === 'finish') {
1183
+ const missingPlanFiles = getMissingPlanFiles(editPlanState);
1184
+ if (missingPlanFiles.length > 0) {
1185
+ observation = [
1186
+ `Previous thought: ${decision.thought || '(none)'}`,
1187
+ 'Action: finish',
1188
+ 'Observation:',
1189
+ [
1190
+ 'Error: Approved plan is not complete yet.',
1191
+ `Missing planned file edits: ${missingPlanFiles.join(', ')}`,
1192
+ 'Complete every file listed in the approved plan before finishing, or create a new plan if the scope changed.'
1193
+ ].join('\n')
1194
+ ].join('\n');
1195
+ continue;
1196
+ }
826
1197
  finalSessionSummary = input.sessionSummary || input.summary || task;
827
1198
  finalSummary = input.summary || 'Task complete.';
828
1199
  finalVerification = input.verification || 'Not specified.';
829
- if (onFinalSummary) {
830
- await onFinalSummary({
831
- summary: finalSummary,
832
- verification: finalVerification,
833
- providerInfo: {
834
- provider: client.lastSuccessfulProvider || client.provider || provider,
835
- model: getCodeProviderModel(client.lastSuccessfulProvider || client.provider || provider, config)
836
- }
837
- });
838
- }
839
1200
  writeWorkspaceSession(workspaceRoot, {
840
1201
  summary: finalSessionSummary,
841
1202
  lastTask: task,
@@ -846,6 +1207,57 @@ async function executeCodeTask(task, options = {}) {
846
1207
 
847
1208
  let toolResult = '';
848
1209
  try {
1210
+ if (readOnlyTask && isWriteLikeAction(action)) {
1211
+ observation = [
1212
+ `Previous thought: ${decision.thought || '(none)'}`,
1213
+ `Action: ${action}`,
1214
+ 'Observation:',
1215
+ [
1216
+ 'Error: The latest user request is read-only and explicitly forbids edits or changes.',
1217
+ 'Do not create a plan or request approval for edits.',
1218
+ 'Use read_file/search_code/find_path as needed, then finish with an analysis summary.'
1219
+ ].join('\n')
1220
+ ].join('\n');
1221
+ continue;
1222
+ }
1223
+
1224
+ if (requiresMultiFilePlan(action, input, editPlanState)) {
1225
+ const nextPath = getEditTargetPath(action, input);
1226
+ observation = [
1227
+ `Previous thought: ${decision.thought || '(none)'}`,
1228
+ `Action: ${action}`,
1229
+ 'Observation:',
1230
+ [
1231
+ 'Error: Multi-file edit plan required before editing another file.',
1232
+ 'Use the "plan" action first with input.plan starting with "Plan:" bullets and input.files listing every file you expect to touch.',
1233
+ `Already edited: ${Array.from(editPlanState.touchedFiles).join(', ')}`,
1234
+ `Next requested file: ${nextPath}`
1235
+ ].join('\n')
1236
+ ].join('\n');
1237
+ continue;
1238
+ }
1239
+
1240
+ if (action === 'apply_patch' || action === 'write_file') {
1241
+ const explanation = validateEditExplanation(action, input, decision.thought);
1242
+ if (!explanation.ok) {
1243
+ observation = [
1244
+ `Previous thought: ${decision.thought || '(none)'}`,
1245
+ `Action: ${action}`,
1246
+ 'Observation:',
1247
+ `Error: ${explanation.message}`
1248
+ ].join('\n');
1249
+ continue;
1250
+ }
1251
+ }
1252
+
1253
+ // Show progress only after the action passes local validation, so retry attempts do not spam near-duplicate notes.
1254
+ onProgress({
1255
+ step,
1256
+ phase: 'acting',
1257
+ action: 'thinking',
1258
+ thought: decision.thought
1259
+ });
1260
+
849
1261
  switch (action) {
850
1262
  case 'web_search':
851
1263
  toolResult = await webSearch(input.query, onProgress);
@@ -857,7 +1269,7 @@ async function executeCodeTask(task, options = {}) {
857
1269
  toolResult = readFileRange(workspaceRoot, input.path, input.startLine, input.endLine);
858
1270
  break;
859
1271
  case 'search_code':
860
- toolResult = await searchCode(workspaceRoot, input.query);
1272
+ toolResult = await searchCode(workspaceRoot, input.query, input.path || '.');
861
1273
  break;
862
1274
  case 'find_path':
863
1275
  toolResult = await findPaths(workspaceRoot, input.query, input.type);
@@ -888,12 +1300,85 @@ async function executeCodeTask(task, options = {}) {
888
1300
  toolResult = await runShell(workspaceRoot, input.command);
889
1301
  break;
890
1302
  }
1303
+ case 'verify': {
1304
+ verificationAttempts += 1;
1305
+ const result = await runVerificationCommands(workspaceRoot, input.commands, {
1306
+ requestApproval,
1307
+ budget: verificationBudget,
1308
+ attempt: verificationAttempts
1309
+ });
1310
+ toolResult = result.output;
1311
+ if (options.taskId) {
1312
+ taskManager.addCheckpoint(options.taskId, {
1313
+ phase: 'verification',
1314
+ attempt: verificationAttempts,
1315
+ passed: result.passed,
1316
+ output: truncate(result.output, 4000)
1317
+ });
1318
+ }
1319
+ if (!result.passed && verificationAttempts >= verificationBudget) {
1320
+ toolResult += '\nVerification budget exhausted. Finish with the remaining failure clearly explained.';
1321
+ }
1322
+ break;
1323
+ }
1324
+ case 'plan': {
1325
+ const createdAt = new Date().toISOString();
1326
+ let planFile = writePlanFile(workspaceRoot, input, { task, createdAt });
1327
+ const approved = await requestApproval({
1328
+ type: 'plan',
1329
+ label: PLAN_FILE_LABEL,
1330
+ preview: planFile.content,
1331
+ summary: formatPlanApprovalSummary(input),
1332
+ openPath: planFile.path
1333
+ });
1334
+ if (!approved) {
1335
+ planFile = updatePlanApprovalStatus(planFile, input, {
1336
+ task,
1337
+ createdAt,
1338
+ approvalStatus: 'Denied',
1339
+ approvalTime: new Date().toISOString()
1340
+ });
1341
+ toolResult = 'User denied multi-file plan.';
1342
+ break;
1343
+ }
1344
+ planFile = updatePlanApprovalStatus(planFile, input, {
1345
+ task,
1346
+ createdAt,
1347
+ approvalStatus: 'Approved',
1348
+ approvalTime: new Date().toISOString()
1349
+ });
1350
+ editPlanState.approved = true;
1351
+ editPlanState.expectedFiles = new Set(
1352
+ Array.isArray(input.files)
1353
+ ? input.files.map(file => String(file || '').trim()).filter(Boolean)
1354
+ : []
1355
+ );
1356
+ safetyManager.appendActionLog({
1357
+ source: 'code_agent',
1358
+ action: 'plan',
1359
+ path: planFile.path,
1360
+ preview: planFile.content,
1361
+ approved
1362
+ });
1363
+ toolResult = `User approved multi-file plan at ${PLAN_FILE_LABEL}:\n${planFile.content}`;
1364
+ break;
1365
+ }
891
1366
  case 'apply_patch': {
892
1367
  const patchInput = input.patch || {};
1368
+ let patchWarnings = [];
1369
+ try {
1370
+ patchWarnings = buildApprovalWarnings(
1371
+ patchInput.path,
1372
+ buildPatchedContent(workspaceRoot, patchInput).nextContent
1373
+ );
1374
+ } catch (_) {
1375
+ patchWarnings = buildApprovalWarnings(patchInput.path, '');
1376
+ }
893
1377
  const approved = await requestApproval({
894
1378
  type: 'patch',
895
1379
  label: patchInput.path,
896
- preview: formatPatchPreview(patchInput)
1380
+ preview: formatPatchPreview(workspaceRoot, patchInput),
1381
+ warnings: patchWarnings
897
1382
  });
898
1383
  if (!approved) {
899
1384
  toolResult = `User denied patch for ${patchInput.path}`;
@@ -906,13 +1391,15 @@ async function executeCodeTask(task, options = {}) {
906
1391
  approved
907
1392
  });
908
1393
  toolResult = applyPatch(workspaceRoot, patchInput);
1394
+ editPlanState.touchedFiles.add(patchInput.path);
909
1395
  break;
910
1396
  }
911
1397
  case 'write_file': {
912
1398
  const approved = await requestApproval({
913
1399
  type: 'write_file',
914
1400
  label: input.path,
915
- preview: `${input.path}\n${truncate(input.content || '', 800)}`
1401
+ preview: formatWritePreview(workspaceRoot, input.path, input.content),
1402
+ warnings: buildApprovalWarnings(input.path, input.content)
916
1403
  });
917
1404
  if (!approved) {
918
1405
  toolResult = `User denied full file write for ${input.path}`;
@@ -925,6 +1412,7 @@ async function executeCodeTask(task, options = {}) {
925
1412
  approved
926
1413
  });
927
1414
  toolResult = writeFile(workspaceRoot, input.path, input.content);
1415
+ editPlanState.touchedFiles.add(input.path);
928
1416
  break;
929
1417
  }
930
1418
  case 'ask_user': {
@@ -933,11 +1421,23 @@ async function executeCodeTask(task, options = {}) {
933
1421
  break;
934
1422
  }
935
1423
  case 'open_url':
1424
+ case 'search':
936
1425
  case 'open_app':
1426
+ case 'web_automation':
937
1427
  case 'open_file':
938
1428
  case 'open_folder':
939
1429
  case 'create_folder':
1430
+ case 'delete_file':
1431
+ case 'clipboard_write':
1432
+ case 'learn_file':
1433
+ case 'learn_folder':
940
1434
  case 'system_info':
1435
+ case 'plugin':
1436
+ case 'mcp_tool':
1437
+ case 'mouse_move':
1438
+ case 'mouse_click':
1439
+ case 'type_text':
1440
+ case 'key_tap':
941
1441
  case 'system_automation': {
942
1442
  const executorAction = normalizeExecutorAction(action, input);
943
1443
  const safety = safetyManager.classifyAction(executorAction);
@@ -970,6 +1470,7 @@ async function executeCodeTask(task, options = {}) {
970
1470
  }
971
1471
 
972
1472
  const evaluation = evaluateActionResult(action, toolResult);
1473
+ const toolStatus = getToolCallStatus(action, toolResult, evaluation);
973
1474
  if (evaluation) {
974
1475
  onProgress({
975
1476
  step,
@@ -985,6 +1486,14 @@ async function executeCodeTask(task, options = {}) {
985
1486
  ].join('\n');
986
1487
  }
987
1488
 
1489
+ onProgress({
1490
+ step,
1491
+ phase: 'tool_call',
1492
+ action,
1493
+ status: toolStatus,
1494
+ target: summarizeToolTarget(action, input)
1495
+ });
1496
+
988
1497
  // Log the finished step with result
989
1498
  let resultSummary = '';
990
1499
  if (action === 'search_code') {
@@ -998,7 +1507,7 @@ async function executeCodeTask(task, options = {}) {
998
1507
  step,
999
1508
  phase: 'finished',
1000
1509
  action,
1001
- target: (input.path || input.command || input.query || '') + resultSummary
1510
+ target: summarizeToolTarget(action, input) + resultSummary
1002
1511
  });
1003
1512
 
1004
1513
  // Format tool result to be more readable and structured for the agent
@@ -1015,7 +1524,7 @@ async function executeCodeTask(task, options = {}) {
1015
1524
  ].join('\n'); }
1016
1525
 
1017
1526
  // Check for Agent Collaboration (Review) - Disabled by default to save tokens
1018
- if (config.enableAgentCollaboration === true && executedSteps > 8 && finalSummary) {
1527
+ if (config.enableAgentCollaboration === true && !readOnlyTask && executedSteps > 8 && finalSummary) {
1019
1528
  const availableProviders = getAvailableProviders(config);
1020
1529
  // Exclude providers that often need special local setup or are slow/unreliable for tiny reviews
1021
1530
  const altProviders = availableProviders.filter(p => p !== provider && p !== 'ollama' && p !== 'huggingface' && p !== 'local_openai');
@@ -1028,7 +1537,14 @@ async function executeCodeTask(task, options = {}) {
1028
1537
  if (reviewerProvider && finalSummary) {
1029
1538
  onProgress({ phase: 'reviewing', action: 'reviewer_start', message: `Invoking Reviewer Agent (${reviewerProvider})...` });
1030
1539
 
1031
- const reviewerClient = new UnifiedAgentClient(reviewerProvider, config);
1540
+ const reviewerClient = new providerAdapter.AgentProviderClient({
1541
+ provider: reviewerProvider,
1542
+ config,
1543
+ providerOrder: [reviewerProvider],
1544
+ systemInstruction: CODE_AGENT_PROMPT,
1545
+ responseMimeType: 'application/json',
1546
+ maxTokens: 4096
1547
+ });
1032
1548
  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.";
1033
1549
 
1034
1550
  const reviewPrompt = `The primary agent (${provider}) just completed the task: "${task}".\nSummary: ${finalSummary}\nVerification: ${finalVerification}\nGit Status: ${(await getGitContext(workspaceRoot)).status}\n\nPlease review this. Return JSON with action: 'finish'.`;
@@ -1048,15 +1564,20 @@ async function executeCodeTask(task, options = {}) {
1048
1564
  if (finalSummary) {
1049
1565
  memoryStore.recordInteraction(task, finalSummary);
1050
1566
  const answeredProvider = client.lastSuccessfulProvider || client.provider || provider;
1051
- return {
1567
+ const result = {
1052
1568
  summary: finalSummary,
1053
1569
  verification: finalVerification,
1054
1570
  steps: executedSteps,
1055
1571
  providerInfo: {
1056
1572
  provider: answeredProvider,
1057
- model: getCodeProviderModel(answeredProvider, config)
1573
+ model: getCodeProviderModel(answeredProvider, config),
1574
+ usage: client.getUsageSummary()
1058
1575
  }
1059
1576
  };
1577
+ if (onFinalSummary) {
1578
+ await onFinalSummary(result);
1579
+ }
1580
+ return result;
1060
1581
  }
1061
1582
 
1062
1583
  writeWorkspaceSession(workspaceRoot, {
@@ -1072,7 +1593,8 @@ async function executeCodeTask(task, options = {}) {
1072
1593
  steps: executedSteps || MAX_AGENT_STEPS,
1073
1594
  providerInfo: {
1074
1595
  provider: answeredProvider,
1075
- model: getCodeProviderModel(answeredProvider, config)
1596
+ model: getCodeProviderModel(answeredProvider, config),
1597
+ usage: client.getUsageSummary()
1076
1598
  }
1077
1599
  };
1078
1600
  }
@@ -1086,6 +1608,26 @@ module.exports = {
1086
1608
  findPaths,
1087
1609
  listFiles,
1088
1610
  searchCode,
1089
- walkDirectory
1611
+ runVerificationCommands,
1612
+ walkDirectory,
1613
+ buildUnifiedDiffPreview,
1614
+ buildFullFileDiffPreview,
1615
+ buildApprovalWarnings,
1616
+ validateEditExplanation,
1617
+ formatPatchPreview,
1618
+ formatWritePreview,
1619
+ formatPlanPreview,
1620
+ formatPlanApprovalSummary,
1621
+ formatPlanMarkdown,
1622
+ writePlanFile,
1623
+ updatePlanApprovalStatus,
1624
+ normalizePlanItems,
1625
+ normalizePlanItemLanguage,
1626
+ requiresMultiFilePlan,
1627
+ getMissingPlanFiles,
1628
+ isReadOnlyTask,
1629
+ isWriteLikeAction,
1630
+ getEditTargetPath,
1631
+ PLAN_FILE_PATH
1090
1632
  }
1091
1633
  };