winter-super-cli 2026.6.23 → 2026.6.26
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 +19 -0
- package/bin/winter.js +23 -1
- package/extensions/vscode/src/extension.js +34 -7
- package/package.json +3 -2
- package/skill.md +4 -1
- package/src/agent/agent-definitions.js +4 -4
- package/src/ai/prompts/system-prompt.js +2 -0
- package/src/cli/commands.js +32 -1
- package/src/cli/config.js +4 -3
- package/src/cli/prompt-builder.js +32 -8
- package/src/cli/repl-commands.js +6 -0
- package/src/cli/repl.js +269 -13
- package/src/cli/slash-commands.js +2 -1
- package/src/mcp/client.js +8 -6
- package/src/mcp/ide-server.js +114 -2
- package/src/mcp/presets.js +114 -0
- package/src/rag/cli.js +195 -0
- package/src/rag/embeddings.js +183 -0
- package/src/rag/rag-engine.js +187 -0
- package/src/rag/retriever.js +112 -0
- package/src/rag/vector-store.js +225 -0
- package/src/tools/executor.js +103 -4
package/src/cli/repl.js
CHANGED
|
@@ -22,6 +22,8 @@ import { SessionManager } from '../session/manager.js';
|
|
|
22
22
|
import { AIProviderManager } from '../ai/providers.js';
|
|
23
23
|
import { ConfigLoader } from './config.js';
|
|
24
24
|
import { PermissionManager } from '../tools/permission.js';
|
|
25
|
+
import { MCPClient } from '../mcp/client.js';
|
|
26
|
+
import { getMcpPreset, upsertMcpServer } from '../mcp/presets.js';
|
|
25
27
|
import { compressConversation } from '../context/compress.js';
|
|
26
28
|
import { getToolUsageSummary } from '../tools/analytics.js';
|
|
27
29
|
import { SweAgent } from '../agent/swe-agent.js';
|
|
@@ -1123,6 +1125,19 @@ export class WinterREPL {
|
|
|
1123
1125
|
return;
|
|
1124
1126
|
}
|
|
1125
1127
|
|
|
1128
|
+
if (!input.startsWith('/')) {
|
|
1129
|
+
const browserShortcut = this.resolveBrowserShortcut(input);
|
|
1130
|
+
if (browserShortcut) {
|
|
1131
|
+
await this.handleOpenBrowserIntent(input, browserShortcut);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (!input.startsWith('/') && this.isOpenBrowserIntent(input)) {
|
|
1137
|
+
await this.handleOpenBrowserIntent(input);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1126
1141
|
// Parse @-symbols for non-command input
|
|
1127
1142
|
if (!input.startsWith('/')) {
|
|
1128
1143
|
const canUseHeavyContext = await this.shouldUseHeavyProjectContext();
|
|
@@ -1196,6 +1211,84 @@ export class WinterREPL {
|
|
|
1196
1211
|
}
|
|
1197
1212
|
}
|
|
1198
1213
|
|
|
1214
|
+
isOpenBrowserIntent(input = '') {
|
|
1215
|
+
const raw = String(input || '').trim();
|
|
1216
|
+
if (!raw) return false;
|
|
1217
|
+
const text = `${raw.toLowerCase()}\n${this.normalizeIntentText(raw).toLowerCase()}`;
|
|
1218
|
+
return /\b(mo|open|launch|start)\b.*\b(chrome|browser|trinh duyet|google chrome)\b/i.test(text)
|
|
1219
|
+
|| /\b(chrome|browser|trinh duyet|google chrome)\b.*\b(mo|open|launch|start)\b/i.test(text);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
resolveBrowserShortcut(input = '') {
|
|
1223
|
+
const raw = String(input || '').trim();
|
|
1224
|
+
if (!raw) return null;
|
|
1225
|
+
const normalized = this.normalizeIntentText(raw).toLowerCase();
|
|
1226
|
+
const text = `${raw.toLowerCase()}\n${normalized}`;
|
|
1227
|
+
|
|
1228
|
+
const url = this.extractUrlFromText(raw);
|
|
1229
|
+
if (url && /\b(mo|open|launch|start|browse)\b/i.test(normalized)) {
|
|
1230
|
+
return { url, label: url };
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const knownSites = [
|
|
1234
|
+
{ pattern: /\b(youtube music|yt music|music youtube)\b/i, url: 'https://music.youtube.com', label: 'YouTube Music' },
|
|
1235
|
+
{ pattern: /\b(youtube|you tube)\b/i, url: 'https://www.youtube.com', label: 'YouTube' },
|
|
1236
|
+
{ pattern: /\b(spotify)\b/i, url: 'https://open.spotify.com', label: 'Spotify' },
|
|
1237
|
+
{ pattern: /\b(google)\b/i, url: 'https://www.google.com', label: 'Google' },
|
|
1238
|
+
];
|
|
1239
|
+
|
|
1240
|
+
if (/\b(mo|open|launch|start)\b/i.test(normalized)) {
|
|
1241
|
+
const site = knownSites.find(item => item.pattern.test(text));
|
|
1242
|
+
if (site) return site;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (/\b(tim|search|kiem)\b/i.test(normalized) && /\b(chrome|google|browser|trinh duyet)\b/i.test(normalized)) {
|
|
1246
|
+
const query = this.extractBrowserSearchQuery(raw);
|
|
1247
|
+
if (query) {
|
|
1248
|
+
return {
|
|
1249
|
+
url: `https://www.google.com/search?${new URLSearchParams({ q: query }).toString()}`,
|
|
1250
|
+
label: `Google search: ${query}`,
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
extractBrowserSearchQuery(input = '') {
|
|
1259
|
+
let query = String(input || '').trim();
|
|
1260
|
+
query = query.replace(/^\s*(tìm|tim|search|kiếm|kiem)\s+/i, '');
|
|
1261
|
+
query = query.replace(/\s+(trên|tren|on)\s+(chrome|google|browser|trình duyệt|trinh duyet)\b.*$/i, '');
|
|
1262
|
+
query = query.replace(/\s+(đi|di)\s*$/i, '');
|
|
1263
|
+
query = query.trim();
|
|
1264
|
+
return query || null;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
extractUrlFromText(input = '') {
|
|
1268
|
+
const match = String(input || '').match(/https?:\/\/[^\s]+/i);
|
|
1269
|
+
return match ? match[0].replace(/[),.;]+$/g, '') : null;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
async handleOpenBrowserIntent(input = '', options = {}) {
|
|
1273
|
+
const url = options.url || this.extractUrlFromText(input) || 'about:blank';
|
|
1274
|
+
const result = await this.tools.execute('OpenBrowser', { browser: 'chrome', url }, { cwd: this.projectPath });
|
|
1275
|
+
await this.session?.addToHistory?.({ role: 'user', content: input });
|
|
1276
|
+
|
|
1277
|
+
if (result?.success === false) {
|
|
1278
|
+
const message = `Không mở được Chrome: ${result.error || 'unknown error'}`;
|
|
1279
|
+
console.log(`${colors.red}${message}${colors.reset}`);
|
|
1280
|
+
if (result.recovery) console.log(`${colors.dim}${result.recovery}${colors.reset}`);
|
|
1281
|
+
await this.session?.addToHistory?.({ role: 'assistant', content: message });
|
|
1282
|
+
return result;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const label = options.label || url;
|
|
1286
|
+
const message = `Đã mở Chrome${url && url !== 'about:blank' ? `: ${label}` : '.'}`;
|
|
1287
|
+
console.log(`${colors.green}${message}${colors.reset}`);
|
|
1288
|
+
await this.session?.addToHistory?.({ role: 'assistant', content: message });
|
|
1289
|
+
return result;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1199
1292
|
showSmartTip(input = '') {
|
|
1200
1293
|
const text = input.toLowerCase();
|
|
1201
1294
|
let tip = null;
|
|
@@ -1395,7 +1488,7 @@ export class WinterREPL {
|
|
|
1395
1488
|
|
|
1396
1489
|
CRITICAL DEBUG/AGENT RULES:
|
|
1397
1490
|
1. Inspect the project before changing anything. Read the failing file, related caller, config, and logs.
|
|
1398
|
-
2. Reproduce or locate the first hard failure. For frontend/runtime UI issues, use
|
|
1491
|
+
2. Reproduce or locate the first hard failure. For frontend/runtime UI issues, use chrome-devtools MCP in visible Chrome when a URL/dev server is available; use BrowserDebug only as a headless fallback.
|
|
1399
1492
|
3. Patch the smallest root cause with Write/Edit.
|
|
1400
1493
|
4. Run the closest verification command(s): ${verifyCommands.join(' && ')}.
|
|
1401
1494
|
5. If verification fails, read the new error, patch again, and run verification again.
|
|
@@ -1651,14 +1744,14 @@ ${colors.reset}
|
|
|
1651
1744
|
case 'review':
|
|
1652
1745
|
return byName(['Read', 'Grep', 'Glob', 'Bash', 'WebFetch']);
|
|
1653
1746
|
case 'debug':
|
|
1654
|
-
return byName(['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'BrowserDebug', 'WebFetch', 'Parallel']);
|
|
1747
|
+
return byName(['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'OpenBrowser', 'BrowserDebug', 'WebFetch', 'MCP', 'Parallel']);
|
|
1655
1748
|
case 'research':
|
|
1656
1749
|
return byName(['Read', 'Grep', 'Glob', 'WebFetch', 'WebSearch', 'Parallel']);
|
|
1657
1750
|
case 'design':
|
|
1658
1751
|
case 'ui':
|
|
1659
|
-
return byName(['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'BrowserDebug', 'WebFetch']);
|
|
1752
|
+
return byName(['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'OpenBrowser', 'BrowserDebug', 'WebFetch', 'MCP']);
|
|
1660
1753
|
default:
|
|
1661
|
-
return byName(['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'BrowserDebug', 'WebFetch', 'WebSearch', 'Parallel', 'Agent']);
|
|
1754
|
+
return byName(['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'OpenBrowser', 'BrowserDebug', 'WebFetch', 'WebSearch', 'MCP', 'Parallel', 'Agent']);
|
|
1662
1755
|
}
|
|
1663
1756
|
}
|
|
1664
1757
|
|
|
@@ -1761,8 +1854,18 @@ ${colors.reset}
|
|
|
1761
1854
|
return '';
|
|
1762
1855
|
}
|
|
1763
1856
|
|
|
1857
|
+
normalizeIntentText(text = '') {
|
|
1858
|
+
return String(text || '')
|
|
1859
|
+
.normalize('NFD')
|
|
1860
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
1861
|
+
.replace(/đ/g, 'd')
|
|
1862
|
+
.replace(/Đ/g, 'D')
|
|
1863
|
+
.toLowerCase();
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1764
1866
|
actionRequiresTools(messages = []) {
|
|
1765
|
-
const
|
|
1867
|
+
const rawText = this.getLatestUserText(messages).toLowerCase();
|
|
1868
|
+
const text = `${rawText}\n${this.normalizeIntentText(rawText)}`;
|
|
1766
1869
|
if (!text.trim()) return false;
|
|
1767
1870
|
|
|
1768
1871
|
// Direct action verbs (EN + VN)
|
|
@@ -1773,6 +1876,8 @@ ${colors.reset}
|
|
|
1773
1876
|
const pureQuestionPattern = /^(what|why|how|when|where|is|are|can|could|should|would|explain|describe|tell me|compare|giải thích|mô tả|so sánh|tai sao|vi sao|la gi|co nen|co phai|tại sao|vì sao|là gì|có nên|có phải|nhu the nao|như thế nào|khi nào)\b/i;
|
|
1774
1877
|
|
|
1775
1878
|
if (pureQuestionPattern.test(text) && !actionPattern.test(text)) return false;
|
|
1879
|
+
|
|
1880
|
+
if (this.isBrowserInteractionRequest(rawText)) return true;
|
|
1776
1881
|
|
|
1777
1882
|
// Even without explicit target, some verbs are strong enough on their own
|
|
1778
1883
|
const strongActionAlone = /\b(fix|debug|deploy|build|test|commit|install|run|refactor|sửa|chạy|cài|triển khai|xây dựng)\b/i;
|
|
@@ -1802,6 +1907,15 @@ ${colors.reset}
|
|
|
1802
1907
|
return true;
|
|
1803
1908
|
}
|
|
1804
1909
|
|
|
1910
|
+
isBrowserInteractionRequest(text = '') {
|
|
1911
|
+
const raw = String(text || '');
|
|
1912
|
+
const normalized = this.normalizeIntentText(raw);
|
|
1913
|
+
const combined = `${raw}\n${normalized}`;
|
|
1914
|
+
const action = /\b(click|fill|submit|press|select|navigate|bam|dien|chon|nhan|vao|bấm|điền|chọn|nhấn|vào)\b/i;
|
|
1915
|
+
const target = /\b(url|http|https|site|website|web|page|form|button|link|chrome|browser|trang|nut|nút|dang ky|dang nhap|khach hang|khách hàng|đăng ký|đăng nhập)\b/i;
|
|
1916
|
+
return action.test(combined) && target.test(combined);
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1805
1919
|
detectFakeCompletion(content = '') {
|
|
1806
1920
|
const text = String(content || '').toLowerCase();
|
|
1807
1921
|
if (!text.trim()) return false;
|
|
@@ -1810,6 +1924,9 @@ ${colors.reset}
|
|
|
1810
1924
|
const fakeCompletionClaims = /(?:đã (?:sửa|tạo|viết|xóa|cập nhật|thêm|chỉnh|xong|hoàn thành|fix|update|edit|write|create|delete|remove|modify|change|apply|deploy|push)|i(?:'ve| have) (?:fixed|created|written|updated|added|modified|changed|edited|applied|deployed|deleted|removed|patched|implemented|refactored)|done!|xong rồi|hoàn thành|đã hoàn tất|hoàn tất|the (?:fix|change|update|edit|modification) (?:has been|is) (?:applied|done|completed|made)|here(?:'s| is) the (?:fix|update|change|solution|implementation|code)|file (?:has been|was) (?:updated|created|modified|written|changed)|changes? (?:have been|has been|were) (?:made|applied|saved)|successfully (?:updated|created|modified|fixed|applied|changed|written))/i;
|
|
1811
1925
|
if (fakeCompletionClaims.test(text)) return true;
|
|
1812
1926
|
|
|
1927
|
+
const fakeBrowserClaims = /(?:đã|da|i(?:'ve| have))\s+(?:bấm|bam|click(?:ed)?|mở|mo|open(?:ed)?|điền|dien|fill(?:ed)?|chọn|chon|select(?:ed)?|submit(?:ted)?|vào|vao|navigate(?:d)?)/i;
|
|
1928
|
+
if (fakeBrowserClaims.test(text)) return true;
|
|
1929
|
+
|
|
1813
1930
|
// Detect code blocks that pretend to show "changes" without tool use
|
|
1814
1931
|
const codeBlockWithFilePath = /```[\s\S]*?(?:[\/\\][\w.-]+\.(?:js|ts|py|css|html|json|md|jsx|tsx|vue|go|rs|java|c|cpp|rb|sh))[\s\S]*?```/i;
|
|
1815
1932
|
const claimsFileChange = /(?:here(?:'s| is)|below|sau đây|dưới đây|như sau|updated|modified|changed|new|fixed)/i;
|
|
@@ -1820,6 +1937,19 @@ ${colors.reset}
|
|
|
1820
1937
|
|
|
1821
1938
|
buildToolEvidenceCorrection(messages = []) {
|
|
1822
1939
|
const request = this.getLatestUserText(messages);
|
|
1940
|
+
if (this.isBrowserInteractionRequest(request)) {
|
|
1941
|
+
return [
|
|
1942
|
+
'RUNTIME ENFORCEMENT: Your previous response was BLOCKED because you claimed a browser/web action without real browser tool evidence.',
|
|
1943
|
+
'',
|
|
1944
|
+
'For browser interaction tasks you MUST use tools, not prose:',
|
|
1945
|
+
'1. If chrome-devtools MCP is configured, call MCP {"server":"chrome-devtools","tool":"list"} first if needed.',
|
|
1946
|
+
'2. Use chrome-devtools MCP tools such as new_page/navigate_page, take_snapshot, click, fill/fill_form, evaluate_script, list_network_requests, and take_screenshot in visible Chrome.',
|
|
1947
|
+
'3. Use WebFetch only for static text extraction. WebFetch cannot click buttons, fill forms, or preserve page state. BrowserDebug is headless and should be fallback only.',
|
|
1948
|
+
'4. Do not say "đã bấm", "đã điền", "đã mở", or "đã kiểm tra" until a browser/MCP tool result proves it.',
|
|
1949
|
+
'',
|
|
1950
|
+
`Original user request: ${request}`,
|
|
1951
|
+
].join('\n');
|
|
1952
|
+
}
|
|
1823
1953
|
return [
|
|
1824
1954
|
'⚠️ RUNTIME ENFORCEMENT: Your previous response was BLOCKED because you did not use any tool.',
|
|
1825
1955
|
'',
|
|
@@ -1832,7 +1962,7 @@ ${colors.reset}
|
|
|
1832
1962
|
'DO NOT say "I have updated/created/fixed" without a tool call proving it.',
|
|
1833
1963
|
'DO NOT describe what you would do. Actually DO IT with tool calls.',
|
|
1834
1964
|
'',
|
|
1835
|
-
'Available tools: Read, Write, Edit, Bash, Glob, Grep, BrowserDebug, WebFetch, WebSearch.',
|
|
1965
|
+
'Available tools: Read, Write, Edit, Bash, Glob, Grep, OpenBrowser, BrowserDebug, WebFetch, WebSearch, MCP.',
|
|
1836
1966
|
'',
|
|
1837
1967
|
'If native tool calls are not supported, output exactly one fallback tool call:',
|
|
1838
1968
|
'<invoke name="Read"><parameter name="path">README.md</parameter></invoke>',
|
|
@@ -1848,11 +1978,16 @@ ${colors.reset}
|
|
|
1848
1978
|
* Giúp model yếu/nhỏ chọn đúng tool thay vì dùng Bash cho mọi thứ.
|
|
1849
1979
|
*/
|
|
1850
1980
|
buildToolRoutingHint(userMessage = '') {
|
|
1851
|
-
const
|
|
1981
|
+
const rawText = String(userMessage || '').toLowerCase();
|
|
1982
|
+
const text = `${rawText}\n${this.normalizeIntentText(rawText)}`;
|
|
1852
1983
|
if (!text.trim()) return null;
|
|
1853
1984
|
|
|
1854
1985
|
const hints = [];
|
|
1855
1986
|
|
|
1987
|
+
if (/\b(click|fill|submit|press|select|navigate|bam|dien|chon|nhan|vao|bấm|điền|chọn|nhấn|vào)\b/i.test(text) && /\b(web|website|page|url|http|chrome|browser|form|button|link|trang|nut|nút|dang ky|dang nhap|khach hang|đăng ký|đăng nhập|khách hàng)\b/i.test(text)) {
|
|
1988
|
+
hints.push('TOOL HINT: This is a live browser interaction. Do NOT use WebFetch alone. Prefer visible Chrome via MCP {"server":"chrome-devtools","tool":"list"} then chrome-devtools tools new_page/navigate_page, take_snapshot, click, fill/fill_form, evaluate_script, list_network_requests, and take_screenshot. Only use BrowserDebug as headless fallback. Only claim click/fill/navigation after MCP or BrowserDebug evidence.');
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1856
1991
|
// Detect file reading requests
|
|
1857
1992
|
const hasPath = /[A-Za-z]:[\\/][\w.\\/\\-]+/i.test(text) || /(?:^|\s)[.~]?\/[\w.\/-]+/i.test(text);
|
|
1858
1993
|
const readVerbs = /\b(đọc|doc|read|xem|view|mở|open|show|hiện|hiển thị|cat|type)\b/i;
|
|
@@ -1891,7 +2026,11 @@ ${colors.reset}
|
|
|
1891
2026
|
|
|
1892
2027
|
// Detect URL/web requests
|
|
1893
2028
|
if (/\b(https?:\/\/[^\s]+|url|website|trang web|web page)\b/i.test(text)) {
|
|
1894
|
-
hints.push('TOOL HINT: To fetch
|
|
2029
|
+
hints.push('TOOL HINT: To fetch static URL text, call WebFetch. For live visible browser debugging/control, prefer MCP server chrome-devtools with new_page/navigate_page, take_snapshot, click, fill/fill_form, evaluate_script, list_console_messages, list_network_requests, or performance trace tools; use BrowserDebug only as headless fallback.');
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
if (/\b(chrome|devtools|browser debug|debug browser|screenshot|console|network|lcp|performance|perf|web vitals|localhost|127\.0\.0\.1)\b/i.test(text)) {
|
|
2033
|
+
hints.push('TOOL HINT: For "open Chrome" / "mở chrome", call OpenBrowser {"browser":"chrome","url":"about:blank"}. Do NOT call Bash/Get-Command/Start-Process. If configured, use MCP {"server":"chrome-devtools","tool":"list"} for page state, console, network, screenshots, and performance.');
|
|
1895
2034
|
}
|
|
1896
2035
|
|
|
1897
2036
|
if (hints.length === 0) return null;
|
|
@@ -2233,6 +2372,9 @@ ${colors.reset}
|
|
|
2233
2372
|
|
|
2234
2373
|
async requestFinalAnswer(messages, toolSummaries, startedAt, totalUsage) {
|
|
2235
2374
|
const executionProfile = this.selectExecutionProfile(messages, { enableTools: false });
|
|
2375
|
+
const latestUserText = this.getLatestUserText(messages);
|
|
2376
|
+
const browserInteraction = this.isBrowserInteractionRequest(latestUserText);
|
|
2377
|
+
const hasBrowserEvidence = toolSummaries.some(summary => /^(MCP|BrowserDebug|OpenBrowser):/i.test(summary));
|
|
2236
2378
|
const finalMessages = [
|
|
2237
2379
|
...messages,
|
|
2238
2380
|
{
|
|
@@ -2241,7 +2383,11 @@ ${colors.reset}
|
|
|
2241
2383
|
'You have finished using tools.',
|
|
2242
2384
|
'Now answer the user directly in the same language as the user.',
|
|
2243
2385
|
'Do not call more tools. Do not ask the user to provide files you already read.',
|
|
2244
|
-
'Use the tool results above
|
|
2386
|
+
'Use only the tool results above as evidence for claims about files, commands, tests, and changes.',
|
|
2387
|
+
'Start with the actual outcome, then mention only the most relevant files/commands. Avoid broad generic advice.',
|
|
2388
|
+
'If a tool failed, explain the concrete failure briefly and answer with the available evidence.',
|
|
2389
|
+
'Do not repeat the plan. Do not re-summarize unrelated project context. Do not claim memory/tool state that is not visible in the transcript.',
|
|
2390
|
+
browserInteraction && !hasBrowserEvidence ? 'Important: The user asked for browser interaction, but no MCP/BrowserDebug/OpenBrowser result is available. You must say the browser action was NOT performed; do not claim you clicked, filled, navigated, or inspected pages.' : '',
|
|
2245
2391
|
toolSummaries.length ? `Tool summary:\n${toolSummaries.join('\n')}` : '',
|
|
2246
2392
|
].filter(Boolean).join('\n'),
|
|
2247
2393
|
},
|
|
@@ -2254,7 +2400,7 @@ ${colors.reset}
|
|
|
2254
2400
|
}
|
|
2255
2401
|
|
|
2256
2402
|
if (typeof this.ai.streamRequest === 'function') {
|
|
2257
|
-
return await this.streamFinalAnswer(finalMessages, startedAt, totalUsage, executionProfile);
|
|
2403
|
+
return await this.streamFinalAnswer(finalMessages, startedAt, totalUsage, executionProfile, { browserInteraction, hasBrowserEvidence });
|
|
2258
2404
|
}
|
|
2259
2405
|
|
|
2260
2406
|
const response = await this.ai.sendRequest(finalMessages, {
|
|
@@ -2264,7 +2410,10 @@ ${colors.reset}
|
|
|
2264
2410
|
signal: this.currentAbortController?.signal,
|
|
2265
2411
|
});
|
|
2266
2412
|
this.addUsage(totalUsage, response.usage);
|
|
2267
|
-
|
|
2413
|
+
let content = response.choices?.[0]?.message?.content || '';
|
|
2414
|
+
if (browserInteraction && !hasBrowserEvidence && this.detectFakeCompletion(content)) {
|
|
2415
|
+
content = 'Chưa thực hiện được thao tác trên trình duyệt: lượt này không có bằng chứng từ MCP/BrowserDebug/OpenBrowser, nên Winter chặn câu trả lời để tránh báo sai. Hãy bật chrome-devtools MCP hoặc dùng lại yêu cầu để Winter gọi đúng browser tool.';
|
|
2416
|
+
}
|
|
2268
2417
|
|
|
2269
2418
|
if (this.spinner) this.spinner.stop();
|
|
2270
2419
|
|
|
@@ -2281,7 +2430,7 @@ ${colors.reset}
|
|
|
2281
2430
|
}
|
|
2282
2431
|
}
|
|
2283
2432
|
|
|
2284
|
-
async streamFinalAnswer(messages, startedAt, totalUsage, executionProfile = null) {
|
|
2433
|
+
async streamFinalAnswer(messages, startedAt, totalUsage, executionProfile = null, validation = {}) {
|
|
2285
2434
|
let content = '';
|
|
2286
2435
|
const profile = executionProfile || this.selectExecutionProfile(messages, { enableTools: false });
|
|
2287
2436
|
|
|
@@ -2307,6 +2456,10 @@ ${colors.reset}
|
|
|
2307
2456
|
|
|
2308
2457
|
if (this.spinner) this.spinner.stop();
|
|
2309
2458
|
|
|
2459
|
+
if (validation.browserInteraction && !validation.hasBrowserEvidence && this.detectFakeCompletion(content)) {
|
|
2460
|
+
content = 'Chưa thực hiện được thao tác trên trình duyệt: lượt này không có bằng chứng từ MCP/BrowserDebug/OpenBrowser, nên Winter chặn câu trả lời để tránh báo sai. Hãy bật chrome-devtools MCP hoặc dùng lại yêu cầu để Winter gọi đúng browser tool.';
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2310
2463
|
if (content) {
|
|
2311
2464
|
this.printAssistantAnswer(content, startedAt, totalUsage);
|
|
2312
2465
|
return content;
|
|
@@ -2327,6 +2480,9 @@ ${colors.reset}
|
|
|
2327
2480
|
});
|
|
2328
2481
|
this.addUsage(totalUsage, response.usage);
|
|
2329
2482
|
content = response.choices?.[0]?.message?.content || '';
|
|
2483
|
+
if (validation.browserInteraction && !validation.hasBrowserEvidence && this.detectFakeCompletion(content)) {
|
|
2484
|
+
content = 'Chưa thực hiện được thao tác trên trình duyệt: lượt này không có bằng chứng từ MCP/BrowserDebug/OpenBrowser, nên Winter chặn câu trả lời để tránh báo sai. Hãy bật chrome-devtools MCP hoặc dùng lại yêu cầu để Winter gọi đúng browser tool.';
|
|
2485
|
+
}
|
|
2330
2486
|
if (content) {
|
|
2331
2487
|
this.printAssistantAnswer(content, startedAt, totalUsage);
|
|
2332
2488
|
}
|
|
@@ -2495,6 +2651,44 @@ ${colors.reset}
|
|
|
2495
2651
|
};
|
|
2496
2652
|
}
|
|
2497
2653
|
|
|
2654
|
+
async updateRecentWorkLedger({ userMessage = '', finalContent = '', toolCalls = [], usedTools = false, usedMutatingTools = false } = {}) {
|
|
2655
|
+
if (typeof this.session?.replaceMemory !== 'function') return;
|
|
2656
|
+
|
|
2657
|
+
const toolLines = Array.isArray(toolCalls) && toolCalls.length > 0
|
|
2658
|
+
? toolCalls.slice(-8).map(call => `- ${this.summarizeToolCallForLedger(call)}`).join('\n')
|
|
2659
|
+
: '- none';
|
|
2660
|
+
const status = usedMutatingTools ? 'changed project state' : (usedTools ? 'inspected project state' : 'answered without tools');
|
|
2661
|
+
const final = this.compactText(String(finalContent || '').replace(/\s+/g, ' ').trim(), 700, 'final answer');
|
|
2662
|
+
const request = this.compactText(String(userMessage || '').replace(/\s+/g, ' ').trim(), 400, 'user request');
|
|
2663
|
+
const content = [
|
|
2664
|
+
`Updated: ${new Date().toISOString()}`,
|
|
2665
|
+
`Status: ${status}`,
|
|
2666
|
+
`Last user request: ${request || '(empty)'}`,
|
|
2667
|
+
'Tool evidence from last turn:',
|
|
2668
|
+
toolLines,
|
|
2669
|
+
final ? `Last answer summary: ${final}` : 'Last answer summary: (empty)',
|
|
2670
|
+
'',
|
|
2671
|
+
'Instruction for next turn: treat this ledger as the source of truth for what Winter already did. Do not repeat completed inspection unless the user asks or new evidence is needed.',
|
|
2672
|
+
].join('\n');
|
|
2673
|
+
|
|
2674
|
+
await this.session.replaceMemory('[Recent Work Ledger]', content, 'summary');
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
summarizeToolCallForLedger(call = {}) {
|
|
2678
|
+
const fn = call.function || {};
|
|
2679
|
+
const name = this.tools?.normalizeToolName?.(fn.name || call.name || call.toolName || 'unknown') || fn.name || call.name || call.toolName || 'unknown';
|
|
2680
|
+
let args = {};
|
|
2681
|
+
try {
|
|
2682
|
+
args = typeof fn.arguments === 'string' ? JSON.parse(fn.arguments || '{}') : (call.toolArgs || fn.arguments || {});
|
|
2683
|
+
} catch {
|
|
2684
|
+
args = call.toolArgs || {};
|
|
2685
|
+
}
|
|
2686
|
+
const target = args.file_path || args.path || args.cwd || args.url || args.server || args.pattern || '';
|
|
2687
|
+
const command = args.command || args.cmd || '';
|
|
2688
|
+
const detail = command || target || '';
|
|
2689
|
+
return detail ? `${name}: ${this.compactText(String(detail), 180, `${name} args`)}` : name;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2498
2692
|
async compressSessionContext(verbose = false) {
|
|
2499
2693
|
const raw = this.session.getHistory(80)
|
|
2500
2694
|
.filter(entry => entry && typeof entry.content === 'string')
|
|
@@ -2711,6 +2905,13 @@ ${colors.reset}
|
|
|
2711
2905
|
historyEntry.tool_calls = allToolCalls;
|
|
2712
2906
|
}
|
|
2713
2907
|
await this.session.addToHistory(historyEntry);
|
|
2908
|
+
await this.updateRecentWorkLedger({
|
|
2909
|
+
userMessage: message,
|
|
2910
|
+
finalContent,
|
|
2911
|
+
toolCalls: allToolCalls,
|
|
2912
|
+
usedTools: allToolCalls.length > 0,
|
|
2913
|
+
usedMutatingTools,
|
|
2914
|
+
});
|
|
2714
2915
|
|
|
2715
2916
|
// Tự động verify: nếu AI đã dùng tools (sửa code), chạy test/build.
|
|
2716
2917
|
if (finalContent && this.shouldAutoVerifyAfterTools(message, usedMutatingTools)) {
|
|
@@ -3319,6 +3520,8 @@ Light mode enabled for safety. Heavy codebase, graph, and git context are skippe
|
|
|
3319
3520
|
const [action, ...rest] = args;
|
|
3320
3521
|
const config = await this.config.load();
|
|
3321
3522
|
config.mcp = config.mcp || { servers: [] };
|
|
3523
|
+
config.permissions = config.permissions || { allowlist: {} };
|
|
3524
|
+
config.permissions.allowlist = config.permissions.allowlist || { tools: [], commands: [], mcpServers: [] };
|
|
3322
3525
|
|
|
3323
3526
|
switch (action) {
|
|
3324
3527
|
case undefined:
|
|
@@ -3351,10 +3554,33 @@ Light mode enabled for safety. Heavy codebase, graph, and git context are skippe
|
|
|
3351
3554
|
}
|
|
3352
3555
|
config.mcp.servers = (config.mcp.servers || []).filter(server => server.name !== name);
|
|
3353
3556
|
config.mcp.servers.push({ name, command, args: parsedArgs, enabled: true });
|
|
3557
|
+
config.permissions.allowlist.mcpServers = [...new Set([...(config.permissions.allowlist.mcpServers || []), name])];
|
|
3354
3558
|
await this.config.save(config);
|
|
3355
3559
|
console.log(`${colors.green}✓ Added MCP server: ${name}${colors.reset}`);
|
|
3356
3560
|
break;
|
|
3357
3561
|
}
|
|
3562
|
+
case 'preset':
|
|
3563
|
+
case 'install': {
|
|
3564
|
+
const [presetName, ...presetOptions] = rest;
|
|
3565
|
+
if (!presetName) {
|
|
3566
|
+
console.log(`${colors.yellow}Usage: /mcp preset <chrome-devtools> [--isolated] [--headless] [--browser-url <url>]${colors.reset}`);
|
|
3567
|
+
break;
|
|
3568
|
+
}
|
|
3569
|
+
try {
|
|
3570
|
+
const server = getMcpPreset(presetName, presetOptions);
|
|
3571
|
+
upsertMcpServer(config, server);
|
|
3572
|
+
await this.config.save(config);
|
|
3573
|
+
console.log(`${colors.green}OK Installed MCP preset: ${server.name}${colors.reset}`);
|
|
3574
|
+
console.log(` ${colors.dim}${server.command} ${server.args.join(' ')}${colors.reset}`);
|
|
3575
|
+
if (!server.args.includes('--headless')) {
|
|
3576
|
+
console.log(` ${colors.dim}Visible Chrome mode: enabled. Use --headless only for background browser runs.${colors.reset}`);
|
|
3577
|
+
}
|
|
3578
|
+
console.log(` ${colors.dim}Inspect tools with: /mcp tools ${server.name}${colors.reset}`);
|
|
3579
|
+
} catch (error) {
|
|
3580
|
+
console.log(`${colors.red}${error.message}${colors.reset}`);
|
|
3581
|
+
}
|
|
3582
|
+
break;
|
|
3583
|
+
}
|
|
3358
3584
|
case 'remove': {
|
|
3359
3585
|
const name = rest[0];
|
|
3360
3586
|
if (!name) {
|
|
@@ -3362,6 +3588,7 @@ Light mode enabled for safety. Heavy codebase, graph, and git context are skippe
|
|
|
3362
3588
|
break;
|
|
3363
3589
|
}
|
|
3364
3590
|
config.mcp.servers = (config.mcp.servers || []).filter(server => server.name !== name);
|
|
3591
|
+
config.permissions.allowlist.mcpServers = (config.permissions.allowlist.mcpServers || []).filter(server => server !== name);
|
|
3365
3592
|
await this.config.save(config);
|
|
3366
3593
|
console.log(`${colors.green}✓ Removed MCP server: ${name}${colors.reset}`);
|
|
3367
3594
|
break;
|
|
@@ -3376,8 +3603,37 @@ Light mode enabled for safety. Heavy codebase, graph, and git context are skippe
|
|
|
3376
3603
|
console.log(`${colors.green}✓ MCP server allowed: ${name}${colors.reset}`);
|
|
3377
3604
|
break;
|
|
3378
3605
|
}
|
|
3606
|
+
case 'tools': {
|
|
3607
|
+
const name = rest[0];
|
|
3608
|
+
if (!name) {
|
|
3609
|
+
console.log(`${colors.yellow}Usage: /mcp tools <name>${colors.reset}`);
|
|
3610
|
+
break;
|
|
3611
|
+
}
|
|
3612
|
+
const server = (config.mcp.servers || []).find(item => item.name === name && item.enabled !== false);
|
|
3613
|
+
if (!server) {
|
|
3614
|
+
console.log(`${colors.red}MCP server not configured or disabled: ${name}${colors.reset}`);
|
|
3615
|
+
break;
|
|
3616
|
+
}
|
|
3617
|
+
const client = new MCPClient(server);
|
|
3618
|
+
try {
|
|
3619
|
+
const tools = await client.listTools();
|
|
3620
|
+
console.log(`${colors.cyan}MCP Tools: ${name}${colors.reset}`);
|
|
3621
|
+
if (!tools.length) {
|
|
3622
|
+
console.log(` ${colors.dim}No tools reported.${colors.reset}`);
|
|
3623
|
+
}
|
|
3624
|
+
tools.forEach(tool => {
|
|
3625
|
+
const description = tool.description ? ` - ${tool.description}` : '';
|
|
3626
|
+
console.log(` ${colors.green}${tool.name}${colors.reset}${description}`);
|
|
3627
|
+
});
|
|
3628
|
+
} catch (error) {
|
|
3629
|
+
console.log(`${colors.red}Failed to list MCP tools: ${error.message}${colors.reset}`);
|
|
3630
|
+
} finally {
|
|
3631
|
+
await client.close();
|
|
3632
|
+
}
|
|
3633
|
+
break;
|
|
3634
|
+
}
|
|
3379
3635
|
default:
|
|
3380
|
-
console.log(`${colors.yellow}Usage: /mcp <list|add|remove|allow>${colors.reset}`);
|
|
3636
|
+
console.log(`${colors.yellow}Usage: /mcp <list|add|preset|install|remove|allow|tools>${colors.reset}`);
|
|
3381
3637
|
}
|
|
3382
3638
|
}
|
|
3383
3639
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const SLASH_COMMANDS = [
|
|
2
2
|
{ cmd: '/project', desc: 'Show/set current project' },
|
|
3
3
|
{ cmd: '/index', desc: 'Index codebase for semantic search', usage: '/index [full]' },
|
|
4
|
+
{ cmd: '/rag', desc: 'RAG query and index management', sub: ['query', 'index', 'status', 'search'] },
|
|
4
5
|
{ cmd: '/search', desc: 'Semantic codebase search', usage: '/search <query>' },
|
|
5
6
|
{ cmd: '/search-def', desc: 'Find symbol/definition in codebase', usage: '/search-def <name>' },
|
|
6
7
|
{ cmd: '/undo', desc: 'Undo last change from backup', usage: '/undo [file]' },
|
|
@@ -40,7 +41,7 @@ export const SLASH_COMMANDS = [
|
|
|
40
41
|
{ cmd: '/skill', desc: 'Skills management', sub: ['list', 'enable', 'create'] },
|
|
41
42
|
{ cmd: '/skills', desc: 'List local Winter/Codex/Claude skills' },
|
|
42
43
|
{ cmd: '/plugin', desc: 'Plugin management', sub: ['list', 'install', 'remove'] },
|
|
43
|
-
{ cmd: '/mcp', desc: 'MCP server management', sub: ['list', 'add', 'remove', 'allow'] },
|
|
44
|
+
{ cmd: '/mcp', desc: 'MCP server management', sub: ['list', 'add', 'preset', 'install', 'remove', 'allow', 'tools'] },
|
|
44
45
|
{ cmd: '/permissions', desc: 'Permission allowlist', sub: ['list', 'allow', 'prompt'] },
|
|
45
46
|
{ cmd: '/stats', desc: 'Tool usage statistics' },
|
|
46
47
|
{ cmd: '/replay', desc: 'Replay recent session/tool events', usage: '/replay [count]' },
|
package/src/mcp/client.js
CHANGED
|
@@ -22,12 +22,14 @@ export class MCPClient {
|
|
|
22
22
|
throw new Error('MCP server command is required');
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const args = Array.isArray(this.serverConfig.args) ? this.serverConfig.args : [];
|
|
26
|
-
this.process = spawn(command, args, {
|
|
27
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
const args = Array.isArray(this.serverConfig.args) ? this.serverConfig.args : [];
|
|
26
|
+
this.process = spawn(command, args, {
|
|
27
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
28
|
+
cwd: this.serverConfig.cwd || undefined,
|
|
29
|
+
env: { ...process.env, ...(this.serverConfig.env || {}) },
|
|
30
|
+
shell: false,
|
|
31
|
+
windowsHide: true,
|
|
32
|
+
});
|
|
31
33
|
|
|
32
34
|
this.process.stdout.on('data', chunk => this.handleStdout(chunk));
|
|
33
35
|
this.process.stderr.on('data', chunk => {
|
package/src/mcp/ide-server.js
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
import { createServer } from 'net';
|
|
7
7
|
import { promises as fs } from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
|
-
import {
|
|
9
|
+
import { CompletionProvider } from './completions.js';
|
|
10
|
+
import { ConfigLoader } from '../cli/config.js';
|
|
11
|
+
import { AIProviderManager } from '../ai/providers.js';
|
|
12
|
+
import { extractTextFromResponse } from '../ai/provider-adapters.js';
|
|
10
13
|
|
|
11
14
|
const SERVER_NAME = 'winter-ide';
|
|
12
15
|
const SERVER_VERSION = '1.0.0';
|
|
@@ -16,10 +19,13 @@ export class IDEServer {
|
|
|
16
19
|
this.port = options.port || 4157;
|
|
17
20
|
this.host = options.host || '127.0.0.1';
|
|
18
21
|
this.projectPath = options.projectPath || process.cwd();
|
|
19
|
-
this.protocol = new MCPProtocol();
|
|
20
22
|
this.server = null;
|
|
21
23
|
this.clients = new Map();
|
|
22
24
|
this.handlers = new Map();
|
|
25
|
+
this.completionProvider = new CompletionProvider();
|
|
26
|
+
this.config = options.config || new ConfigLoader();
|
|
27
|
+
this.ai = options.ai || new AIProviderManager(this.config);
|
|
28
|
+
this.aiReady = false;
|
|
23
29
|
this._registerDefaultHandlers();
|
|
24
30
|
}
|
|
25
31
|
|
|
@@ -162,6 +168,73 @@ export class IDEServer {
|
|
|
162
168
|
this.handlers.set('ping', (clientId) => {
|
|
163
169
|
this._sendTo(clientId, { type: 'pong', timestamp: Date.now() });
|
|
164
170
|
});
|
|
171
|
+
|
|
172
|
+
this.handlers.set('inline:complete', async (clientId, msg) => {
|
|
173
|
+
try {
|
|
174
|
+
const filePath = msg.path || msg.filePath;
|
|
175
|
+
if (!filePath) {
|
|
176
|
+
this._sendTo(clientId, { type: 'error', message: 'inline:complete requires path' });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.resolve(this.projectPath, filePath);
|
|
181
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
182
|
+
const lines = content.split(/\r?\n/);
|
|
183
|
+
const line = Number.isFinite(Number(msg.line)) ? Math.max(1, Number(msg.line)) : lines.length;
|
|
184
|
+
const column = Number.isFinite(Number(msg.column)) ? Math.max(0, Number(msg.column)) : (lines[line - 1] || '').length;
|
|
185
|
+
|
|
186
|
+
const result = await this.completionProvider.generate({
|
|
187
|
+
filePath: fullPath,
|
|
188
|
+
content,
|
|
189
|
+
cursorLine: Math.min(line - 1, Math.max(0, lines.length - 1)),
|
|
190
|
+
cursorColumn: column,
|
|
191
|
+
language: detectLanguage(fullPath),
|
|
192
|
+
});
|
|
193
|
+
this._sendTo(clientId, { type: 'inline:complete:result', ...result });
|
|
194
|
+
} catch (error) {
|
|
195
|
+
this._sendTo(clientId, { type: 'error', message: `Inline completion failed: ${error.message}` });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
this.handlers.set('ai:action', async (clientId, msg) => {
|
|
200
|
+
try {
|
|
201
|
+
const action = String(msg.action || 'explain').toLowerCase();
|
|
202
|
+
const code = String(msg.code || '');
|
|
203
|
+
if (!code.trim()) {
|
|
204
|
+
this._sendTo(clientId, { type: 'error', message: 'ai:action requires code' });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await this._ensureAi();
|
|
209
|
+
const prompt = buildActionPrompt(action, code, msg.filePath || msg.path);
|
|
210
|
+
const data = await this.ai.sendRequest([
|
|
211
|
+
{ role: 'system', content: 'You are Winter IDE assistant. Be concise, practical, and return code only when asked to fix, refactor, or generate tests.' },
|
|
212
|
+
{ role: 'user', content: prompt },
|
|
213
|
+
], { reason: `ide:${action}` });
|
|
214
|
+
|
|
215
|
+
const response = extractTextFromResponse(data);
|
|
216
|
+
const payload = {
|
|
217
|
+
type: 'ai:action:result',
|
|
218
|
+
action,
|
|
219
|
+
response,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
if (['fix', 'refactor', 'generate-tests'].includes(action)) {
|
|
223
|
+
const editedContent = extractCodeBlock(response);
|
|
224
|
+
if (editedContent) payload.editedContent = editedContent;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this._sendTo(clientId, payload);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
this._sendTo(clientId, { type: 'error', message: `AI action failed: ${error.message}` });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async _ensureAi() {
|
|
235
|
+
if (this.aiReady) return;
|
|
236
|
+
await this.ai.init();
|
|
237
|
+
this.aiReady = true;
|
|
165
238
|
}
|
|
166
239
|
|
|
167
240
|
async _handleMessage(clientId, raw) {
|
|
@@ -192,4 +265,43 @@ export class IDEServer {
|
|
|
192
265
|
}
|
|
193
266
|
}
|
|
194
267
|
|
|
268
|
+
function detectLanguage(filePath) {
|
|
269
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
270
|
+
const map = {
|
|
271
|
+
'.js': 'javascript',
|
|
272
|
+
'.jsx': 'javascriptreact',
|
|
273
|
+
'.ts': 'typescript',
|
|
274
|
+
'.tsx': 'typescriptreact',
|
|
275
|
+
'.py': 'python',
|
|
276
|
+
'.go': 'go',
|
|
277
|
+
'.rs': 'rust',
|
|
278
|
+
'.java': 'java',
|
|
279
|
+
'.cs': 'csharp',
|
|
280
|
+
'.cpp': 'cpp',
|
|
281
|
+
'.c': 'c',
|
|
282
|
+
'.h': 'c',
|
|
283
|
+
'.hpp': 'cpp',
|
|
284
|
+
'.json': 'json',
|
|
285
|
+
'.md': 'markdown',
|
|
286
|
+
};
|
|
287
|
+
return map[ext] || ext.replace(/^\./, '') || 'text';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function buildActionPrompt(action, code, filePath = '') {
|
|
291
|
+
const location = filePath ? `File: ${filePath}\n\n` : '';
|
|
292
|
+
const prompts = {
|
|
293
|
+
explain: 'Explain what this code does and point out notable risks.',
|
|
294
|
+
refactor: 'Refactor this code. Return the improved code in one fenced code block, followed by a brief note.',
|
|
295
|
+
fix: 'Find and fix bugs in this code. Return the fixed code in one fenced code block, followed by a brief note.',
|
|
296
|
+
review: 'Review this code for bugs, risks, and missing tests. Prioritize findings.',
|
|
297
|
+
'generate-tests': 'Generate practical tests for this file. Return the test code in one fenced code block.',
|
|
298
|
+
};
|
|
299
|
+
return `${location}${prompts[action] || prompts.explain}\n\nCode:\n\`\`\`\n${code}\n\`\`\``;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function extractCodeBlock(text = '') {
|
|
303
|
+
const match = String(text).match(/```(?:[\w-]+)?\r?\n([\s\S]*?)```/);
|
|
304
|
+
return match ? match[1].trimEnd() : '';
|
|
305
|
+
}
|
|
306
|
+
|
|
195
307
|
export default IDEServer;
|