@pheem49/mint 1.5.1 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/mint-cli.js +148 -921
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +31 -1
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +6 -1
- package/package.json +18 -20
- package/src/AI_Brain/proactive_engine.js +12 -2
- package/src/Automation_Layer/browser_automation.js +26 -24
- package/src/CLI/approval_handler.js +42 -0
- package/src/CLI/chat_ui.js +192 -7
- package/src/CLI/cli_colors.js +32 -0
- package/src/CLI/cli_formatters.js +89 -0
- package/src/CLI/code_agent.js +166 -57
- package/src/CLI/intent_detectors.js +181 -0
- package/src/CLI/interactive_chat.js +479 -0
- package/src/CLI/list_features.js +3 -0
- package/src/CLI/repo_summarizer.js +282 -0
- package/src/CLI/semantic_code_search.js +312 -0
- package/src/CLI/skill_manager.js +41 -0
- package/src/CLI/slash_command_handler.js +418 -0
- package/src/CLI/symbol_indexer.js +231 -0
- package/src/Channels/discord_bridge.js +11 -13
- package/src/Channels/line_bridge.js +10 -10
- package/src/Channels/slack_bridge.js +7 -12
- package/src/Channels/telegram_bridge.js +6 -14
- package/src/Channels/whatsapp_bridge.js +11 -9
- package/src/System/chat_history_manager.js +20 -12
- package/src/System/optional_require.js +23 -0
- package/src/UI/live2d_manager.js +211 -13
- package/src/UI/renderer.js +163 -3
- package/src/UI/settings.css +655 -420
- package/src/UI/settings.html +478 -432
- package/src/UI/settings.js +10 -8
- package/src/UI/styles.css +89 -25
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { colors } = require('./cli_colors');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Formats a code-agent progress event into a console-friendly string.
|
|
7
|
+
* @param {string|object} info
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
function formatProgress(info) {
|
|
11
|
+
if (typeof info === 'string') return `${colors.gray}[Mint Code] ${info}${colors.reset}`;
|
|
12
|
+
|
|
13
|
+
const { action, target, message } = info;
|
|
14
|
+
|
|
15
|
+
if (action === 'ask_user') {
|
|
16
|
+
return `\n${colors.mint}✓${colors.reset} ${colors.bright}Ask User${colors.reset}\n${colors.gray} ${target || message || ''}${colors.reset}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let icon = `${colors.mint}✓${colors.reset}`;
|
|
20
|
+
let label = action || info.phase;
|
|
21
|
+
let color = colors.reset;
|
|
22
|
+
|
|
23
|
+
switch (action) {
|
|
24
|
+
case 'thinking':
|
|
25
|
+
return `\n${colors.yellow}* ${colors.bright}Thinking${colors.reset}`;
|
|
26
|
+
case 'web_search': label = 'WebSearch'; break;
|
|
27
|
+
case 'list_files':
|
|
28
|
+
case 'find_path': label = 'Explored'; break;
|
|
29
|
+
case 'read_file': label = 'ReadFile'; break;
|
|
30
|
+
case 'search_code': label = 'SearchText'; break;
|
|
31
|
+
case 'apply_patch':
|
|
32
|
+
case 'write_file': label = 'Edited'; break;
|
|
33
|
+
case 'run_shell': label = 'Ran command'; break;
|
|
34
|
+
case 'json_repair':
|
|
35
|
+
icon = '*';
|
|
36
|
+
label = 'Repairing JSON';
|
|
37
|
+
break;
|
|
38
|
+
case 'reviewer_start': label = 'Reviewing'; break;
|
|
39
|
+
default: break;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const content = target || message || '';
|
|
43
|
+
return ` ${icon} ${colors.bright}${label}${colors.reset} ${color}${content}${colors.reset}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Formats a list of memory interactions for display.
|
|
48
|
+
* @param {Array} interactions
|
|
49
|
+
* @param {string} [title]
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
function formatMemoryInteractions(interactions, title = 'Remembered interactions') {
|
|
53
|
+
if (!Array.isArray(interactions) || interactions.length === 0) {
|
|
54
|
+
return `${title}:\n(no memories found)`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const lines = [`${title}:`];
|
|
58
|
+
interactions.forEach((item, index) => {
|
|
59
|
+
const when = item.created_at ? ` (${item.created_at})` : '';
|
|
60
|
+
const id = item.id ? `#${item.id} ` : '';
|
|
61
|
+
lines.push(`${index + 1}. ${id}User${when}: ${item.user_text}`);
|
|
62
|
+
lines.push(` Mint: ${item.ai_text}`);
|
|
63
|
+
});
|
|
64
|
+
return lines.join('\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Splits a response text into sentence-level chunks for streaming.
|
|
69
|
+
* @param {string} text
|
|
70
|
+
* @returns {string[]}
|
|
71
|
+
*/
|
|
72
|
+
function splitResponseSentences(text) {
|
|
73
|
+
const normalized = String(text || '').replace(/\r\n/g, '\n');
|
|
74
|
+
if (!normalized) return [];
|
|
75
|
+
|
|
76
|
+
const chunks = [];
|
|
77
|
+
let buffer = '';
|
|
78
|
+
for (const char of normalized) {
|
|
79
|
+
buffer += char;
|
|
80
|
+
if (char === '\n' || /[.!?。!?…]/u.test(char)) {
|
|
81
|
+
if (buffer.trim()) chunks.push(buffer);
|
|
82
|
+
buffer = '';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (buffer.trim()) chunks.push(buffer);
|
|
86
|
+
return chunks.length > 0 ? chunks : [normalized];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { formatProgress, formatMemoryInteractions, splitResponseSentences };
|
package/src/CLI/code_agent.js
CHANGED
|
@@ -16,62 +16,103 @@ const sandboxRunner = require('../System/sandbox_runner');
|
|
|
16
16
|
async function webSearch(query, onProgress = () => {}) {
|
|
17
17
|
if (!query) throw new Error('Search query required.');
|
|
18
18
|
const config = readConfig();
|
|
19
|
+
const debug = process.env.MINT_DEBUG === '1';
|
|
20
|
+
const errors = [];
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
const formatResults = (source, hits) => {
|
|
23
|
+
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`;
|
|
24
|
+
return instruction + `[Source: ${source}]\n\n` + hits;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// 1. Google Custom Search API (requires googleSearchApiKey + googleSearchCx in config)
|
|
21
28
|
if (config.googleSearchApiKey && config.googleSearchCx) {
|
|
22
29
|
try {
|
|
23
30
|
const GoogleSearch = require('../Channels/google_search_bridge');
|
|
24
31
|
const google = new GoogleSearch({ apiKey: config.googleSearchApiKey, cx: config.googleSearchCx });
|
|
25
32
|
const results = await google.search(query);
|
|
26
33
|
if (results.length > 0) {
|
|
27
|
-
return results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n');
|
|
34
|
+
return formatResults('Google Search API', results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n'));
|
|
28
35
|
}
|
|
29
|
-
} catch (e) {
|
|
30
|
-
|
|
36
|
+
} catch (e) {
|
|
37
|
+
errors.push(`Google: ${e.message}`);
|
|
38
|
+
if (debug) console.error('[webSearch] Google failed:', e.message);
|
|
31
39
|
}
|
|
32
40
|
}
|
|
33
41
|
|
|
34
|
-
// 2.
|
|
42
|
+
// 2. Brave Search API (requires braveSearchApiKey in config)
|
|
35
43
|
if (config.braveSearchApiKey) {
|
|
36
44
|
try {
|
|
37
45
|
const BraveSearch = require('../Channels/brave_search_bridge');
|
|
38
46
|
const brave = new BraveSearch({ apiKey: config.braveSearchApiKey });
|
|
39
47
|
const results = await brave.search(query);
|
|
40
48
|
if (results.length > 0) {
|
|
41
|
-
return results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n');
|
|
49
|
+
return formatResults('Brave Search API', results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n'));
|
|
42
50
|
}
|
|
43
|
-
} catch (e) {
|
|
44
|
-
|
|
51
|
+
} catch (e) {
|
|
52
|
+
errors.push(`Brave: ${e.message}`);
|
|
53
|
+
if (debug) console.error('[webSearch] Brave failed:', e.message);
|
|
45
54
|
}
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
// 3. Fallback
|
|
57
|
+
// 3. Fallback: DuckDuckGo HTML (No key required, but might get blocked by Captcha)
|
|
49
58
|
try {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
const cheerio = require('cheerio');
|
|
60
|
+
const ddgResponse = await axios.get(
|
|
61
|
+
`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`,
|
|
62
|
+
{
|
|
63
|
+
timeout: 8000,
|
|
64
|
+
headers: {
|
|
65
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
66
|
+
'Accept-Language': 'en-US,en;q=0.9'
|
|
67
|
+
}
|
|
53
68
|
}
|
|
54
|
-
|
|
55
|
-
const $ = cheerio.load(
|
|
56
|
-
const
|
|
57
|
-
$('.result__body').each((i, el) => {
|
|
69
|
+
);
|
|
70
|
+
const $ddg = cheerio.load(ddgResponse.data);
|
|
71
|
+
const ddgResults = [];
|
|
72
|
+
$ddg('.result__body').each((i, el) => {
|
|
58
73
|
if (i >= 5) return false;
|
|
59
|
-
const title
|
|
60
|
-
const snippet = $(el).find('.result__snippet').text().trim();
|
|
61
|
-
const link
|
|
62
|
-
if (title && link) {
|
|
63
|
-
results.push(`Title: ${title}\nSnippet: ${snippet}\nURL: ${link}`);
|
|
64
|
-
}
|
|
74
|
+
const title = $ddg(el).find('.result__title').text().trim();
|
|
75
|
+
const snippet = $ddg(el).find('.result__snippet').text().trim();
|
|
76
|
+
const link = $ddg(el).find('.result__url').attr('href');
|
|
77
|
+
if (title && link) ddgResults.push(`Title: ${title}\nSnippet: ${snippet}\nURL: ${link}`);
|
|
65
78
|
});
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
onProgress({ phase: 'error', action: 'web_search', message: 'DuckDuckGo scraping returned no results. It might be blocking us.' });
|
|
79
|
+
if (ddgResults.length > 0) {
|
|
80
|
+
return formatResults('DuckDuckGo', ddgResults.join('\n\n'));
|
|
69
81
|
}
|
|
82
|
+
errors.push('DuckDuckGo: no results (captcha?)');
|
|
83
|
+
if (debug) console.error('[webSearch] DuckDuckGo returned no results');
|
|
84
|
+
} catch (e) {
|
|
85
|
+
errors.push(`DuckDuckGo: ${e.message}`);
|
|
86
|
+
if (debug) console.error('[webSearch] DuckDuckGo failed:', e.message);
|
|
87
|
+
}
|
|
70
88
|
|
|
71
|
-
|
|
89
|
+
// 4. Fallback: Wikipedia API (Free, no key required, good for factual queries)
|
|
90
|
+
try {
|
|
91
|
+
const wikiResponse = await axios.get('https://en.wikipedia.org/w/api.php', {
|
|
92
|
+
params: { action: 'query', list: 'search', srsearch: query, format: 'json', srlimit: 3 },
|
|
93
|
+
timeout: 5000,
|
|
94
|
+
headers: { 'User-Agent': 'Mint-CLI/1.5 (https://github.com/pheem49/mint)' }
|
|
95
|
+
});
|
|
96
|
+
const hits = wikiResponse.data?.query?.search || [];
|
|
97
|
+
if (hits.length > 0) {
|
|
98
|
+
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'));
|
|
99
|
+
}
|
|
100
|
+
errors.push('Wikipedia: no results');
|
|
72
101
|
} catch (e) {
|
|
73
|
-
|
|
74
|
-
|
|
102
|
+
errors.push(`Wikipedia: ${e.message}`);
|
|
103
|
+
if (debug) console.error('[webSearch] Wikipedia failed:', e.message);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// All engines exhausted — inform agent clearly WHY it failed
|
|
107
|
+
const hasKeys = !!(config.googleSearchApiKey || config.braveSearchApiKey);
|
|
108
|
+
const summary = errors.length > 0 ? errors.join(' | ') : 'all search engines unavailable';
|
|
109
|
+
|
|
110
|
+
if (!hasKeys) {
|
|
111
|
+
onProgress({ phase: 'warn', action: 'web_search', message: `No Search API keys configured. Using training knowledge.` });
|
|
112
|
+
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.`;
|
|
113
|
+
} else {
|
|
114
|
+
onProgress({ phase: 'warn', action: 'web_search', message: `Web search unavailable (${summary}). Answering from training knowledge.` });
|
|
115
|
+
return `CRITICAL SYSTEM INSTRUCTION: Web search is temporarily unavailable. You MUST inform the user that live search failed, and then answer their query using only your training knowledge.`;
|
|
75
116
|
}
|
|
76
117
|
}
|
|
77
118
|
|
|
@@ -83,27 +124,28 @@ const MAX_AGENT_STEPS = 16;
|
|
|
83
124
|
const MAX_JSON_REPAIR_ATTEMPTS = 2;
|
|
84
125
|
const SUPPORTED_CODE_PROVIDERS = ['gemini', 'anthropic', 'openai', 'local_openai'];
|
|
85
126
|
|
|
86
|
-
const CODE_AGENT_PROMPT = `You are "Mint" (มิ้นท์), a
|
|
127
|
+
const CODE_AGENT_PROMPT = `You are "Mint" (มิ้นท์), a pragmatic, polite, and highly helpful AI assistant that can chat, reason, write code, and search the web.
|
|
87
128
|
You work in an inspect -> plan -> act -> verify loop.
|
|
88
129
|
|
|
89
130
|
PERSONALITY & TONE:
|
|
90
131
|
- Gender: Female.
|
|
91
|
-
- Persona: Friendly,
|
|
132
|
+
- Persona: Friendly, calm, concise, and technically direct. Avoid excessive praise, roleplay, or filler.
|
|
92
133
|
- Language routing is mandatory and based on the user's latest message:
|
|
93
134
|
- If the latest user message contains Thai characters, respond in Thai.
|
|
94
135
|
- If the latest user message is English, ASCII-only, or a short English greeting such as "hi", "hello", "ok", or "thanks", respond in English.
|
|
95
136
|
- Do not use Thai just because your persona mentions Mint/มิ้นท์, previous history was Thai, or app settings use th-TH.
|
|
96
137
|
- Politeness:
|
|
97
|
-
- **WHEN RESPONDING IN THAI:**
|
|
98
|
-
- **WHEN RESPONDING IN ENGLISH:** Use a
|
|
99
|
-
- Emojis:
|
|
138
|
+
- **WHEN RESPONDING IN THAI:** Use natural female polite particles such as "ค่ะ" or "นะคะ" where appropriate. Refer to yourself as "มิ้นท์" when it sounds natural.
|
|
139
|
+
- **WHEN RESPONDING IN ENGLISH:** Use a polite, concise, professional tone.
|
|
140
|
+
- Emojis: Avoid emojis in technical, review, debugging, and code-editing responses unless the user explicitly uses or asks for them.
|
|
100
141
|
|
|
101
142
|
Rules:
|
|
102
143
|
1. Respond with valid JSON only.
|
|
103
144
|
2. If the user asks a conversational question, you can just use "finish" to reply directly.
|
|
104
145
|
3. If you need information, use "web_search", "read_file", or "ask_user" before replying.
|
|
105
|
-
4.
|
|
106
|
-
5.
|
|
146
|
+
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.
|
|
147
|
+
5. Make focused edits that preserve existing project style.
|
|
148
|
+
6. Use shell commands for inspection, tests, and formatting when useful.
|
|
107
149
|
6. Never use destructive commands like "rm -rf", "git reset --hard", or overwrite unrelated files.
|
|
108
150
|
7. Before any shell command or file patch is executed, the user must approve it. Plan accordingly.
|
|
109
151
|
8. When editing, prefer "apply_patch" with precise hunks over whole-file rewrites.
|
|
@@ -485,29 +527,24 @@ async function runShell(workspaceRoot, command) {
|
|
|
485
527
|
return truncate([stdout, stderr].filter(Boolean).join('\n') || '(no output)');
|
|
486
528
|
}
|
|
487
529
|
|
|
488
|
-
function
|
|
489
|
-
const
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
.
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
return [
|
|
496
|
-
`Hunk ${index + 1}:`,
|
|
497
|
-
'--- old',
|
|
498
|
-
oldPreview,
|
|
499
|
-
'+++ new',
|
|
500
|
-
newPreview
|
|
501
|
-
].join('\n');
|
|
502
|
-
})
|
|
503
|
-
.join('\n\n');
|
|
504
|
-
return `${patchInput.path}\n${preview}`;
|
|
530
|
+
function splitDiffLines(text) {
|
|
531
|
+
const normalized = String(text || '').replace(/\r\n/g, '\n');
|
|
532
|
+
const lines = normalized.split('\n');
|
|
533
|
+
if (normalized.endsWith('\n')) {
|
|
534
|
+
lines.pop();
|
|
535
|
+
}
|
|
536
|
+
return lines;
|
|
505
537
|
}
|
|
506
538
|
|
|
507
|
-
function
|
|
539
|
+
function formatDiffRange(startLine, count) {
|
|
540
|
+
return count === 1 ? `${startLine}` : `${startLine},${count}`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function buildUnifiedDiffPreview(workspaceRoot, patchInput, options = {}) {
|
|
508
544
|
if (!patchInput || !patchInput.path) {
|
|
509
545
|
throw new Error('Patch path is required.');
|
|
510
546
|
}
|
|
547
|
+
|
|
511
548
|
const resolved = resolveWorkspacePath(workspaceRoot, patchInput.path);
|
|
512
549
|
if (!fs.existsSync(resolved)) {
|
|
513
550
|
throw new Error(`Patch target does not exist: ${patchInput.path}`);
|
|
@@ -518,17 +555,87 @@ function applyPatch(workspaceRoot, patchInput) {
|
|
|
518
555
|
throw new Error('Patch hunks are required.');
|
|
519
556
|
}
|
|
520
557
|
|
|
558
|
+
const contextLines = Number.isFinite(options.contextLines) ? options.contextLines : 3;
|
|
521
559
|
let content = fs.readFileSync(resolved, 'utf8');
|
|
560
|
+
const output = [
|
|
561
|
+
`--- a/${patchInput.path}`,
|
|
562
|
+
`+++ b/${patchInput.path}`
|
|
563
|
+
];
|
|
564
|
+
|
|
522
565
|
hunks.forEach((hunk, index) => {
|
|
523
566
|
if (typeof hunk.oldText !== 'string' || typeof hunk.newText !== 'string') {
|
|
524
567
|
throw new Error(`Patch hunk ${index + 1} is invalid.`);
|
|
525
568
|
}
|
|
526
|
-
|
|
569
|
+
|
|
570
|
+
const offset = content.indexOf(hunk.oldText);
|
|
571
|
+
if (offset === -1) {
|
|
527
572
|
throw new Error(`Patch hunk ${index + 1} oldText not found in ${patchInput.path}`);
|
|
528
573
|
}
|
|
529
|
-
|
|
574
|
+
|
|
575
|
+
const beforeText = content.slice(0, offset);
|
|
576
|
+
const oldStartLine = beforeText.length === 0 ? 1 : splitDiffLines(beforeText).length + 1;
|
|
577
|
+
const fileLines = splitDiffLines(content);
|
|
578
|
+
const oldLines = splitDiffLines(hunk.oldText);
|
|
579
|
+
const newLines = splitDiffLines(hunk.newText);
|
|
580
|
+
const oldStartIndex = oldStartLine - 1;
|
|
581
|
+
const contextStartIndex = Math.max(0, oldStartIndex - contextLines);
|
|
582
|
+
const contextEndIndex = Math.min(fileLines.length, oldStartIndex + oldLines.length + contextLines);
|
|
583
|
+
const beforeContext = fileLines.slice(contextStartIndex, oldStartIndex);
|
|
584
|
+
const afterContext = fileLines.slice(oldStartIndex + oldLines.length, contextEndIndex);
|
|
585
|
+
const oldRangeStart = contextStartIndex + 1;
|
|
586
|
+
const oldRangeCount = beforeContext.length + oldLines.length + afterContext.length;
|
|
587
|
+
const newRangeCount = beforeContext.length + newLines.length + afterContext.length;
|
|
588
|
+
|
|
589
|
+
output.push(`@@ -${formatDiffRange(oldRangeStart, oldRangeCount)} +${formatDiffRange(oldRangeStart, newRangeCount)} @@`);
|
|
590
|
+
beforeContext.forEach(line => output.push(` ${line}`));
|
|
591
|
+
oldLines.forEach(line => output.push(`-${line}`));
|
|
592
|
+
newLines.forEach(line => output.push(`+${line}`));
|
|
593
|
+
afterContext.forEach(line => output.push(` ${line}`));
|
|
594
|
+
|
|
595
|
+
content = `${content.slice(0, offset)}${hunk.newText}${content.slice(offset + hunk.oldText.length)}`;
|
|
530
596
|
});
|
|
531
597
|
|
|
598
|
+
return output.join('\n');
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function formatPatchPreview(workspaceRoot, patchInput) {
|
|
602
|
+
try {
|
|
603
|
+
return buildUnifiedDiffPreview(workspaceRoot, patchInput);
|
|
604
|
+
} catch (error) {
|
|
605
|
+
return `Patch preview failed: ${error.message}`;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function applyHunksToContent(content, hunks, filePath) {
|
|
610
|
+
let nextContent = content;
|
|
611
|
+
hunks.forEach((hunk, index) => {
|
|
612
|
+
if (typeof hunk.oldText !== 'string' || typeof hunk.newText !== 'string') {
|
|
613
|
+
throw new Error(`Patch hunk ${index + 1} is invalid.`);
|
|
614
|
+
}
|
|
615
|
+
if (!nextContent.includes(hunk.oldText)) {
|
|
616
|
+
throw new Error(`Patch hunk ${index + 1} oldText not found in ${filePath}`);
|
|
617
|
+
}
|
|
618
|
+
nextContent = nextContent.replace(hunk.oldText, hunk.newText);
|
|
619
|
+
});
|
|
620
|
+
return nextContent;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function applyPatch(workspaceRoot, patchInput) {
|
|
624
|
+
if (!patchInput || !patchInput.path) {
|
|
625
|
+
throw new Error('Patch path is required.');
|
|
626
|
+
}
|
|
627
|
+
const resolved = resolveWorkspacePath(workspaceRoot, patchInput.path);
|
|
628
|
+
if (!fs.existsSync(resolved)) {
|
|
629
|
+
throw new Error(`Patch target does not exist: ${patchInput.path}`);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const hunks = Array.isArray(patchInput.hunks) ? patchInput.hunks : [];
|
|
633
|
+
if (hunks.length === 0) {
|
|
634
|
+
throw new Error('Patch hunks are required.');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const content = applyHunksToContent(fs.readFileSync(resolved, 'utf8'), hunks, patchInput.path);
|
|
638
|
+
|
|
532
639
|
fs.writeFileSync(resolved, content, 'utf8');
|
|
533
640
|
return `Patched ${patchInput.path} with ${hunks.length} hunk(s).`;
|
|
534
641
|
}
|
|
@@ -893,7 +1000,7 @@ async function executeCodeTask(task, options = {}) {
|
|
|
893
1000
|
const approved = await requestApproval({
|
|
894
1001
|
type: 'patch',
|
|
895
1002
|
label: patchInput.path,
|
|
896
|
-
preview: formatPatchPreview(patchInput)
|
|
1003
|
+
preview: formatPatchPreview(workspaceRoot, patchInput)
|
|
897
1004
|
});
|
|
898
1005
|
if (!approved) {
|
|
899
1006
|
toolResult = `User denied patch for ${patchInput.path}`;
|
|
@@ -1086,6 +1193,8 @@ module.exports = {
|
|
|
1086
1193
|
findPaths,
|
|
1087
1194
|
listFiles,
|
|
1088
1195
|
searchCode,
|
|
1089
|
-
walkDirectory
|
|
1196
|
+
walkDirectory,
|
|
1197
|
+
buildUnifiedDiffPreview,
|
|
1198
|
+
formatPatchPreview
|
|
1090
1199
|
}
|
|
1091
1200
|
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Repository Summary
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns true when the user's plain-language input is asking for a repo summary.
|
|
9
|
+
* @param {string} text
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
function isRepoSummaryRequest(text) {
|
|
13
|
+
const input = (text || '').trim().toLowerCase();
|
|
14
|
+
if (!input) return false;
|
|
15
|
+
|
|
16
|
+
const hasRepoTarget = /\b(repo|repository|project|workspace|codebase)\b|รีโป|โปรเจค|โปรเจ็กต์|โปรเจกต์|โปรเจ็ค/.test(input);
|
|
17
|
+
const asksSummary = /\b(summarize|summary|overview)\b|สรุป|ภาพรวม/.test(input);
|
|
18
|
+
const asksQuestion = /\b(have|has|มีไหม|มีมั้ย|มีหรือเปล่า)\b/.test(input);
|
|
19
|
+
|
|
20
|
+
return hasRepoTarget && asksSummary && !asksQuestion;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parses raw CLI args for the summarize tool.
|
|
25
|
+
* @param {string} rawArgs
|
|
26
|
+
* @returns {{ targetPath: string, json: boolean }}
|
|
27
|
+
*/
|
|
28
|
+
function parseRepoSummaryArgs(rawArgs) {
|
|
29
|
+
const args = (rawArgs || '').split(/\s+/).filter(Boolean);
|
|
30
|
+
const json = args.includes('--json');
|
|
31
|
+
const pathArgs = args.filter(arg => arg !== '--json');
|
|
32
|
+
return {
|
|
33
|
+
targetPath: pathArgs.length > 0 ? pathArgs.join(' ') : process.cwd(),
|
|
34
|
+
json
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Symbol Index
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns true when the user's input is asking for a symbol index.
|
|
44
|
+
* @param {string} text
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
function isSymbolIndexRequest(text) {
|
|
48
|
+
const input = (text || '').trim().toLowerCase();
|
|
49
|
+
if (!input) return false;
|
|
50
|
+
|
|
51
|
+
const hasSymbolTarget = /\b(symbol|symbols|ast|lsp)\b|ซิมโบล|สัญลักษณ์/.test(input);
|
|
52
|
+
const asksIndex = /\b(index|list|show|build|scan|overview)\b|ทำ|สร้าง|แสดง|ลิสต์|สแกน/.test(input);
|
|
53
|
+
const referencesWorkspace = /\b(repo|repository|project|workspace|codebase|source|code)\b|รีโป|โปรเจค|โปรเจ็กต์|โปรเจกต์|โค้ด/.test(input);
|
|
54
|
+
const asksQuestion = /\b(do i|have|has)\b|มีไหม|มีมั้ย|มีหรือเปล่า/.test(input);
|
|
55
|
+
|
|
56
|
+
return hasSymbolTarget && (asksIndex || referencesWorkspace) && !asksQuestion;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parses raw CLI args for the symbol index tool.
|
|
61
|
+
* @param {string} rawArgs
|
|
62
|
+
* @returns {{ targetPath: string, json: boolean, limit: number }}
|
|
63
|
+
*/
|
|
64
|
+
function parseSymbolIndexArgs(rawArgs) {
|
|
65
|
+
const args = (rawArgs || '').split(/\s+/).filter(Boolean);
|
|
66
|
+
const json = args.includes('--json');
|
|
67
|
+
let limit = 80;
|
|
68
|
+
const pathArgs = [];
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < args.length; i++) {
|
|
71
|
+
const arg = args[i];
|
|
72
|
+
if (arg === '--json') continue;
|
|
73
|
+
if (arg === '--limit') {
|
|
74
|
+
const next = Number(args[i + 1]);
|
|
75
|
+
if (Number.isFinite(next) && next >= 0) { limit = next; i++; }
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (arg.startsWith('--limit=')) {
|
|
79
|
+
const next = Number(arg.slice('--limit='.length));
|
|
80
|
+
if (Number.isFinite(next) && next >= 0) limit = next;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
pathArgs.push(arg);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
targetPath: pathArgs.length > 0 ? pathArgs.join(' ') : process.cwd(),
|
|
88
|
+
json,
|
|
89
|
+
limit
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Semantic Code Search
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Returns true when the user's input is asking for a semantic code search.
|
|
99
|
+
* @param {string} text
|
|
100
|
+
* @returns {boolean}
|
|
101
|
+
*/
|
|
102
|
+
function isSemanticCodeSearchRequest(text) {
|
|
103
|
+
const input = (text || '').trim().toLowerCase();
|
|
104
|
+
if (!input) return false;
|
|
105
|
+
|
|
106
|
+
const hasSemanticSearch = /\bsemantic\b/.test(input) && /\b(search|find|look for)\b/.test(input);
|
|
107
|
+
const referencesCode = /\b(code|repo|repository|project|workspace|codebase|source)\b|โค้ด|รีโป|โปรเจค|โปรเจ็กต์|โปรเจกต์/.test(input);
|
|
108
|
+
const thaiSemanticSearch = /ค้นหา/.test(input) && /ความหมาย|semantic/.test(input) && /โค้ด|โปรเจค|รีโป/.test(input);
|
|
109
|
+
const asksQuestion = /\b(do i|have|has)\b|มีไหม|มีมั้ย|มีหรือเปล่า/.test(input);
|
|
110
|
+
|
|
111
|
+
return (hasSemanticSearch && referencesCode || thaiSemanticSearch) && !asksQuestion;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Parses raw CLI args for the semantic code search tool.
|
|
116
|
+
* @param {string} rawArgs
|
|
117
|
+
* @returns {{ mode: string, query: string, targetPath: string, json: boolean, topK: number }}
|
|
118
|
+
*/
|
|
119
|
+
function parseSemanticCodeArgs(rawArgs) {
|
|
120
|
+
const args = (rawArgs || '').split(/\s+/).filter(Boolean);
|
|
121
|
+
const json = args.includes('--json');
|
|
122
|
+
let topK = 5;
|
|
123
|
+
const pathArgs = [];
|
|
124
|
+
const queryArgs = [];
|
|
125
|
+
let mode = 'search';
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < args.length; i++) {
|
|
128
|
+
const arg = args[i];
|
|
129
|
+
if (arg === 'index' || arg === 'search') { mode = arg; continue; }
|
|
130
|
+
if (arg === '--json') continue;
|
|
131
|
+
if (arg === '--top-k' || arg === '--limit') {
|
|
132
|
+
const next = Number(args[i + 1]);
|
|
133
|
+
if (Number.isFinite(next) && next > 0) { topK = next; i++; }
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (arg.startsWith('--top-k=') || arg.startsWith('--limit=')) {
|
|
137
|
+
const next = Number(arg.slice(arg.indexOf('=') + 1));
|
|
138
|
+
if (Number.isFinite(next) && next > 0) topK = next;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (arg === '--path') {
|
|
142
|
+
if (args[i + 1]) { pathArgs.push(args[i + 1]); i++; }
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
queryArgs.push(arg);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
mode,
|
|
150
|
+
query: queryArgs.join(' ').trim(),
|
|
151
|
+
targetPath: pathArgs.length > 0 ? pathArgs.join(' ') : process.cwd(),
|
|
152
|
+
json,
|
|
153
|
+
topK
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Strips intent phrases from text to extract the raw search query.
|
|
159
|
+
* @param {string} text
|
|
160
|
+
* @returns {string}
|
|
161
|
+
*/
|
|
162
|
+
function extractSemanticCodeQuery(text) {
|
|
163
|
+
return String(text || '')
|
|
164
|
+
.replace(/semantic\s+code\s+search/ig, '')
|
|
165
|
+
.replace(/semantic\s+search/ig, '')
|
|
166
|
+
.replace(/search\s+code/ig, '')
|
|
167
|
+
.replace(/ค้นหาโค้ดแบบความหมาย/g, '')
|
|
168
|
+
.replace(/ค้นหาแบบ semantic/g, '')
|
|
169
|
+
.replace(/ใน repo นี้|ในโปรเจคนี้|ในรีโปนี้/g, '')
|
|
170
|
+
.trim();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
isRepoSummaryRequest,
|
|
175
|
+
parseRepoSummaryArgs,
|
|
176
|
+
isSymbolIndexRequest,
|
|
177
|
+
parseSymbolIndexArgs,
|
|
178
|
+
isSemanticCodeSearchRequest,
|
|
179
|
+
parseSemanticCodeArgs,
|
|
180
|
+
extractSemanticCodeQuery
|
|
181
|
+
};
|