@pheem49/mint 1.5.5 → 1.6.1
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/.codex +0 -0
- package/.github/FUNDING.yml +2 -0
- package/.github/workflows/ci.yml +45 -0
- package/.github/workflows/release.yml +79 -0
- package/Cargo.lock +5792 -0
- package/Cargo.toml +32 -0
- package/README.md +387 -353
- package/assets/icon.png +0 -0
- package/bin/mint +0 -0
- package/crates/mint-cli/Cargo.toml +23 -0
- package/crates/mint-cli/src/agent.rs +851 -0
- package/crates/mint-cli/src/gmail.rs +216 -0
- package/crates/mint-cli/src/image.rs +142 -0
- package/crates/mint-cli/src/main.rs +2837 -0
- package/crates/mint-cli/src/mcp.rs +63 -0
- package/crates/mint-cli/src/onboard.rs +1149 -0
- package/crates/mint-cli/src/setup.rs +390 -0
- package/crates/mint-cli/src/skills.rs +8 -0
- package/crates/mint-cli/src/updater.rs +279 -0
- package/crates/mint-core/Cargo.toml +22 -0
- package/crates/mint-core/src/agent_loop.rs +94 -0
- package/crates/mint-core/src/api_server.rs +991 -0
- package/crates/mint-core/src/channels.rs +248 -0
- package/crates/mint-core/src/chat.rs +895 -0
- package/crates/mint-core/src/code_tools.rs +729 -0
- package/crates/mint-core/src/config.rs +368 -0
- package/crates/mint-core/src/files.rs +159 -0
- package/crates/mint-core/src/knowledge.rs +541 -0
- package/crates/mint-core/src/lib.rs +84 -0
- package/crates/mint-core/src/mcp.rs +273 -0
- package/crates/mint-core/src/memory.rs +673 -0
- package/crates/mint-core/src/orchestration.rs +2157 -0
- package/crates/mint-core/src/pictures.rs +314 -0
- package/crates/mint-core/src/plugins.rs +727 -0
- package/crates/mint-core/src/safety.rs +416 -0
- package/crates/mint-core/src/semantic.rs +254 -0
- package/crates/mint-core/src/shell.rs +317 -0
- package/crates/mint-core/src/skills.rs +71 -0
- package/crates/mint-core/src/symbols.rs +157 -0
- package/crates/mint-core/src/tasks.rs +308 -0
- package/crates/mint-core/src/tts.rs +92 -0
- package/crates/mint-core/src/weather.rs +93 -0
- package/crates/mint-core/src/web_search.rs +200 -0
- package/crates/mint-core/src/workflows.rs +81 -0
- package/crates/mint-core/tests/mcp_stdio.rs +45 -0
- package/crates/mint-core/tests/memory_persistence.rs +172 -0
- package/crates/mint-core/tests/pictures_storage.rs +14 -0
- package/crates/mint-core/tests/task_lifecycle.rs +87 -0
- package/package.json +35 -99
- package/src/bin/index.js +16 -0
- package/src/renderer/index-web.html +17 -0
- package/src/renderer/index.html +17 -0
- package/src/renderer/public/Live2DCubismCore.js +9 -0
- package/src/renderer/public/assets/icon.png +0 -0
- package/src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.model3.json +36 -0
- package/src/renderer/src/App.tsx +33 -0
- package/src/renderer/src/calculator.ts +47 -0
- package/src/renderer/src/components/ChatPanel.tsx +1598 -0
- package/src/renderer/src/components/DashboardSidebar.tsx +358 -0
- package/src/renderer/src/components/Live2DStage.tsx +374 -0
- package/src/renderer/src/components/MintDashboard.tsx +950 -0
- package/src/renderer/src/components/ModelPanel.tsx +154 -0
- package/src/renderer/src/components/PicturesLibrary.tsx +46 -0
- package/src/renderer/src/components/ProactiveGlow.tsx +19 -0
- package/src/renderer/src/components/ScreenPicker.tsx +579 -0
- package/src/renderer/src/components/SettingsWindow.tsx +1467 -0
- package/src/renderer/src/components/SpotlightWindow.tsx +280 -0
- package/src/renderer/src/components/WidgetWindow.tsx +36 -0
- package/src/renderer/src/components/WorkspacePanel.tsx +268 -0
- package/src/{UI → renderer/src/css}/settings.css +69 -16
- package/src/renderer/src/css/spotlight.css +113 -0
- package/src/renderer/src/css/styles.css +3722 -0
- package/src/renderer/src/css/widget.css +185 -0
- package/src/renderer/src/env.d.ts +116 -0
- package/src/renderer/src/index.css +379 -0
- package/src/renderer/src/main.tsx +13 -0
- package/src/renderer/src/tauri.ts +996 -0
- package/src/renderer/src-web/App.tsx +25 -0
- package/src/renderer/src-web/calculator.ts +47 -0
- package/src/renderer/src-web/components/ChatPanel.tsx +1662 -0
- package/src/renderer/src-web/components/DashboardSidebar.tsx +242 -0
- package/src/renderer/src-web/components/MintDashboard.tsx +763 -0
- package/src/renderer/src-web/components/PicturesLibrary.tsx +73 -0
- package/src/renderer/src-web/components/SettingsWindow.tsx +1500 -0
- package/src/renderer/src-web/css/settings.css +1100 -0
- package/src/{UI → renderer/src-web/css}/spotlight.css +4 -4
- package/src/{UI → renderer/src-web/css}/styles.css +1055 -159
- package/src/{UI → renderer/src-web/css}/widget.css +2 -2
- package/src/renderer/src-web/env.d.ts +107 -0
- package/src/renderer/src-web/index.css +379 -0
- package/src/renderer/src-web/main.tsx +13 -0
- package/src/renderer/src-web/tauri.ts +983 -0
- package/tsconfig.json +30 -0
- package/vite.config.ts +33 -0
- package/vite.config.web.ts +51 -0
- package/GUIDE_TH.md +0 -125
- package/assets/Agent_Mint.png +0 -0
- package/assets/CLI_Screen.png +0 -0
- package/assets/Settings.png +0 -0
- package/benchmark_ai.js +0 -71
- package/install.ps1 +0 -64
- package/install.sh +0 -54
- package/main.js +0 -139
- package/mint-cli-logic.js +0 -3
- package/mint-cli.js +0 -410
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +0 -47
- package/models/Shiroko_Model/Shiroko//342/232/241/351/253/230/344/272/256/342/232/241/344/275/277/347/224/250/346/225/231/347/250/213/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.txt +0 -23
- package/preload-picker.js +0 -11
- package/preload-settings.js +0 -11
- package/preload.js +0 -41
- package/scripts/install_linux_desktop_entry.js +0 -48
- package/src/AI_Brain/Gemini_API.js +0 -813
- package/src/AI_Brain/agent_orchestrator.js +0 -73
- package/src/AI_Brain/autonomous_brain.js +0 -179
- package/src/AI_Brain/behavior_memory.js +0 -135
- package/src/AI_Brain/headless_agent.js +0 -143
- package/src/AI_Brain/knowledge_base.js +0 -349
- package/src/AI_Brain/memory_store.js +0 -662
- package/src/AI_Brain/proactive_engine.js +0 -172
- package/src/AI_Brain/provider_adapter.js +0 -365
- package/src/Automation_Layer/browser_automation.js +0 -149
- package/src/Automation_Layer/file_operations.js +0 -286
- package/src/Automation_Layer/open_app.js +0 -85
- package/src/Automation_Layer/open_website.js +0 -38
- package/src/CLI/approval_handler.js +0 -47
- package/src/CLI/chat_router.js +0 -247
- package/src/CLI/chat_ui.js +0 -1159
- package/src/CLI/cli_colors.js +0 -115
- package/src/CLI/cli_formatters.js +0 -94
- package/src/CLI/code_agent.js +0 -1667
- package/src/CLI/code_session_memory.js +0 -62
- package/src/CLI/gmail_auth.js +0 -210
- package/src/CLI/image_input.js +0 -90
- package/src/CLI/intent_detectors.js +0 -181
- package/src/CLI/interactive_chat.js +0 -658
- package/src/CLI/list_features.js +0 -64
- package/src/CLI/onboarding.js +0 -416
- package/src/CLI/repo_summarizer.js +0 -282
- package/src/CLI/semantic_code_search.js +0 -312
- package/src/CLI/skill_manager.js +0 -41
- package/src/CLI/slash_command_handler.js +0 -418
- package/src/CLI/symbol_indexer.js +0 -231
- package/src/CLI/updater.js +0 -230
- package/src/CLI/workspace_manager.js +0 -90
- package/src/Channels/brave_search_bridge.js +0 -35
- package/src/Channels/discord_bridge.js +0 -66
- package/src/Channels/google_search_bridge.js +0 -38
- package/src/Channels/line_bridge.js +0 -60
- package/src/Channels/slack_bridge.js +0 -48
- package/src/Channels/telegram_bridge.js +0 -41
- package/src/Channels/whatsapp_bridge.js +0 -57
- package/src/Command_Parser/parser.js +0 -45
- package/src/Plugins/dev_tools.js +0 -41
- package/src/Plugins/discord.js +0 -20
- package/src/Plugins/docker.js +0 -47
- package/src/Plugins/gmail.js +0 -251
- package/src/Plugins/google_calendar.js +0 -252
- package/src/Plugins/mcp_manager.js +0 -95
- package/src/Plugins/notion.js +0 -256
- package/src/Plugins/obsidian.js +0 -54
- package/src/Plugins/plugin_manager.js +0 -81
- package/src/Plugins/spotify.js +0 -173
- package/src/Plugins/system_metrics.js +0 -31
- package/src/Plugins/system_monitor.js +0 -72
- package/src/System/action_executor.js +0 -178
- package/src/System/bridge_manager.js +0 -76
- package/src/System/chat_history_manager.js +0 -83
- package/src/System/config_manager.js +0 -194
- package/src/System/custom_workflows.js +0 -163
- package/src/System/daemon_manager.js +0 -67
- package/src/System/google_tts_urls.js +0 -51
- package/src/System/granular_automation.js +0 -157
- package/src/System/ipc_handlers.js +0 -332
- package/src/System/notifications.js +0 -23
- package/src/System/optional_require.js +0 -23
- package/src/System/picture_store.js +0 -109
- package/src/System/proactive_loop.js +0 -153
- package/src/System/safety_manager.js +0 -273
- package/src/System/sandbox_runner.js +0 -182
- package/src/System/screen_capture.js +0 -175
- package/src/System/smart_context.js +0 -227
- package/src/System/system_automation.js +0 -162
- package/src/System/system_events.js +0 -79
- package/src/System/system_info.js +0 -125
- package/src/System/task_manager.js +0 -222
- package/src/System/tool_registry.js +0 -293
- package/src/System/window_manager.js +0 -220
- package/src/UI/floating.css +0 -80
- package/src/UI/floating.html +0 -17
- package/src/UI/floating.js +0 -67
- package/src/UI/live2d_manager.js +0 -600
- package/src/UI/preload-floating.js +0 -7
- package/src/UI/preload-spotlight.js +0 -11
- package/src/UI/preload-widget.js +0 -5
- package/src/UI/proactive-glow.html +0 -42
- package/src/UI/renderer.js +0 -2127
- package/src/UI/screenPicker.html +0 -214
- package/src/UI/screenPicker.js +0 -262
- package/src/UI/settings.html +0 -577
- package/src/UI/settings.js +0 -770
- package/src/UI/spotlight.html +0 -23
- package/src/UI/spotlight.js +0 -185
- package/src/UI/widget.html +0 -29
- package/src/UI/widget.js +0 -10
- /package/{models → src/renderer/public/models}/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/apron.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/catfilter.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/click.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/dazed.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253/347/234/274/347/217/240/346/221/207/346/231/203.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/dazedeyes.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/glasses.exp3.json} +0 -0
- /package/{models → src/renderer/public/models}/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/pen.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/photo.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_00.png} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_01.png} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_02.png} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_03.png} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.cdi3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.moc3} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.physics3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.vtube.json} +0 -0
package/src/CLI/code_agent.js
DELETED
|
@@ -1,1667 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const os = require('os');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const { execFile, execFileSync } = require('child_process');
|
|
5
|
-
const { promisify } = require('util');
|
|
6
|
-
const axios = require('axios');
|
|
7
|
-
const cheerio = require('cheerio');
|
|
8
|
-
const { readConfig, getAvailableProviders, CONFIG_DIR } = require('../System/config_manager');
|
|
9
|
-
const safetyManager = require('../System/safety_manager');
|
|
10
|
-
const memoryStore = require('../AI_Brain/memory_store');
|
|
11
|
-
const { readWorkspaceSession, writeWorkspaceSession } = require('./code_session_memory');
|
|
12
|
-
const { executeAction } = require('../System/action_executor');
|
|
13
|
-
const toolRegistry = require('../System/tool_registry');
|
|
14
|
-
const sandboxRunner = require('../System/sandbox_runner');
|
|
15
|
-
const providerAdapter = require('../AI_Brain/provider_adapter');
|
|
16
|
-
const taskManager = require('../System/task_manager');
|
|
17
|
-
|
|
18
|
-
async function webSearch(query, onProgress = () => {}) {
|
|
19
|
-
if (!query) throw new Error('Search query required.');
|
|
20
|
-
const config = readConfig();
|
|
21
|
-
const debug = process.env.MINT_DEBUG === '1';
|
|
22
|
-
const errors = [];
|
|
23
|
-
|
|
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)
|
|
30
|
-
if (config.googleSearchApiKey && config.googleSearchCx) {
|
|
31
|
-
try {
|
|
32
|
-
const GoogleSearch = require('../Channels/google_search_bridge');
|
|
33
|
-
const google = new GoogleSearch({ apiKey: config.googleSearchApiKey, cx: config.googleSearchCx });
|
|
34
|
-
const results = await google.search(query);
|
|
35
|
-
if (results.length > 0) {
|
|
36
|
-
return formatResults('Google Search API', results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n'));
|
|
37
|
-
}
|
|
38
|
-
} catch (e) {
|
|
39
|
-
errors.push(`Google: ${e.message}`);
|
|
40
|
-
if (debug) console.error('[webSearch] Google failed:', e.message);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// 2. Brave Search API (requires braveSearchApiKey in config)
|
|
45
|
-
if (config.braveSearchApiKey) {
|
|
46
|
-
try {
|
|
47
|
-
const BraveSearch = require('../Channels/brave_search_bridge');
|
|
48
|
-
const brave = new BraveSearch({ apiKey: config.braveSearchApiKey });
|
|
49
|
-
const results = await brave.search(query);
|
|
50
|
-
if (results.length > 0) {
|
|
51
|
-
return formatResults('Brave Search API', results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n'));
|
|
52
|
-
}
|
|
53
|
-
} catch (e) {
|
|
54
|
-
errors.push(`Brave: ${e.message}`);
|
|
55
|
-
if (debug) console.error('[webSearch] Brave failed:', e.message);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// 3. Fallback: DuckDuckGo HTML (No key required, but might get blocked by Captcha)
|
|
60
|
-
try {
|
|
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
|
-
}
|
|
70
|
-
}
|
|
71
|
-
);
|
|
72
|
-
const $ddg = cheerio.load(ddgResponse.data);
|
|
73
|
-
const ddgResults = [];
|
|
74
|
-
$ddg('.result__body').each((i, el) => {
|
|
75
|
-
if (i >= 5) return false;
|
|
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}`);
|
|
80
|
-
});
|
|
81
|
-
if (ddgResults.length > 0) {
|
|
82
|
-
return formatResults('DuckDuckGo', ddgResults.join('\n\n'));
|
|
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
|
-
}
|
|
90
|
-
|
|
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');
|
|
103
|
-
} catch (e) {
|
|
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.`;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const execFileAsync = promisify(execFile);
|
|
123
|
-
const MAX_TOOL_OUTPUT = 12000;
|
|
124
|
-
const MAX_AGENT_STEPS = 16;
|
|
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');
|
|
130
|
-
const SUPPORTED_CODE_PROVIDERS = ['gemini', 'anthropic', 'openai', 'local_openai'];
|
|
131
|
-
|
|
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.
|
|
133
|
-
You work in an inspect -> plan -> act -> verify loop.
|
|
134
|
-
|
|
135
|
-
PERSONALITY & TONE:
|
|
136
|
-
- Gender: Female.
|
|
137
|
-
- Persona: Friendly, calm, concise, and technically direct. Avoid excessive praise, roleplay, or filler.
|
|
138
|
-
- Language routing is mandatory and based on the user's latest message:
|
|
139
|
-
- If the latest user message contains Thai characters, respond in Thai.
|
|
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.
|
|
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.
|
|
144
|
-
- Politeness:
|
|
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.
|
|
150
|
-
|
|
151
|
-
Rules:
|
|
152
|
-
1. Respond with valid JSON only.
|
|
153
|
-
2. If the user asks a conversational question, you can just use "finish" to reply directly.
|
|
154
|
-
3. If you need information, use "web_search", "read_file", or "ask_user" before replying.
|
|
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.
|
|
158
|
-
6. Never use destructive commands like "rm -rf", "git reset --hard", or overwrite unrelated files.
|
|
159
|
-
7. Before any shell command or file patch is executed, the user must approve it. Plan accordingly.
|
|
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.
|
|
183
|
-
|
|
184
|
-
Response format:
|
|
185
|
-
{
|
|
186
|
-
"thought": "short reasoning about what to do next",
|
|
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",
|
|
188
|
-
"input": {
|
|
189
|
-
"question": "your question to the user for ask_user",
|
|
190
|
-
"query": "search text for web_search, search_code, or find_path",
|
|
191
|
-
"target": "URL for open_url, app name for open_app, or command for system_automation",
|
|
192
|
-
"path": "relative/path",
|
|
193
|
-
"type": "file" | "dir" | "any",
|
|
194
|
-
"command": "shell command",
|
|
195
|
-
"commands": ["npm test", "npm run build"],
|
|
196
|
-
"startLine": 1,
|
|
197
|
-
"endLine": 120,
|
|
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"],
|
|
201
|
-
"summary": "your final conversational or technical response to the user (Matches user language and uses polite particles)",
|
|
202
|
-
"verification": "tests or checks (if applicable)",
|
|
203
|
-
"sessionSummary": "brief persistent summary for the workspace",
|
|
204
|
-
"patch": {
|
|
205
|
-
"path": "relative/path",
|
|
206
|
-
"hunks": [
|
|
207
|
-
{
|
|
208
|
-
"oldText": "exact existing text",
|
|
209
|
-
"newText": "replacement text"
|
|
210
|
-
}
|
|
211
|
-
]
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
Tool notes:
|
|
217
|
-
- "web_search": search the internet for information when you lack knowledge.
|
|
218
|
-
- "list_files": inspect the workspace or a subdirectory.
|
|
219
|
-
- "read_file": read a file, optionally with startLine/endLine.
|
|
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.
|
|
221
|
-
- "find_path": find files or directories by path/name when the user is looking for a folder, filename, or location.
|
|
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.
|
|
225
|
-
- "apply_patch": update an existing file using one or more exact replacement hunks.
|
|
226
|
-
- "write_file": create a new file or fully rewrite a file when replacement is not practical.
|
|
227
|
-
- "ask_user": ask the user for clarification, preference, or more information before proceeding.
|
|
228
|
-
- "open_url": open a URL in the user's default browser.
|
|
229
|
-
- "open_app": open a local application on the user's computer.
|
|
230
|
-
- "system_info": get system information like CPU, memory, date, or weather.
|
|
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.
|
|
234
|
-
- "finish": stop and reply to the user using the "summary" field.
|
|
235
|
-
`;
|
|
236
|
-
|
|
237
|
-
function truncate(text, max = MAX_TOOL_OUTPUT) {
|
|
238
|
-
if (!text) return '';
|
|
239
|
-
return text.length > max ? `${text.slice(0, max)}\n...<truncated>` : text;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function extractJson(text) {
|
|
243
|
-
try {
|
|
244
|
-
return JSON.parse(text);
|
|
245
|
-
} catch (error) {
|
|
246
|
-
const match = text.match(/\{[\s\S]*\}/);
|
|
247
|
-
if (!match) {
|
|
248
|
-
throw error;
|
|
249
|
-
}
|
|
250
|
-
return JSON.parse(match[0]);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function normalizeExecutorAction(action, input = {}) {
|
|
255
|
-
return {
|
|
256
|
-
type: action,
|
|
257
|
-
target: input.target || input.path || input.query || '',
|
|
258
|
-
path: input.path,
|
|
259
|
-
pathType: input.type,
|
|
260
|
-
openAfter: input.openAfter,
|
|
261
|
-
pluginName: input.pluginName,
|
|
262
|
-
server: input.server,
|
|
263
|
-
args: input.args,
|
|
264
|
-
x: input.x,
|
|
265
|
-
y: input.y,
|
|
266
|
-
button: input.button
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
|
|
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
|
-
}
|
|
275
|
-
if (input.command) return input.command;
|
|
276
|
-
if (input.path) return input.path;
|
|
277
|
-
if (input.target) return input.target;
|
|
278
|
-
if (input.query) return input.query;
|
|
279
|
-
return action;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function evaluateActionResult(action, toolResult = '') {
|
|
283
|
-
if (!toolRegistry.isImportantAction(action)) {
|
|
284
|
-
return null;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const text = String(toolResult || '');
|
|
288
|
-
if (/^Error:|blocked|denied|failed|exception|not found/i.test(text)) {
|
|
289
|
-
return {
|
|
290
|
-
status: 'failed',
|
|
291
|
-
message: `Evaluator: ${action} may have failed. Review the observation before continuing.`
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (action === 'run_shell' && /(ERR!|Error:|FAIL|failed|not found|permission denied)/i.test(text)) {
|
|
296
|
-
return {
|
|
297
|
-
status: 'warning',
|
|
298
|
-
message: 'Evaluator: shell output contains error-like text; verify before claiming success.'
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return {
|
|
303
|
-
status: 'passed',
|
|
304
|
-
message: `Evaluator: ${action} completed without obvious errors.`
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
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';
|
|
312
|
-
}
|
|
313
|
-
if (evaluation && evaluation.status === 'failed') {
|
|
314
|
-
return 'failed';
|
|
315
|
-
}
|
|
316
|
-
if (action === 'run_shell' && /(ERR!|Error:|FAIL|failed|not found|permission denied)/i.test(text)) {
|
|
317
|
-
return 'failed';
|
|
318
|
-
}
|
|
319
|
-
return 'success';
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function summarizeToolTarget(action, input = {}) {
|
|
323
|
-
if (action === 'plan') return 'Multi-file plan';
|
|
324
|
-
return formatActionPreview(action, input);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function getSupportedCodeProviderOrder(config, availableProviders = getAvailableProviders(config || {}), requestedOverride = null) {
|
|
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
|
-
});
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function selectSupportedCodeProvider(config, availableProviders = getAvailableProviders(config || {})) {
|
|
337
|
-
return getSupportedCodeProviderOrder(config, availableProviders)[0];
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function getCodeProviderModel(provider, config = {}) {
|
|
341
|
-
return providerAdapter.getProviderModel(provider, config);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function resolveWorkspacePath(workspaceRoot, targetPath = '.') {
|
|
345
|
-
const resolved = path.resolve(workspaceRoot, targetPath);
|
|
346
|
-
const relative = path.relative(workspaceRoot, resolved);
|
|
347
|
-
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
348
|
-
throw new Error(`Path is outside the workspace: ${targetPath}`);
|
|
349
|
-
}
|
|
350
|
-
return resolved;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
async function safeExecFile(command, args, options = {}) {
|
|
354
|
-
try {
|
|
355
|
-
return await execFileAsync(command, args, {
|
|
356
|
-
maxBuffer: 1024 * 1024 * 4,
|
|
357
|
-
...options
|
|
358
|
-
});
|
|
359
|
-
} catch (error) {
|
|
360
|
-
if (typeof error.code === 'number' && error.code === 1) {
|
|
361
|
-
return { stdout: error.stdout || '', stderr: error.stderr || '' };
|
|
362
|
-
}
|
|
363
|
-
throw error;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const IGNORED_DIRS = ['.git', 'node_modules', '.cache', 'dist', 'build', 'out'];
|
|
368
|
-
|
|
369
|
-
function walkDirectory(dir, workspaceRoot, results = [], max = 400) {
|
|
370
|
-
let entries = [];
|
|
371
|
-
try {
|
|
372
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
373
|
-
} catch (e) {
|
|
374
|
-
return results;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
for (const entry of entries) {
|
|
378
|
-
const fullPath = path.join(dir, entry.name);
|
|
379
|
-
if (entry.isDirectory()) {
|
|
380
|
-
if (IGNORED_DIRS.includes(entry.name)) continue;
|
|
381
|
-
walkDirectory(fullPath, workspaceRoot, results, max);
|
|
382
|
-
} else {
|
|
383
|
-
results.push(path.relative(workspaceRoot, fullPath));
|
|
384
|
-
}
|
|
385
|
-
if (results.length >= max) break;
|
|
386
|
-
}
|
|
387
|
-
return results;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
async function listFiles(workspaceRoot, targetPath = '.') {
|
|
391
|
-
const cwd = resolveWorkspacePath(workspaceRoot, targetPath);
|
|
392
|
-
try {
|
|
393
|
-
const { stdout } = await execFileAsync('rg', ['--files', cwd], { cwd: workspaceRoot, maxBuffer: 1024 * 1024 * 4 });
|
|
394
|
-
const rel = stdout
|
|
395
|
-
.split('\n')
|
|
396
|
-
.filter(Boolean)
|
|
397
|
-
.map(file => path.relative(workspaceRoot, file))
|
|
398
|
-
.slice(0, 400)
|
|
399
|
-
.join('\n');
|
|
400
|
-
return rel || '(no files found)';
|
|
401
|
-
} catch (error) {
|
|
402
|
-
if (error.code !== 'ENOENT' && error.stdout) {
|
|
403
|
-
return truncate(error.stdout);
|
|
404
|
-
}
|
|
405
|
-
// Recursive fallback for missing ripgrep
|
|
406
|
-
const files = walkDirectory(cwd, workspaceRoot, [], 400);
|
|
407
|
-
return files.join('\n') || '(no files found)';
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function readFileRange(workspaceRoot, targetPath, startLine = 1, endLine = 200) {
|
|
412
|
-
const resolved = resolveWorkspacePath(workspaceRoot, targetPath);
|
|
413
|
-
const content = fs.readFileSync(resolved, 'utf8');
|
|
414
|
-
const lines = content.split('\n');
|
|
415
|
-
const start = Math.max(1, startLine);
|
|
416
|
-
const end = Math.max(start, endLine);
|
|
417
|
-
return lines
|
|
418
|
-
.slice(start - 1, end)
|
|
419
|
-
.map((line, index) => `${start + index}: ${line}`)
|
|
420
|
-
.join('\n');
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
async function searchCode(workspaceRoot, query, targetPath = '.') {
|
|
424
|
-
if (!query || !query.trim()) {
|
|
425
|
-
throw new Error('Search query is required.');
|
|
426
|
-
}
|
|
427
|
-
const searchRoot = resolveWorkspacePath(workspaceRoot, targetPath || '.');
|
|
428
|
-
if (!fs.existsSync(searchRoot)) {
|
|
429
|
-
throw new Error(`Search path does not exist: ${targetPath}`);
|
|
430
|
-
}
|
|
431
|
-
try {
|
|
432
|
-
const { stdout } = await execFileAsync('rg', ['-n', '--hidden', '--glob', '!.git', query, searchRoot], {
|
|
433
|
-
cwd: workspaceRoot,
|
|
434
|
-
maxBuffer: 1024 * 1024 * 4
|
|
435
|
-
});
|
|
436
|
-
return truncate(stdout || '(no matches)');
|
|
437
|
-
} catch (error) {
|
|
438
|
-
if (typeof error.code === 'number' && error.code === 1) {
|
|
439
|
-
return '(no matches)';
|
|
440
|
-
}
|
|
441
|
-
if (error.code === 'ENOENT') {
|
|
442
|
-
// Recursive fallback search for missing ripgrep
|
|
443
|
-
const results = [];
|
|
444
|
-
const files = walkDirectory(searchRoot, workspaceRoot, [], 1000);
|
|
445
|
-
const lowerQuery = query.toLowerCase();
|
|
446
|
-
|
|
447
|
-
for (const relPath of files) {
|
|
448
|
-
try {
|
|
449
|
-
const fullPath = path.join(workspaceRoot, relPath);
|
|
450
|
-
const content = fs.readFileSync(fullPath, 'utf8');
|
|
451
|
-
const lines = content.split('\n');
|
|
452
|
-
lines.forEach((line, idx) => {
|
|
453
|
-
if (line.toLowerCase().includes(lowerQuery)) {
|
|
454
|
-
results.push(`${relPath}:${idx + 1}:${line.trim()}`);
|
|
455
|
-
}
|
|
456
|
-
});
|
|
457
|
-
} catch (e) {
|
|
458
|
-
// Skip binary or unreadable files
|
|
459
|
-
}
|
|
460
|
-
if (results.length >= 100) break;
|
|
461
|
-
}
|
|
462
|
-
return truncate(results.join('\n') || '(no matches)');
|
|
463
|
-
}
|
|
464
|
-
if (error.stdout) {
|
|
465
|
-
return truncate(error.stdout);
|
|
466
|
-
}
|
|
467
|
-
throw error;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
async function findPaths(workspaceRoot, query, type = 'any') {
|
|
472
|
-
if (!query || !query.trim()) {
|
|
473
|
-
throw new Error('Path search query is required.');
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
const normalizedType = ['file', 'dir', 'any'].includes(type) ? type : 'any';
|
|
477
|
-
const loweredQuery = query.trim().toLowerCase();
|
|
478
|
-
const results = [];
|
|
479
|
-
|
|
480
|
-
function visit(currentPath) {
|
|
481
|
-
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
482
|
-
for (const entry of entries) {
|
|
483
|
-
const absoluteEntryPath = path.join(currentPath, entry.name);
|
|
484
|
-
const relativeEntryPath = path.relative(workspaceRoot, absoluteEntryPath) || '.';
|
|
485
|
-
const entryType = entry.isDirectory() ? 'dir' : 'file';
|
|
486
|
-
const matchesType = normalizedType === 'any' || normalizedType === entryType;
|
|
487
|
-
const matchesQuery = entry.name.toLowerCase().includes(loweredQuery) || relativeEntryPath.toLowerCase().includes(loweredQuery);
|
|
488
|
-
|
|
489
|
-
if (matchesType && matchesQuery) {
|
|
490
|
-
results.push(`${entryType === 'dir' ? '[dir]' : '[file]'} ${relativeEntryPath}`);
|
|
491
|
-
if (results.length >= 200) return;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (entry.isDirectory() && results.length < 200) {
|
|
495
|
-
visit(absoluteEntryPath);
|
|
496
|
-
if (results.length >= 200) return;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
visit(workspaceRoot);
|
|
502
|
-
return results.length > 0 ? results.join('\n') : '(no matching paths)';
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function assertSafeShell(command) {
|
|
506
|
-
return safetyManager.assertShellCommandAllowed(command);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
async function runShell(workspaceRoot, command) {
|
|
510
|
-
if (!command || !command.trim()) {
|
|
511
|
-
throw new Error('Shell command is required.');
|
|
512
|
-
}
|
|
513
|
-
assertSafeShell(command);
|
|
514
|
-
const { stdout, stderr } = await sandboxRunner.runShell(command, {
|
|
515
|
-
source: 'code_agent',
|
|
516
|
-
cwd: workspaceRoot,
|
|
517
|
-
maxBuffer: 1024 * 1024 * 4
|
|
518
|
-
});
|
|
519
|
-
return truncate([stdout, stderr].filter(Boolean).join('\n') || '(no output)');
|
|
520
|
-
}
|
|
521
|
-
|
|
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
|
-
};
|
|
580
|
-
}
|
|
581
|
-
|
|
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) {
|
|
651
|
-
if (!patchInput || !patchInput.path) {
|
|
652
|
-
throw new Error('Patch path is required.');
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
const resolved = resolveWorkspacePath(workspaceRoot, patchInput.path);
|
|
656
|
-
if (!fs.existsSync(resolved)) {
|
|
657
|
-
throw new Error(`Patch target does not exist: ${patchInput.path}`);
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
const hunks = Array.isArray(patchInput.hunks) ? patchInput.hunks : [];
|
|
661
|
-
if (hunks.length === 0) {
|
|
662
|
-
throw new Error('Patch hunks are required.');
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
const previousContent = fs.readFileSync(resolved, 'utf8');
|
|
666
|
-
return {
|
|
667
|
-
previousContent,
|
|
668
|
-
nextContent: applyHunksToContent(previousContent, hunks, patchInput.path)
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
function buildUnifiedDiffPreview(workspaceRoot, patchInput, options = {}) {
|
|
673
|
-
const { previousContent, nextContent } = buildPatchedContent(workspaceRoot, patchInput);
|
|
674
|
-
return buildContentDiffPreview(patchInput.path, previousContent, nextContent);
|
|
675
|
-
}
|
|
676
|
-
|
|
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
|
-
|
|
690
|
-
const resolved = resolveWorkspacePath(workspaceRoot, targetPath);
|
|
691
|
-
const previousContent = fs.existsSync(resolved) ? fs.readFileSync(resolved, 'utf8') : '';
|
|
692
|
-
return buildContentDiffPreview(targetPath, previousContent, nextContent || '');
|
|
693
|
-
}
|
|
694
|
-
|
|
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)}`;
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
function normalizeRelativePathForWarning(targetPath = '') {
|
|
704
|
-
return String(targetPath || '').replace(/\\/g, '/').replace(/^\.\/+/, '');
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
function contentLooksLikeGuide(text = '') {
|
|
708
|
-
return /(guide|installation|publish|npm|registry|setup|documentation|คู่มือ|ติดตั้ง|เผยแพร่)/i.test(String(text || ''));
|
|
709
|
-
}
|
|
710
|
-
|
|
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
|
-
}
|
|
718
|
-
|
|
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.');
|
|
738
|
-
}
|
|
739
|
-
|
|
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);
|
|
748
|
-
}
|
|
749
|
-
return String(plan || '')
|
|
750
|
-
.split('\n')
|
|
751
|
-
.map(line => line.trim())
|
|
752
|
-
.filter(Boolean);
|
|
753
|
-
}
|
|
754
|
-
|
|
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
|
-
}
|
|
777
|
-
}
|
|
778
|
-
|
|
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}`);
|
|
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
|
-
}
|
|
874
|
-
|
|
875
|
-
function getEditTargetPath(action, input = {}) {
|
|
876
|
-
if (action === 'apply_patch') {
|
|
877
|
-
return input.patch && input.patch.path ? String(input.patch.path) : '';
|
|
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}`;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
async function getAgentDecision(client, observation, options = {}) {
|
|
1002
|
-
const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
|
|
1003
|
-
const step = options.step || 0;
|
|
1004
|
-
|
|
1005
|
-
let rawText = await client.sendMessage(observation);
|
|
1006
|
-
for (let attempt = 0; attempt <= MAX_JSON_REPAIR_ATTEMPTS; attempt++) {
|
|
1007
|
-
try {
|
|
1008
|
-
return extractJson(rawText);
|
|
1009
|
-
} catch (error) {
|
|
1010
|
-
if (attempt === MAX_JSON_REPAIR_ATTEMPTS) {
|
|
1011
|
-
throw new Error(`Agent returned invalid JSON after ${MAX_JSON_REPAIR_ATTEMPTS + 1} attempts: ${error.message}`);
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
onProgress({ step, phase: 'repairing', action: 'json_repair', message: `invalid JSON response, requesting repair (${attempt + 1}/${MAX_JSON_REPAIR_ATTEMPTS})` });
|
|
1015
|
-
rawText = await client.sendMessage([
|
|
1016
|
-
'Your previous response was not valid JSON for Code Mode.',
|
|
1017
|
-
'Reply again with valid JSON only, following the required schema exactly.',
|
|
1018
|
-
`Previous response:\n${truncate(rawText, 4000)}`
|
|
1019
|
-
].join('\n'));
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
function detectPackageManager(workspaceRoot) {
|
|
1025
|
-
if (fs.existsSync(path.join(workspaceRoot, 'package-lock.json'))) return 'npm';
|
|
1026
|
-
if (fs.existsSync(path.join(workspaceRoot, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
1027
|
-
if (fs.existsSync(path.join(workspaceRoot, 'yarn.lock'))) return 'yarn';
|
|
1028
|
-
if (fs.existsSync(path.join(workspaceRoot, 'bun.lockb')) || fs.existsSync(path.join(workspaceRoot, 'bun.lock'))) return 'bun';
|
|
1029
|
-
return 'npm';
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
function detectTestCommands(workspaceRoot) {
|
|
1033
|
-
const commands = [];
|
|
1034
|
-
const packageJsonPath = path.join(workspaceRoot, 'package.json');
|
|
1035
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
1036
|
-
try {
|
|
1037
|
-
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
1038
|
-
const scripts = pkg.scripts || {};
|
|
1039
|
-
if (scripts.test) commands.push(`${detectPackageManager(workspaceRoot)} test`);
|
|
1040
|
-
if (scripts.lint) commands.push(`${detectPackageManager(workspaceRoot)} run lint`);
|
|
1041
|
-
if (scripts.build) commands.push(`${detectPackageManager(workspaceRoot)} run build`);
|
|
1042
|
-
} catch (error) {
|
|
1043
|
-
// Ignore malformed package.json for context gathering.
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
return commands;
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
async function getGitContext(workspaceRoot) {
|
|
1050
|
-
const gitDir = path.join(workspaceRoot, '.git');
|
|
1051
|
-
if (!fs.existsSync(gitDir)) {
|
|
1052
|
-
return {
|
|
1053
|
-
isRepo: false,
|
|
1054
|
-
branch: '(not a git repo)',
|
|
1055
|
-
status: '',
|
|
1056
|
-
diffSummary: ''
|
|
1057
|
-
};
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
const branch = (await safeExecFile('git', ['branch', '--show-current'], { cwd: workspaceRoot })).stdout.trim() || '(detached HEAD)';
|
|
1061
|
-
const status = truncate((await safeExecFile('git', ['status', '--short'], { cwd: workspaceRoot })).stdout.trim() || '(clean)');
|
|
1062
|
-
const diffSummary = truncate((await safeExecFile('git', ['diff', '--stat'], { cwd: workspaceRoot })).stdout.trim() || '(no unstaged diff)');
|
|
1063
|
-
return { isRepo: true, branch, status, diffSummary };
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
async function buildInitialObservation(task, workspaceRoot, history = []) {
|
|
1067
|
-
const session = readWorkspaceSession(workspaceRoot);
|
|
1068
|
-
const gitContext = await getGitContext(workspaceRoot);
|
|
1069
|
-
const testCommands = detectTestCommands(workspaceRoot);
|
|
1070
|
-
const userContext = memoryStore.getUserContext(task);
|
|
1071
|
-
|
|
1072
|
-
const contextStr = history.length > 0
|
|
1073
|
-
? `Recent Context:\n${history.slice(-10).map(m => `${m.sender}: ${m.text}`).join('\n')}\n`
|
|
1074
|
-
: '';
|
|
1075
|
-
|
|
1076
|
-
return [
|
|
1077
|
-
contextStr,
|
|
1078
|
-
`Task: ${task}`,
|
|
1079
|
-
`Workspace: ${workspaceRoot}`,
|
|
1080
|
-
`Git branch: ${gitContext.branch}`,
|
|
1081
|
-
'Git status:',
|
|
1082
|
-
gitContext.status || '(none)',
|
|
1083
|
-
'Git diff summary:',
|
|
1084
|
-
gitContext.diffSummary || '(none)',
|
|
1085
|
-
'Suggested verification commands:',
|
|
1086
|
-
testCommands.length > 0 ? testCommands.join('\n') : '(none detected)',
|
|
1087
|
-
'Previous workspace session summary:',
|
|
1088
|
-
session.summary || '(none)',
|
|
1089
|
-
`Previous task: ${session.lastTask || '(none)'}`,
|
|
1090
|
-
`Previous verification: ${session.lastVerification || '(none)'}`,
|
|
1091
|
-
'Long-term user context:',
|
|
1092
|
-
userContext || '(none)',
|
|
1093
|
-
'If the task is conversational or trivial, finish directly without inspecting the workspace. For code/workspace tasks, inspect before making edits.'
|
|
1094
|
-
].join('\n');
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
async function executeCodeTask(task, options = {}) {
|
|
1098
|
-
if (options.signal && options.signal.aborted) {
|
|
1099
|
-
throw new Error('Task cancelled by user.');
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
let onAbort;
|
|
1103
|
-
const abortPromise = new Promise((_, reject) => {
|
|
1104
|
-
if (options.signal) {
|
|
1105
|
-
onAbort = () => {
|
|
1106
|
-
reject(new Error('Task cancelled by user.'));
|
|
1107
|
-
};
|
|
1108
|
-
options.signal.addEventListener('abort', onAbort);
|
|
1109
|
-
}
|
|
1110
|
-
});
|
|
1111
|
-
|
|
1112
|
-
try {
|
|
1113
|
-
if (options.signal) {
|
|
1114
|
-
return await Promise.race([_executeCodeTaskInternal(task, options), abortPromise]);
|
|
1115
|
-
} else {
|
|
1116
|
-
return await _executeCodeTaskInternal(task, options);
|
|
1117
|
-
}
|
|
1118
|
-
} finally {
|
|
1119
|
-
if (options.signal && onAbort) {
|
|
1120
|
-
options.signal.removeEventListener('abort', onAbort);
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
async function _executeCodeTaskInternal(task, options = {}) {
|
|
1126
|
-
const workspaceRoot = path.resolve(options.cwd || process.cwd());
|
|
1127
|
-
const history = options.history || [];
|
|
1128
|
-
const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
|
|
1129
|
-
const onFinalSummary = typeof options.onFinalSummary === 'function' ? options.onFinalSummary : null;
|
|
1130
|
-
const requestApproval = typeof options.requestApproval === 'function'
|
|
1131
|
-
? options.requestApproval
|
|
1132
|
-
: async () => true;
|
|
1133
|
-
const askUser = typeof options.askUser === 'function'
|
|
1134
|
-
? options.askUser
|
|
1135
|
-
: async (q) => `User didn't answer: ${q}`;
|
|
1136
|
-
const config = readConfig();
|
|
1137
|
-
const availableProviders = getAvailableProviders(config);
|
|
1138
|
-
const providerOrder = getSupportedCodeProviderOrder(config, availableProviders, options.provider);
|
|
1139
|
-
const provider = providerOrder[0];
|
|
1140
|
-
const client = new providerAdapter.AgentProviderClient({
|
|
1141
|
-
provider,
|
|
1142
|
-
config,
|
|
1143
|
-
providerOrder,
|
|
1144
|
-
systemInstruction: CODE_AGENT_PROMPT,
|
|
1145
|
-
responseMimeType: 'application/json',
|
|
1146
|
-
maxTokens: 8192
|
|
1147
|
-
});
|
|
1148
|
-
|
|
1149
|
-
const initialObservationText = await buildInitialObservation(task, workspaceRoot, history);
|
|
1150
|
-
const relevantMemoryCount = memoryStore.searchInteractions(task, 5).length;
|
|
1151
|
-
onProgress({
|
|
1152
|
-
phase: 'memory',
|
|
1153
|
-
action: 'memory_context',
|
|
1154
|
-
message: `Loaded memory: profile + recent history, ${relevantMemoryCount} direct match${relevantMemoryCount === 1 ? '' : 'es'}`
|
|
1155
|
-
});
|
|
1156
|
-
let observation = options.imageDataUri
|
|
1157
|
-
? {
|
|
1158
|
-
text: [
|
|
1159
|
-
initialObservationText,
|
|
1160
|
-
'',
|
|
1161
|
-
`[Attached image: ${options.imagePath || 'command-line image'}]`,
|
|
1162
|
-
'Use the attached image as visual context when planning and answering.'
|
|
1163
|
-
].join('\n'),
|
|
1164
|
-
imageDataUri: options.imageDataUri
|
|
1165
|
-
}
|
|
1166
|
-
: initialObservationText;
|
|
1167
|
-
|
|
1168
|
-
let finalSummary = '';
|
|
1169
|
-
let finalVerification = '';
|
|
1170
|
-
let finalSessionSummary = '';
|
|
1171
|
-
let executedSteps = 0;
|
|
1172
|
-
const readOnlyTask = isReadOnlyTask(task);
|
|
1173
|
-
const editPlanState = {
|
|
1174
|
-
approved: false,
|
|
1175
|
-
touchedFiles: new Set(),
|
|
1176
|
-
expectedFiles: new Set()
|
|
1177
|
-
};
|
|
1178
|
-
let verificationAttempts = 0;
|
|
1179
|
-
const verificationBudget = Number.isFinite(options.verificationBudget)
|
|
1180
|
-
? options.verificationBudget
|
|
1181
|
-
: DEFAULT_VERIFICATION_BUDGET;
|
|
1182
|
-
|
|
1183
|
-
if (options.taskId) {
|
|
1184
|
-
taskManager.addCheckpoint(options.taskId, {
|
|
1185
|
-
phase: 'code_agent_start',
|
|
1186
|
-
message: task,
|
|
1187
|
-
provider,
|
|
1188
|
-
providerOrder
|
|
1189
|
-
});
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
for (let step = 1; step <= MAX_AGENT_STEPS; step++) {
|
|
1193
|
-
if (options.signal && options.signal.aborted) {
|
|
1194
|
-
throw new Error('Task cancelled by user.');
|
|
1195
|
-
}
|
|
1196
|
-
executedSteps = step;
|
|
1197
|
-
onProgress({ step, phase: 'thinking', action: 'thinking' });
|
|
1198
|
-
const decision = await getAgentDecision(client, observation, { onProgress, step });
|
|
1199
|
-
if (options.signal && options.signal.aborted) {
|
|
1200
|
-
throw new Error('Task cancelled by user.');
|
|
1201
|
-
}
|
|
1202
|
-
const action = decision.action;
|
|
1203
|
-
const input = decision.input || {};
|
|
1204
|
-
try {
|
|
1205
|
-
toolRegistry.validateToolInput(action, input);
|
|
1206
|
-
} catch (e) {
|
|
1207
|
-
observation = [
|
|
1208
|
-
`Previous thought: ${decision.thought || '(none)'}`,
|
|
1209
|
-
`Action: ${action || '(none)'}`,
|
|
1210
|
-
'Observation:',
|
|
1211
|
-
`Error: ${e.message}`
|
|
1212
|
-
].join('\n');
|
|
1213
|
-
continue;
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
if (action === 'finish') {
|
|
1217
|
-
const missingPlanFiles = getMissingPlanFiles(editPlanState);
|
|
1218
|
-
if (missingPlanFiles.length > 0) {
|
|
1219
|
-
observation = [
|
|
1220
|
-
`Previous thought: ${decision.thought || '(none)'}`,
|
|
1221
|
-
'Action: finish',
|
|
1222
|
-
'Observation:',
|
|
1223
|
-
[
|
|
1224
|
-
'Error: Approved plan is not complete yet.',
|
|
1225
|
-
`Missing planned file edits: ${missingPlanFiles.join(', ')}`,
|
|
1226
|
-
'Complete every file listed in the approved plan before finishing, or create a new plan if the scope changed.'
|
|
1227
|
-
].join('\n')
|
|
1228
|
-
].join('\n');
|
|
1229
|
-
continue;
|
|
1230
|
-
}
|
|
1231
|
-
finalSessionSummary = input.sessionSummary || input.summary || task;
|
|
1232
|
-
finalSummary = input.summary || 'Task complete.';
|
|
1233
|
-
finalVerification = input.verification || 'Not specified.';
|
|
1234
|
-
writeWorkspaceSession(workspaceRoot, {
|
|
1235
|
-
summary: finalSessionSummary,
|
|
1236
|
-
lastTask: task,
|
|
1237
|
-
lastVerification: finalVerification
|
|
1238
|
-
});
|
|
1239
|
-
break;
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
let toolResult = '';
|
|
1243
|
-
try {
|
|
1244
|
-
if (readOnlyTask && isWriteLikeAction(action)) {
|
|
1245
|
-
observation = [
|
|
1246
|
-
`Previous thought: ${decision.thought || '(none)'}`,
|
|
1247
|
-
`Action: ${action}`,
|
|
1248
|
-
'Observation:',
|
|
1249
|
-
[
|
|
1250
|
-
'Error: The latest user request is read-only and explicitly forbids edits or changes.',
|
|
1251
|
-
'Do not create a plan or request approval for edits.',
|
|
1252
|
-
'Use read_file/search_code/find_path as needed, then finish with an analysis summary.'
|
|
1253
|
-
].join('\n')
|
|
1254
|
-
].join('\n');
|
|
1255
|
-
continue;
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
if (requiresMultiFilePlan(action, input, editPlanState)) {
|
|
1259
|
-
const nextPath = getEditTargetPath(action, input);
|
|
1260
|
-
observation = [
|
|
1261
|
-
`Previous thought: ${decision.thought || '(none)'}`,
|
|
1262
|
-
`Action: ${action}`,
|
|
1263
|
-
'Observation:',
|
|
1264
|
-
[
|
|
1265
|
-
'Error: Multi-file edit plan required before editing another file.',
|
|
1266
|
-
'Use the "plan" action first with input.plan starting with "Plan:" bullets and input.files listing every file you expect to touch.',
|
|
1267
|
-
`Already edited: ${Array.from(editPlanState.touchedFiles).join(', ')}`,
|
|
1268
|
-
`Next requested file: ${nextPath}`
|
|
1269
|
-
].join('\n')
|
|
1270
|
-
].join('\n');
|
|
1271
|
-
continue;
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
if (action === 'apply_patch' || action === 'write_file') {
|
|
1275
|
-
const explanation = validateEditExplanation(action, input, decision.thought);
|
|
1276
|
-
if (!explanation.ok) {
|
|
1277
|
-
observation = [
|
|
1278
|
-
`Previous thought: ${decision.thought || '(none)'}`,
|
|
1279
|
-
`Action: ${action}`,
|
|
1280
|
-
'Observation:',
|
|
1281
|
-
`Error: ${explanation.message}`
|
|
1282
|
-
].join('\n');
|
|
1283
|
-
continue;
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
// Show progress only after the action passes local validation, so retry attempts do not spam near-duplicate notes.
|
|
1288
|
-
onProgress({
|
|
1289
|
-
step,
|
|
1290
|
-
phase: 'acting',
|
|
1291
|
-
action: 'thinking',
|
|
1292
|
-
thought: decision.thought
|
|
1293
|
-
});
|
|
1294
|
-
|
|
1295
|
-
switch (action) {
|
|
1296
|
-
case 'web_search':
|
|
1297
|
-
toolResult = await webSearch(input.query, onProgress);
|
|
1298
|
-
break;
|
|
1299
|
-
case 'list_files':
|
|
1300
|
-
toolResult = await listFiles(workspaceRoot, input.path || '.');
|
|
1301
|
-
break;
|
|
1302
|
-
case 'read_file':
|
|
1303
|
-
toolResult = readFileRange(workspaceRoot, input.path, input.startLine, input.endLine);
|
|
1304
|
-
break;
|
|
1305
|
-
case 'search_code':
|
|
1306
|
-
toolResult = await searchCode(workspaceRoot, input.query, input.path || '.');
|
|
1307
|
-
break;
|
|
1308
|
-
case 'find_path':
|
|
1309
|
-
toolResult = await findPaths(workspaceRoot, input.query, input.type);
|
|
1310
|
-
if (input.openAfter === true) {
|
|
1311
|
-
const result = JSON.parse(toolResult);
|
|
1312
|
-
if (result.success && result.matches.length === 1) {
|
|
1313
|
-
await executeAction({ type: 'open_folder', target: result.matches[0].path });
|
|
1314
|
-
toolResult = `Found and opened: ${result.matches[0].path}`;
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
break;
|
|
1318
|
-
case 'run_shell': {
|
|
1319
|
-
const approved = await requestApproval({
|
|
1320
|
-
type: 'shell',
|
|
1321
|
-
label: input.command,
|
|
1322
|
-
preview: input.command
|
|
1323
|
-
});
|
|
1324
|
-
if (!approved) {
|
|
1325
|
-
toolResult = `User denied shell command: ${input.command}`;
|
|
1326
|
-
break;
|
|
1327
|
-
}
|
|
1328
|
-
safetyManager.appendActionLog({
|
|
1329
|
-
source: 'code_agent',
|
|
1330
|
-
action: 'run_shell',
|
|
1331
|
-
command: input.command,
|
|
1332
|
-
approved
|
|
1333
|
-
});
|
|
1334
|
-
toolResult = await runShell(workspaceRoot, input.command);
|
|
1335
|
-
break;
|
|
1336
|
-
}
|
|
1337
|
-
case 'verify': {
|
|
1338
|
-
verificationAttempts += 1;
|
|
1339
|
-
const result = await runVerificationCommands(workspaceRoot, input.commands, {
|
|
1340
|
-
requestApproval,
|
|
1341
|
-
budget: verificationBudget,
|
|
1342
|
-
attempt: verificationAttempts
|
|
1343
|
-
});
|
|
1344
|
-
toolResult = result.output;
|
|
1345
|
-
if (options.taskId) {
|
|
1346
|
-
taskManager.addCheckpoint(options.taskId, {
|
|
1347
|
-
phase: 'verification',
|
|
1348
|
-
attempt: verificationAttempts,
|
|
1349
|
-
passed: result.passed,
|
|
1350
|
-
output: truncate(result.output, 4000)
|
|
1351
|
-
});
|
|
1352
|
-
}
|
|
1353
|
-
if (!result.passed && verificationAttempts >= verificationBudget) {
|
|
1354
|
-
toolResult += '\nVerification budget exhausted. Finish with the remaining failure clearly explained.';
|
|
1355
|
-
}
|
|
1356
|
-
break;
|
|
1357
|
-
}
|
|
1358
|
-
case 'plan': {
|
|
1359
|
-
const createdAt = new Date().toISOString();
|
|
1360
|
-
let planFile = writePlanFile(workspaceRoot, input, { task, createdAt });
|
|
1361
|
-
const approved = await requestApproval({
|
|
1362
|
-
type: 'plan',
|
|
1363
|
-
label: PLAN_FILE_LABEL,
|
|
1364
|
-
preview: planFile.content,
|
|
1365
|
-
summary: formatPlanApprovalSummary(input),
|
|
1366
|
-
openPath: planFile.path
|
|
1367
|
-
});
|
|
1368
|
-
if (!approved) {
|
|
1369
|
-
planFile = updatePlanApprovalStatus(planFile, input, {
|
|
1370
|
-
task,
|
|
1371
|
-
createdAt,
|
|
1372
|
-
approvalStatus: 'Denied',
|
|
1373
|
-
approvalTime: new Date().toISOString()
|
|
1374
|
-
});
|
|
1375
|
-
toolResult = 'User denied multi-file plan.';
|
|
1376
|
-
break;
|
|
1377
|
-
}
|
|
1378
|
-
planFile = updatePlanApprovalStatus(planFile, input, {
|
|
1379
|
-
task,
|
|
1380
|
-
createdAt,
|
|
1381
|
-
approvalStatus: 'Approved',
|
|
1382
|
-
approvalTime: new Date().toISOString()
|
|
1383
|
-
});
|
|
1384
|
-
editPlanState.approved = true;
|
|
1385
|
-
editPlanState.expectedFiles = new Set(
|
|
1386
|
-
Array.isArray(input.files)
|
|
1387
|
-
? input.files.map(file => String(file || '').trim()).filter(Boolean)
|
|
1388
|
-
: []
|
|
1389
|
-
);
|
|
1390
|
-
safetyManager.appendActionLog({
|
|
1391
|
-
source: 'code_agent',
|
|
1392
|
-
action: 'plan',
|
|
1393
|
-
path: planFile.path,
|
|
1394
|
-
preview: planFile.content,
|
|
1395
|
-
approved
|
|
1396
|
-
});
|
|
1397
|
-
toolResult = `User approved multi-file plan at ${PLAN_FILE_LABEL}:\n${planFile.content}`;
|
|
1398
|
-
break;
|
|
1399
|
-
}
|
|
1400
|
-
case 'apply_patch': {
|
|
1401
|
-
const patchInput = input.patch || {};
|
|
1402
|
-
let patchWarnings = [];
|
|
1403
|
-
try {
|
|
1404
|
-
patchWarnings = buildApprovalWarnings(
|
|
1405
|
-
patchInput.path,
|
|
1406
|
-
buildPatchedContent(workspaceRoot, patchInput).nextContent
|
|
1407
|
-
);
|
|
1408
|
-
} catch (_) {
|
|
1409
|
-
patchWarnings = buildApprovalWarnings(patchInput.path, '');
|
|
1410
|
-
}
|
|
1411
|
-
const approved = await requestApproval({
|
|
1412
|
-
type: 'patch',
|
|
1413
|
-
label: patchInput.path,
|
|
1414
|
-
preview: formatPatchPreview(workspaceRoot, patchInput),
|
|
1415
|
-
warnings: patchWarnings
|
|
1416
|
-
});
|
|
1417
|
-
if (!approved) {
|
|
1418
|
-
toolResult = `User denied patch for ${patchInput.path}`;
|
|
1419
|
-
break;
|
|
1420
|
-
}
|
|
1421
|
-
safetyManager.appendActionLog({
|
|
1422
|
-
source: 'code_agent',
|
|
1423
|
-
action: 'apply_patch',
|
|
1424
|
-
path: patchInput.path,
|
|
1425
|
-
approved
|
|
1426
|
-
});
|
|
1427
|
-
toolResult = applyPatch(workspaceRoot, patchInput);
|
|
1428
|
-
editPlanState.touchedFiles.add(patchInput.path);
|
|
1429
|
-
break;
|
|
1430
|
-
}
|
|
1431
|
-
case 'write_file': {
|
|
1432
|
-
const approved = await requestApproval({
|
|
1433
|
-
type: 'write_file',
|
|
1434
|
-
label: input.path,
|
|
1435
|
-
preview: formatWritePreview(workspaceRoot, input.path, input.content),
|
|
1436
|
-
warnings: buildApprovalWarnings(input.path, input.content)
|
|
1437
|
-
});
|
|
1438
|
-
if (!approved) {
|
|
1439
|
-
toolResult = `User denied full file write for ${input.path}`;
|
|
1440
|
-
break;
|
|
1441
|
-
}
|
|
1442
|
-
safetyManager.appendActionLog({
|
|
1443
|
-
source: 'code_agent',
|
|
1444
|
-
action: 'write_file',
|
|
1445
|
-
path: input.path,
|
|
1446
|
-
approved
|
|
1447
|
-
});
|
|
1448
|
-
toolResult = writeFile(workspaceRoot, input.path, input.content);
|
|
1449
|
-
editPlanState.touchedFiles.add(input.path);
|
|
1450
|
-
break;
|
|
1451
|
-
}
|
|
1452
|
-
case 'ask_user': {
|
|
1453
|
-
const answer = await askUser(input.question);
|
|
1454
|
-
toolResult = `User answered: ${answer}`;
|
|
1455
|
-
break;
|
|
1456
|
-
}
|
|
1457
|
-
case 'open_url':
|
|
1458
|
-
case 'search':
|
|
1459
|
-
case 'open_app':
|
|
1460
|
-
case 'web_automation':
|
|
1461
|
-
case 'open_file':
|
|
1462
|
-
case 'open_folder':
|
|
1463
|
-
case 'create_folder':
|
|
1464
|
-
case 'delete_file':
|
|
1465
|
-
case 'clipboard_write':
|
|
1466
|
-
case 'learn_file':
|
|
1467
|
-
case 'learn_folder':
|
|
1468
|
-
case 'system_info':
|
|
1469
|
-
case 'plugin':
|
|
1470
|
-
case 'mcp_tool':
|
|
1471
|
-
case 'mouse_move':
|
|
1472
|
-
case 'mouse_click':
|
|
1473
|
-
case 'type_text':
|
|
1474
|
-
case 'key_tap':
|
|
1475
|
-
case 'system_automation': {
|
|
1476
|
-
const executorAction = normalizeExecutorAction(action, input);
|
|
1477
|
-
const safety = safetyManager.classifyAction(executorAction);
|
|
1478
|
-
let allowDangerous = false;
|
|
1479
|
-
let allowApproval = false;
|
|
1480
|
-
if (safety.tier === safetyManager.TIERS.APPROVAL || safety.tier === safetyManager.TIERS.DANGEROUS) {
|
|
1481
|
-
const approved = await requestApproval({
|
|
1482
|
-
type: action,
|
|
1483
|
-
label: formatActionPreview(action, input),
|
|
1484
|
-
preview: `${action}: ${formatActionPreview(action, input)}\nSafety: ${safety.tier} (${safety.reason})`
|
|
1485
|
-
});
|
|
1486
|
-
if (!approved) {
|
|
1487
|
-
toolResult = `User denied ${action}: ${formatActionPreview(action, input)}`;
|
|
1488
|
-
break;
|
|
1489
|
-
}
|
|
1490
|
-
allowApproval = safety.tier === safetyManager.TIERS.APPROVAL;
|
|
1491
|
-
allowDangerous = safety.tier === safetyManager.TIERS.DANGEROUS;
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
toolResult = await executeAction(executorAction, {
|
|
1495
|
-
source: 'code_agent',
|
|
1496
|
-
allowApproval,
|
|
1497
|
-
allowDangerous
|
|
1498
|
-
});
|
|
1499
|
-
break;
|
|
1500
|
-
} default:
|
|
1501
|
-
throw new Error(`Unsupported action: ${action}`);
|
|
1502
|
-
} } catch (e) {
|
|
1503
|
-
toolResult = `Error: ${e.message}`;
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
const evaluation = evaluateActionResult(action, toolResult);
|
|
1507
|
-
const toolStatus = getToolCallStatus(action, toolResult, evaluation);
|
|
1508
|
-
if (evaluation) {
|
|
1509
|
-
onProgress({
|
|
1510
|
-
step,
|
|
1511
|
-
phase: 'evaluating',
|
|
1512
|
-
action: 'evaluator',
|
|
1513
|
-
message: `${evaluation.status}: ${evaluation.message}`
|
|
1514
|
-
});
|
|
1515
|
-
toolResult = [
|
|
1516
|
-
toolResult,
|
|
1517
|
-
'',
|
|
1518
|
-
'Evaluation:',
|
|
1519
|
-
`${evaluation.status}: ${evaluation.message}`
|
|
1520
|
-
].join('\n');
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
onProgress({
|
|
1524
|
-
step,
|
|
1525
|
-
phase: 'tool_call',
|
|
1526
|
-
action,
|
|
1527
|
-
status: toolStatus,
|
|
1528
|
-
target: summarizeToolTarget(action, input)
|
|
1529
|
-
});
|
|
1530
|
-
|
|
1531
|
-
// Log the finished step with result
|
|
1532
|
-
let resultSummary = '';
|
|
1533
|
-
if (action === 'search_code') {
|
|
1534
|
-
const matches = (toolResult.match(/\n/g) || []).length;
|
|
1535
|
-
resultSummary = ` -> Found ${matches} matches`;
|
|
1536
|
-
} else if (action === 'run_shell') {
|
|
1537
|
-
resultSummary = ` -> Exit code 0`; // Simplified
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
onProgress({
|
|
1541
|
-
step,
|
|
1542
|
-
phase: 'finished',
|
|
1543
|
-
action,
|
|
1544
|
-
target: summarizeToolTarget(action, input) + resultSummary
|
|
1545
|
-
});
|
|
1546
|
-
|
|
1547
|
-
// Format tool result to be more readable and structured for the agent
|
|
1548
|
-
let formattedToolResult = toolResult;
|
|
1549
|
-
if (action === 'list_files' || action === 'find_path') {
|
|
1550
|
-
formattedToolResult = `Result of ${action}:\n---\n${toolResult}\n---`;
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
observation = [
|
|
1554
|
-
`Previous thought: ${decision.thought || '(none)'}`,
|
|
1555
|
-
`Action: ${action}`,
|
|
1556
|
-
'Observation:',
|
|
1557
|
-
formattedToolResult
|
|
1558
|
-
].join('\n'); }
|
|
1559
|
-
|
|
1560
|
-
// Check for Agent Collaboration (Review) - Disabled by default to save tokens
|
|
1561
|
-
if (config.enableAgentCollaboration === true && !readOnlyTask && executedSteps > 8 && finalSummary) {
|
|
1562
|
-
const availableProviders = getAvailableProviders(config);
|
|
1563
|
-
// Exclude providers that often need special local setup or are slow/unreliable for tiny reviews
|
|
1564
|
-
const altProviders = availableProviders.filter(p => p !== provider && p !== 'ollama' && p !== 'huggingface' && p !== 'local_openai');
|
|
1565
|
-
|
|
1566
|
-
// Fallback to provider itself if no other good ones exist, or pick the best available
|
|
1567
|
-
const reviewerProvider = altProviders.length > 0
|
|
1568
|
-
? altProviders[0]
|
|
1569
|
-
: (availableProviders.includes('gemini') ? 'gemini' : availableProviders[0]);
|
|
1570
|
-
|
|
1571
|
-
if (reviewerProvider && finalSummary) {
|
|
1572
|
-
onProgress({ phase: 'reviewing', action: 'reviewer_start', message: `Invoking Reviewer Agent (${reviewerProvider})...` });
|
|
1573
|
-
|
|
1574
|
-
const reviewerClient = new providerAdapter.AgentProviderClient({
|
|
1575
|
-
provider: reviewerProvider,
|
|
1576
|
-
config,
|
|
1577
|
-
providerOrder: [reviewerProvider],
|
|
1578
|
-
systemInstruction: CODE_AGENT_PROMPT,
|
|
1579
|
-
responseMimeType: 'application/json',
|
|
1580
|
-
maxTokens: 4096
|
|
1581
|
-
});
|
|
1582
|
-
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.";
|
|
1583
|
-
|
|
1584
|
-
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'.`;
|
|
1585
|
-
|
|
1586
|
-
try {
|
|
1587
|
-
const reviewResponse = await reviewerClient.sendMessage(reviewPrompt);
|
|
1588
|
-
const reviewDecision = extractJson(reviewResponse);
|
|
1589
|
-
const reviewInput = reviewDecision.input || {};
|
|
1590
|
-
|
|
1591
|
-
finalSummary += `\n\n[Review by ${reviewerProvider}]\n${reviewInput.summary || reviewDecision.thought || 'Looks good.'}`;
|
|
1592
|
-
} catch (e) {
|
|
1593
|
-
onProgress({ phase: 'reviewing', action: 'reviewer_error', message: `Reviewer Agent failed: ${e.message}` });
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
if (finalSummary) {
|
|
1599
|
-
memoryStore.recordInteraction(task, finalSummary);
|
|
1600
|
-
const answeredProvider = client.lastSuccessfulProvider || client.provider || provider;
|
|
1601
|
-
const result = {
|
|
1602
|
-
summary: finalSummary,
|
|
1603
|
-
verification: finalVerification,
|
|
1604
|
-
steps: executedSteps,
|
|
1605
|
-
providerInfo: {
|
|
1606
|
-
provider: answeredProvider,
|
|
1607
|
-
model: getCodeProviderModel(answeredProvider, config),
|
|
1608
|
-
usage: client.getUsageSummary()
|
|
1609
|
-
}
|
|
1610
|
-
};
|
|
1611
|
-
if (onFinalSummary) {
|
|
1612
|
-
await onFinalSummary(result);
|
|
1613
|
-
}
|
|
1614
|
-
return result;
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
writeWorkspaceSession(workspaceRoot, {
|
|
1618
|
-
summary: `Task stopped before completion: ${task}`,
|
|
1619
|
-
lastTask: task,
|
|
1620
|
-
lastVerification: 'Agent limit reached before explicit completion.'
|
|
1621
|
-
});
|
|
1622
|
-
|
|
1623
|
-
const answeredProvider = client.lastSuccessfulProvider || client.provider || provider;
|
|
1624
|
-
return {
|
|
1625
|
-
summary: 'Stopped after reaching the maximum number of agent steps.',
|
|
1626
|
-
verification: 'Agent limit reached before explicit completion.',
|
|
1627
|
-
steps: executedSteps || MAX_AGENT_STEPS,
|
|
1628
|
-
providerInfo: {
|
|
1629
|
-
provider: answeredProvider,
|
|
1630
|
-
model: getCodeProviderModel(answeredProvider, config),
|
|
1631
|
-
usage: client.getUsageSummary()
|
|
1632
|
-
}
|
|
1633
|
-
};
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
module.exports = {
|
|
1637
|
-
executeCodeTask,
|
|
1638
|
-
_helpers: {
|
|
1639
|
-
extractJson,
|
|
1640
|
-
selectSupportedCodeProvider,
|
|
1641
|
-
getSupportedCodeProviderOrder,
|
|
1642
|
-
findPaths,
|
|
1643
|
-
listFiles,
|
|
1644
|
-
searchCode,
|
|
1645
|
-
runVerificationCommands,
|
|
1646
|
-
walkDirectory,
|
|
1647
|
-
buildUnifiedDiffPreview,
|
|
1648
|
-
buildFullFileDiffPreview,
|
|
1649
|
-
buildApprovalWarnings,
|
|
1650
|
-
validateEditExplanation,
|
|
1651
|
-
formatPatchPreview,
|
|
1652
|
-
formatWritePreview,
|
|
1653
|
-
formatPlanPreview,
|
|
1654
|
-
formatPlanApprovalSummary,
|
|
1655
|
-
formatPlanMarkdown,
|
|
1656
|
-
writePlanFile,
|
|
1657
|
-
updatePlanApprovalStatus,
|
|
1658
|
-
normalizePlanItems,
|
|
1659
|
-
normalizePlanItemLanguage,
|
|
1660
|
-
requiresMultiFilePlan,
|
|
1661
|
-
getMissingPlanFiles,
|
|
1662
|
-
isReadOnlyTask,
|
|
1663
|
-
isWriteLikeAction,
|
|
1664
|
-
getEditTargetPath,
|
|
1665
|
-
PLAN_FILE_PATH
|
|
1666
|
-
}
|
|
1667
|
-
};
|