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/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 BrowserDebug when a URL/dev server is available.
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 text = this.getLatestUserText(messages).toLowerCase();
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 text = String(userMessage || '').toLowerCase();
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 a URL, call tool WebFetch with {"url": "<url>"}. For browser debugging, use BrowserDebug.');
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. If a tool failed, explain the concrete failure briefly and answer with the available evidence.',
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
- const content = response.choices?.[0]?.message?.content || '';
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
- shell: false,
29
- windowsHide: true,
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 => {
@@ -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 { MCPProtocol } from './protocol.js';
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;