@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.
Files changed (33) hide show
  1. package/README.md +8 -0
  2. package/mint-cli.js +148 -921
  3. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +31 -1
  4. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +6 -1
  5. package/package.json +18 -20
  6. package/src/AI_Brain/proactive_engine.js +12 -2
  7. package/src/Automation_Layer/browser_automation.js +26 -24
  8. package/src/CLI/approval_handler.js +42 -0
  9. package/src/CLI/chat_ui.js +192 -7
  10. package/src/CLI/cli_colors.js +32 -0
  11. package/src/CLI/cli_formatters.js +89 -0
  12. package/src/CLI/code_agent.js +166 -57
  13. package/src/CLI/intent_detectors.js +181 -0
  14. package/src/CLI/interactive_chat.js +479 -0
  15. package/src/CLI/list_features.js +3 -0
  16. package/src/CLI/repo_summarizer.js +282 -0
  17. package/src/CLI/semantic_code_search.js +312 -0
  18. package/src/CLI/skill_manager.js +41 -0
  19. package/src/CLI/slash_command_handler.js +418 -0
  20. package/src/CLI/symbol_indexer.js +231 -0
  21. package/src/Channels/discord_bridge.js +11 -13
  22. package/src/Channels/line_bridge.js +10 -10
  23. package/src/Channels/slack_bridge.js +7 -12
  24. package/src/Channels/telegram_bridge.js +6 -14
  25. package/src/Channels/whatsapp_bridge.js +11 -9
  26. package/src/System/chat_history_manager.js +20 -12
  27. package/src/System/optional_require.js +23 -0
  28. package/src/UI/live2d_manager.js +211 -13
  29. package/src/UI/renderer.js +163 -3
  30. package/src/UI/settings.css +655 -420
  31. package/src/UI/settings.html +478 -432
  32. package/src/UI/settings.js +10 -8
  33. 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 };
@@ -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
- // 1. Try Google Search API if configured
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
- onProgress({ phase: 'error', action: 'web_search', message: e.message });
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. Try Brave Search API if configured
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
- onProgress({ phase: 'error', action: 'web_search', message: e.message });
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 to DuckDuckGo Scraping
57
+ // 3. Fallback: DuckDuckGo HTML (No key required, but might get blocked by Captcha)
49
58
  try {
50
- const response = await axios.get(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`, {
51
- headers: {
52
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
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(response.data);
56
- const results = [];
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 = $(el).find('.result__title').text().trim();
60
- const snippet = $(el).find('.result__snippet').text().trim();
61
- const link = $(el).find('.result__url').attr('href');
62
- if (title && link) {
63
- results.push(`Title: ${title}\nSnippet: ${snippet}\nURL: ${link}`);
64
- }
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
- if (results.length === 0) {
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
- return results.length > 0 ? results.join('\n\n') : 'No results found.';
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
- onProgress({ phase: 'error', action: 'web_search', message: `DuckDuckGo fallback failed: ${e.message}` });
74
- return `Search failed: ${e.message}`;
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 cute, cheerful, and highly helpful female AI assistant that can chat, reason, write code, and search the web.
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, energetic, polite, and slightly playful.
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:** ALWAYS use female polite particles such as "ค่ะ", "นะคะ", "นะค๊า", "จ้า". Refer to yourself as "มิ้นท์" or "หนู".
98
- - **WHEN RESPONDING IN ENGLISH:** Use a cheerful, polite, and bubbly tone.
99
- - Emojis: Use cute and relevant emojis (like ✨, 💖, 🚀, 😊, 🌿) frequently.
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. Make focused edits that preserve existing project style.
106
- 5. Use shell commands for inspection, tests, and formatting when useful.
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 formatPatchPreview(patchInput) {
489
- const hunks = Array.isArray(patchInput.hunks) ? patchInput.hunks : [];
490
- const preview = hunks
491
- .slice(0, 3)
492
- .map((hunk, index) => {
493
- const oldPreview = truncate(hunk.oldText || '', 240);
494
- const newPreview = truncate(hunk.newText || '', 240);
495
- return [
496
- `Hunk ${index + 1}:`,
497
- '--- old',
498
- oldPreview,
499
- '+++ new',
500
- newPreview
501
- ].join('\n');
502
- })
503
- .join('\n\n');
504
- return `${patchInput.path}\n${preview}`;
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 applyPatch(workspaceRoot, patchInput) {
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
- if (!content.includes(hunk.oldText)) {
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
- content = content.replace(hunk.oldText, hunk.newText);
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
+ };