winter-super-cli 2026.6.24 → 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 CHANGED
@@ -162,6 +162,25 @@ Winter reads config from the user profile directory. Typical settings include:
162
162
  - MCP servers
163
163
  - sandbox / allowlist options
164
164
 
165
+ ### Chrome DevTools MCP
166
+
167
+ Winter has a built-in preset for ChromeDevTools/chrome-devtools-mcp:
168
+
169
+ ```bash
170
+ winter mcp preset chrome-devtools --isolated
171
+ winter mcp tools chrome-devtools
172
+ ```
173
+
174
+ In the REPL, use the same flow with slash commands:
175
+
176
+ ```text
177
+ /mcp preset chrome-devtools --isolated
178
+ /mcp tools chrome-devtools
179
+ ```
180
+
181
+ The preset registers the `chrome-devtools` MCP server, allowlists it, and gives Winter runtime hints to use its page navigation, click, fill, snapshot, screenshot, console, network, and performance tools for live browser debugging. Omit `--headless` when you want to watch Winter operate Chrome in a normal visible window.
182
+ It requires Node.js 22.12+ and a current Chrome installation, matching the upstream MCP package requirements.
183
+
165
184
  ### Minimal example
166
185
 
167
186
  ```json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "winter-super-cli",
3
- "version": "2026.6.24",
3
+ "version": "2026.6.26",
4
4
  "description": "❄️ AI-Powered Development CLI with Interactive REPL",
5
5
  "type": "module",
6
6
  "main": "bin/winter.js",
@@ -3,13 +3,13 @@ import { pathToFileURL } from 'url';
3
3
  import { promises as fs } from 'fs';
4
4
 
5
5
  const DEFAULT_TOOL_SETS = {
6
- general: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'BrowserDebug', 'WebFetch', 'WebSearch', 'Parallel', 'Agent'],
6
+ general: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'OpenBrowser', 'BrowserDebug', 'WebFetch', 'WebSearch', 'Parallel', 'Agent'],
7
7
  plan: ['Read', 'Grep', 'Glob', 'WebFetch', 'WebSearch', 'Parallel'],
8
8
  review: ['Read', 'Grep', 'Glob', 'Bash', 'WebFetch'],
9
- debug: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'BrowserDebug', 'WebFetch', 'Parallel'],
10
- design: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'BrowserDebug', 'WebFetch'],
9
+ debug: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'OpenBrowser', 'BrowserDebug', 'WebFetch', 'Parallel'],
10
+ design: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'OpenBrowser', 'BrowserDebug', 'WebFetch'],
11
11
  research: ['Read', 'Grep', 'Glob', 'WebFetch', 'WebSearch', 'Parallel'],
12
- swe: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'BrowserDebug', 'Parallel'],
12
+ swe: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'OpenBrowser', 'BrowserDebug', 'Parallel'],
13
13
  };
14
14
 
15
15
  const BUILTIN_AGENTS = [
@@ -100,7 +100,9 @@ function buildStandardSystemPrompt(options = {}) {
100
100
  'Use tools when they materially improve correctness. Inspect before editing. Verify after changes.',
101
101
  'Use maximum reasoning discipline for every model tier, including tiny, local, free, and routed models.',
102
102
  'Never invent file paths, APIs, command output, or test results.',
103
+ 'For visible browser launch requests such as "mở chrome" or "open Chrome", use OpenBrowser; do not use Bash, Get-Command, Start-Process, or shell launch commands.',
103
104
  'For debug work, locate the first hard failure, patch the root cause, and verify with the closest test/build/browser smoke.',
105
+ 'For live browser debugging and user-visible Chrome control, prefer MCP server chrome-devtools when configured; use its page, click, fill, snapshot, screenshot, console, network, and performance tools before falling back to headless browser automation.',
104
106
  'For design/UI work, inspect the existing interface and design resources first; avoid generic placeholder layouts.',
105
107
  'If the user attaches or pastes an image, analyze it as primary evidence.',
106
108
  '',
@@ -10,6 +10,7 @@ import { DesignCommands } from '../design/commands.js';
10
10
  import { SkillManager } from '../skills/manager.js';
11
11
  import { PluginManager } from '../plugins/manager.js';
12
12
  import { MCPClient } from '../mcp/client.js';
13
+ import { getMcpPreset, upsertMcpServer } from '../mcp/presets.js';
13
14
  import { BenchmarkRunner } from '../ai/benchmark.js';
14
15
  import { redactSecrets } from './secret-env.js';
15
16
  import { formatRuntimeEnvironmentSummary, getRuntimeEnvironment } from './runtime-env.js';
@@ -813,6 +814,29 @@ EXECUTION CONTRACT:
813
814
  console.log(`${colors.green}✓ Added MCP server: ${name}${colors.reset}`);
814
815
  break;
815
816
  }
817
+ case 'preset':
818
+ case 'install': {
819
+ const [presetName, ...presetOptions] = rest;
820
+ if (!presetName) {
821
+ console.log(`${colors.yellow}Usage: winter mcp preset <chrome-devtools> [--isolated] [--headless] [--browser-url <url>]${colors.reset}`);
822
+ break;
823
+ }
824
+
825
+ try {
826
+ const server = getMcpPreset(presetName, presetOptions);
827
+ upsertMcpServer(config, server);
828
+ await this.config.save(config);
829
+ console.log(`${colors.green}OK Installed MCP preset: ${server.name}${colors.reset}`);
830
+ console.log(` ${colors.dim}${server.command} ${server.args.join(' ')}${colors.reset}`);
831
+ if (!server.args.includes('--headless')) {
832
+ console.log(` ${colors.dim}Visible Chrome mode: enabled. Use --headless only for background browser runs.${colors.reset}`);
833
+ }
834
+ console.log(` ${colors.dim}Inspect tools with: winter mcp tools ${server.name}${colors.reset}`);
835
+ } catch (error) {
836
+ console.log(`${colors.red}${error.message}${colors.reset}`);
837
+ }
838
+ break;
839
+ }
816
840
  case 'remove': {
817
841
  const name = rest[0];
818
842
  if (!name) {
@@ -868,7 +892,7 @@ EXECUTION CONTRACT:
868
892
  break;
869
893
  }
870
894
  default:
871
- console.log(`${colors.yellow}Usage: winter mcp <list|add|remove|allow|tools>${colors.reset}`);
895
+ console.log(`${colors.yellow}Usage: winter mcp <list|add|preset|install|remove|allow|tools>${colors.reset}`);
872
896
  }
873
897
  }
874
898
 
package/src/cli/config.js CHANGED
@@ -7,6 +7,7 @@ import { promises as fs } from 'fs';
7
7
  import path from 'path';
8
8
  import { homedir } from 'os';
9
9
  import { loadEnvFile, stripInlineSecrets } from './secret-env.js';
10
+ import { CHROME_DEVTOOLS_MCP_NAME, createChromeDevtoolsMcpServer } from '../mcp/presets.js';
10
11
 
11
12
  export class ConfigLoader {
12
13
  constructor() {
@@ -61,13 +62,13 @@ export class ConfigLoader {
61
62
  permissions: {
62
63
  promptByDefault: true,
63
64
  allowlist: {
64
- tools: ['Read', 'Glob', 'Grep', 'LSP', 'TaskCreate', 'TaskUpdate', 'TaskList', 'WebFetch', 'WebSearch', 'Parallel'],
65
+ tools: ['Read', 'Glob', 'Grep', 'LSP', 'TaskCreate', 'TaskUpdate', 'TaskList', 'WebFetch', 'WebSearch', 'Parallel', 'MCP'],
65
66
  commands: [],
66
- mcpServers: [],
67
+ mcpServers: [CHROME_DEVTOOLS_MCP_NAME],
67
68
  },
68
69
  },
69
70
  mcp: {
70
- servers: [],
71
+ servers: [createChromeDevtoolsMcpServer(['--isolated'])],
71
72
  },
72
73
  routing: {
73
74
  strategy: 'heuristic',
@@ -117,12 +117,14 @@ export class PromptBuilder {
117
117
  `Prefer Read/Grep/Glob before editing. Use Write/Edit for file changes.`,
118
118
  `CRITICAL: When the user asks you to fix/create/edit/run/modify anything, you MUST call tools (Read, Write, Edit, Bash, etc.) to actually do it. NEVER just write code in a markdown code block and claim it is done. Winter will detect and block fake completions. If you say "đã sửa/đã tạo/done/fixed" without a tool call, your response will be rejected.`,
119
119
  `Tool call compatibility: if native tool calls are unavailable, output exactly one of these forms and no prose: <invoke name="Read"><parameter name="path">README.md</parameter></invoke> OR {"tool":"Read","arguments":{"path":"README.md"}} OR CALL_TOOL Read {"path":"README.md"}.`,
120
- `Browser capability: You CAN browse URLs! Use WebFetch to fetch page content (text extraction) or BrowserDebug for Chrome automation (JS rendering, screenshots). If user shares a URL or asks to view a website, use these tools automatically.`,
120
+ `Open browser requests: if the user asks to "mở chrome", "open Chrome", or open a URL in a visible browser, use OpenBrowser. Do NOT use Bash, Get-Command, Start-Process, cmd start, or shell app launch commands for this.`,
121
+ `Browser capability: You CAN browse URLs! Use WebFetch only for static page text extraction. For live Chrome debugging and visible browser control, prefer MCP server "chrome-devtools" when configured: use MCP tool "new_page" or "navigate_page", then "take_snapshot", "click", "fill"/"fill_form", "take_screenshot", "evaluate_script", "list_console_messages", "list_network_requests", or performance trace tools. BrowserDebug is headless fallback only when chrome-devtools MCP is unavailable or the user explicitly asks for headless smoke/debug.`,
122
+ `Browser interaction rule: if the user asks to click, press, fill, select, submit, open a web app path, or inspect page-by-page data, WebFetch is not enough and BrowserDebug is not user-visible. Use chrome-devtools MCP so the user can watch the normal Chrome client. Never claim "đã bấm/đã điền/đã mở/đã kiểm tra" from prose alone.`,
121
123
  `When a task touches coding, agents, UI, brand, or design, inspect the relevant required local resource in depth before deciding.`,
122
124
  `If the user asks you to modify, run, inspect, check, publish, commit, or otherwise act on the project, you MUST use tools. Do not claim completion without a tool result from this turn.`,
123
125
  ``,
124
126
  `## Debug Excellence`,
125
- `For bugs, crashes, test failures, or "not working": identify the first hard failure, reproduce or inspect logs, trace the exact runtime path, patch the smallest root cause, and verify with the closest command. For frontend/runtime UI issues, use BrowserDebug when a URL or dev server is available.`,
127
+ `For bugs, crashes, test failures, or "not working": identify the first hard failure, reproduce or inspect logs, trace the exact runtime path, patch the smallest root cause, and verify with the closest command. For frontend/runtime UI issues with a URL/dev server, prefer chrome-devtools MCP in visible Chrome; use BrowserDebug only as a headless fallback.`,
126
128
  ``,
127
129
  `## Design Excellence`,
128
130
  `For UI/design work: inspect existing components/styles and any design resources first. Build a polished, responsive, domain-appropriate interface with complete states and clear interactions. Avoid generic placeholders, fake controls, one-note palettes, and unverified visual claims.`,
@@ -193,7 +195,7 @@ export class PromptBuilder {
193
195
  review: 'You are a Winter review subagent. Critique the request or implementation with specific issues, edge cases, and concrete improvements.',
194
196
  debug: 'You are a Winter debugging subagent. Reproduce or inspect the exact failing path, isolate the first hard blocker, patch the smallest root cause, and verify with the closest test/build/browser smoke.',
195
197
  research: 'You are a Winter research subagent. Gather the important facts, compare options, and summarize only what matters.',
196
- browser: `You are a Winter browser subagent. Bạn CÓ QUYỀN sử dụng tool 'BrowserDebug' để tương tác với trình duyệt. Hãy dùng nó để mở URL, chụp ảnh màn hình (nếu cần), hoặc chạy JS để kiểm tra trang web.`,
198
+ browser: `You are a Winter browser subagent. Bạn CÓ QUYỀN sử dụng chrome-devtools MCP để thao tác Chrome visible cho user xem: mở URL, click, fill form, snapshot, screenshot, đọc console/network. Dùng BrowserDebug chỉ khi cần headless fallback.`,
197
199
  };
198
200
 
199
201
  const rolePrompt = rolePrompts[role] || 'You are a Winter coding subagent. Solve the task directly, use tools when needed, and return a concise result.';
@@ -208,14 +210,14 @@ export class PromptBuilder {
208
210
  '3. [DEBUG EXCELLENCE]: Reproduce or inspect the failing path first, isolate the first hard blocker, patch root cause, and verify with the closest test/build/browser smoke.',
209
211
  '4. [DESIGN EXCELLENCE]: Use rich aesthetics. Default to modern UI frameworks if applicable. Never output plain, ugly HTML/CSS. Ensure responsive, premium feel with micro-animations.',
210
212
  '5. [CODE QUALITY]: Write clean, modular, SOLID code. Check for syntax errors carefully. Do not generate incomplete code blocks.',
211
- '6. [NO HALLUCINATION]: If you don\'t know, use tools (Grep/Read/Web/BrowserDebug) to find out. Do not guess file paths or APIs.',
213
+ '6. [NO HALLUCINATION]: If you don\'t know, use tools (Grep/Read/Web/chrome-devtools MCP/BrowserDebug) to find out. Do not guess file paths or APIs.',
212
214
  '7. [TOOL EXECUTION FIRST]: You DO have file tools. Use Write to create/overwrite files and Edit to patch files. Never say there is no write tool.',
213
215
  '8. [IMAGE INPUTS]: If an image is attached or pasted, analyze it directly and use it as evidence for UI/debug/design decisions.',
214
216
  '',
215
217
  rolePrompt,
216
218
  '',
217
219
  '## Tool Rules',
218
- '- Canonical tools: Read, Write, Edit, Bash, Glob, Grep, TaskCreate, TaskUpdate, TaskList, BrowserDebug, WebFetch, WebSearch.',
220
+ '- Canonical tools: Read, Write, Edit, Bash, Glob, Grep, TaskCreate, TaskUpdate, TaskList, OpenBrowser, BrowserDebug, WebFetch, WebSearch, MCP.',
219
221
  '- If native tool calls are unavailable, output exactly one fallback tool call and no prose: <invoke name="Read"><parameter name="path">README.md</parameter></invoke> OR {"tool":"Read","arguments":{"path":"README.md"}} OR CALL_TOOL Read {"path":"README.md"}.',
220
222
  '- Treat skills, memories, bundled resources, local project rules, and the tool list as operational context. Use them proactively when relevant.',
221
223
  `- Runtime environment:\n${runtimeSummary}`,
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
 
@@ -1783,6 +1876,8 @@ ${colors.reset}
1783
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;
1784
1877
 
1785
1878
  if (pureQuestionPattern.test(text) && !actionPattern.test(text)) return false;
1879
+
1880
+ if (this.isBrowserInteractionRequest(rawText)) return true;
1786
1881
 
1787
1882
  // Even without explicit target, some verbs are strong enough on their own
1788
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;
@@ -1812,6 +1907,15 @@ ${colors.reset}
1812
1907
  return true;
1813
1908
  }
1814
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
+
1815
1919
  detectFakeCompletion(content = '') {
1816
1920
  const text = String(content || '').toLowerCase();
1817
1921
  if (!text.trim()) return false;
@@ -1820,6 +1924,9 @@ ${colors.reset}
1820
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;
1821
1925
  if (fakeCompletionClaims.test(text)) return true;
1822
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
+
1823
1930
  // Detect code blocks that pretend to show "changes" without tool use
1824
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;
1825
1932
  const claimsFileChange = /(?:here(?:'s| is)|below|sau đây|dưới đây|như sau|updated|modified|changed|new|fixed)/i;
@@ -1830,6 +1937,19 @@ ${colors.reset}
1830
1937
 
1831
1938
  buildToolEvidenceCorrection(messages = []) {
1832
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
+ }
1833
1953
  return [
1834
1954
  '⚠️ RUNTIME ENFORCEMENT: Your previous response was BLOCKED because you did not use any tool.',
1835
1955
  '',
@@ -1842,7 +1962,7 @@ ${colors.reset}
1842
1962
  'DO NOT say "I have updated/created/fixed" without a tool call proving it.',
1843
1963
  'DO NOT describe what you would do. Actually DO IT with tool calls.',
1844
1964
  '',
1845
- '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.',
1846
1966
  '',
1847
1967
  'If native tool calls are not supported, output exactly one fallback tool call:',
1848
1968
  '<invoke name="Read"><parameter name="path">README.md</parameter></invoke>',
@@ -1864,6 +1984,10 @@ ${colors.reset}
1864
1984
 
1865
1985
  const hints = [];
1866
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
+
1867
1991
  // Detect file reading requests
1868
1992
  const hasPath = /[A-Za-z]:[\\/][\w.\\/\\-]+/i.test(text) || /(?:^|\s)[.~]?\/[\w.\/-]+/i.test(text);
1869
1993
  const readVerbs = /\b(đọc|doc|read|xem|view|mở|open|show|hiện|hiển thị|cat|type)\b/i;
@@ -1902,7 +2026,11 @@ ${colors.reset}
1902
2026
 
1903
2027
  // Detect URL/web requests
1904
2028
  if (/\b(https?:\/\/[^\s]+|url|website|trang web|web page)\b/i.test(text)) {
1905
- 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.');
1906
2034
  }
1907
2035
 
1908
2036
  if (hints.length === 0) return null;
@@ -2244,6 +2372,9 @@ ${colors.reset}
2244
2372
 
2245
2373
  async requestFinalAnswer(messages, toolSummaries, startedAt, totalUsage) {
2246
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));
2247
2378
  const finalMessages = [
2248
2379
  ...messages,
2249
2380
  {
@@ -2256,6 +2387,7 @@ ${colors.reset}
2256
2387
  'Start with the actual outcome, then mention only the most relevant files/commands. Avoid broad generic advice.',
2257
2388
  'If a tool failed, explain the concrete failure briefly and answer with the available evidence.',
2258
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.' : '',
2259
2391
  toolSummaries.length ? `Tool summary:\n${toolSummaries.join('\n')}` : '',
2260
2392
  ].filter(Boolean).join('\n'),
2261
2393
  },
@@ -2268,7 +2400,7 @@ ${colors.reset}
2268
2400
  }
2269
2401
 
2270
2402
  if (typeof this.ai.streamRequest === 'function') {
2271
- return await this.streamFinalAnswer(finalMessages, startedAt, totalUsage, executionProfile);
2403
+ return await this.streamFinalAnswer(finalMessages, startedAt, totalUsage, executionProfile, { browserInteraction, hasBrowserEvidence });
2272
2404
  }
2273
2405
 
2274
2406
  const response = await this.ai.sendRequest(finalMessages, {
@@ -2278,7 +2410,10 @@ ${colors.reset}
2278
2410
  signal: this.currentAbortController?.signal,
2279
2411
  });
2280
2412
  this.addUsage(totalUsage, response.usage);
2281
- 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
+ }
2282
2417
 
2283
2418
  if (this.spinner) this.spinner.stop();
2284
2419
 
@@ -2295,7 +2430,7 @@ ${colors.reset}
2295
2430
  }
2296
2431
  }
2297
2432
 
2298
- async streamFinalAnswer(messages, startedAt, totalUsage, executionProfile = null) {
2433
+ async streamFinalAnswer(messages, startedAt, totalUsage, executionProfile = null, validation = {}) {
2299
2434
  let content = '';
2300
2435
  const profile = executionProfile || this.selectExecutionProfile(messages, { enableTools: false });
2301
2436
 
@@ -2321,6 +2456,10 @@ ${colors.reset}
2321
2456
 
2322
2457
  if (this.spinner) this.spinner.stop();
2323
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
+
2324
2463
  if (content) {
2325
2464
  this.printAssistantAnswer(content, startedAt, totalUsage);
2326
2465
  return content;
@@ -2341,6 +2480,9 @@ ${colors.reset}
2341
2480
  });
2342
2481
  this.addUsage(totalUsage, response.usage);
2343
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
+ }
2344
2486
  if (content) {
2345
2487
  this.printAssistantAnswer(content, startedAt, totalUsage);
2346
2488
  }
@@ -3378,6 +3520,8 @@ Light mode enabled for safety. Heavy codebase, graph, and git context are skippe
3378
3520
  const [action, ...rest] = args;
3379
3521
  const config = await this.config.load();
3380
3522
  config.mcp = config.mcp || { servers: [] };
3523
+ config.permissions = config.permissions || { allowlist: {} };
3524
+ config.permissions.allowlist = config.permissions.allowlist || { tools: [], commands: [], mcpServers: [] };
3381
3525
 
3382
3526
  switch (action) {
3383
3527
  case undefined:
@@ -3410,10 +3554,33 @@ Light mode enabled for safety. Heavy codebase, graph, and git context are skippe
3410
3554
  }
3411
3555
  config.mcp.servers = (config.mcp.servers || []).filter(server => server.name !== name);
3412
3556
  config.mcp.servers.push({ name, command, args: parsedArgs, enabled: true });
3557
+ config.permissions.allowlist.mcpServers = [...new Set([...(config.permissions.allowlist.mcpServers || []), name])];
3413
3558
  await this.config.save(config);
3414
3559
  console.log(`${colors.green}✓ Added MCP server: ${name}${colors.reset}`);
3415
3560
  break;
3416
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
+ }
3417
3584
  case 'remove': {
3418
3585
  const name = rest[0];
3419
3586
  if (!name) {
@@ -3421,6 +3588,7 @@ Light mode enabled for safety. Heavy codebase, graph, and git context are skippe
3421
3588
  break;
3422
3589
  }
3423
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);
3424
3592
  await this.config.save(config);
3425
3593
  console.log(`${colors.green}✓ Removed MCP server: ${name}${colors.reset}`);
3426
3594
  break;
@@ -3435,8 +3603,37 @@ Light mode enabled for safety. Heavy codebase, graph, and git context are skippe
3435
3603
  console.log(`${colors.green}✓ MCP server allowed: ${name}${colors.reset}`);
3436
3604
  break;
3437
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
+ }
3438
3635
  default:
3439
- 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}`);
3440
3637
  }
3441
3638
  }
3442
3639
 
@@ -41,7 +41,7 @@ export const SLASH_COMMANDS = [
41
41
  { cmd: '/skill', desc: 'Skills management', sub: ['list', 'enable', 'create'] },
42
42
  { cmd: '/skills', desc: 'List local Winter/Codex/Claude skills' },
43
43
  { cmd: '/plugin', desc: 'Plugin management', sub: ['list', 'install', 'remove'] },
44
- { 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'] },
45
45
  { cmd: '/permissions', desc: 'Permission allowlist', sub: ['list', 'allow', 'prompt'] },
46
46
  { cmd: '/stats', desc: 'Tool usage statistics' },
47
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 => {
@@ -0,0 +1,114 @@
1
+ export const CHROME_DEVTOOLS_MCP_NAME = 'chrome-devtools';
2
+
3
+ const CHROME_DEVTOOLS_PACKAGE = 'chrome-devtools-mcp@latest';
4
+ const CHROME_DEVTOOLS_SOURCE = 'https://github.com/ChromeDevTools/chrome-devtools-mcp';
5
+
6
+ const CHROME_DEVTOOLS_FLAGS_WITH_VALUES = new Set([
7
+ '--browser-url',
8
+ '--channel',
9
+ '--executablePath',
10
+ '--logFile',
11
+ '--viewport',
12
+ '--proxy-server',
13
+ ]);
14
+
15
+ const CHROME_DEVTOOLS_BOOLEAN_FLAGS = new Set([
16
+ '--headless',
17
+ '--isolated',
18
+ '--acceptInsecureCerts',
19
+ '--help',
20
+ ]);
21
+
22
+ export function normalizeMcpPresetName(name = '') {
23
+ return String(name || '').trim().toLowerCase();
24
+ }
25
+
26
+ export function isChromeDevtoolsPreset(name = '') {
27
+ return ['chrome-devtools', 'chromedevtools', 'chrome', 'devtools', 'cdp'].includes(normalizeMcpPresetName(name));
28
+ }
29
+
30
+ export function buildChromeDevtoolsArgs(options = []) {
31
+ const input = Array.isArray(options) ? [...options] : [];
32
+ const args = ['-y', CHROME_DEVTOOLS_PACKAGE];
33
+
34
+ for (let index = 0; index < input.length; index += 1) {
35
+ const flag = input[index];
36
+ if (!String(flag || '').startsWith('--')) continue;
37
+
38
+ if (CHROME_DEVTOOLS_BOOLEAN_FLAGS.has(flag)) {
39
+ args.push(flag);
40
+ continue;
41
+ }
42
+
43
+ if (CHROME_DEVTOOLS_FLAGS_WITH_VALUES.has(flag)) {
44
+ const value = input[index + 1];
45
+ if (value === undefined || String(value).startsWith('--')) {
46
+ throw new Error(`Missing value for ${flag}`);
47
+ }
48
+ args.push(flag, String(value));
49
+ index += 1;
50
+ }
51
+ }
52
+
53
+ return args;
54
+ }
55
+
56
+ export function createChromeDevtoolsMcpServer(options = [], platform = process.platform, env = process.env) {
57
+ const npxArgs = buildChromeDevtoolsArgs(options);
58
+ const common = {
59
+ name: CHROME_DEVTOOLS_MCP_NAME,
60
+ enabled: true,
61
+ requestTimeoutMs: 60000,
62
+ metadata: {
63
+ preset: CHROME_DEVTOOLS_MCP_NAME,
64
+ source: CHROME_DEVTOOLS_SOURCE,
65
+ purpose: 'Chrome DevTools MCP for live browser automation, debugging, screenshots, console, network, and performance traces.',
66
+ },
67
+ };
68
+
69
+ if (platform === 'win32') {
70
+ return {
71
+ ...common,
72
+ command: 'cmd',
73
+ args: ['/c', 'npx', ...npxArgs],
74
+ env: {
75
+ SystemRoot: env.SystemRoot || 'C:\\Windows',
76
+ PROGRAMFILES: env.PROGRAMFILES || 'C:\\Program Files',
77
+ },
78
+ };
79
+ }
80
+
81
+ return {
82
+ ...common,
83
+ command: 'npx',
84
+ args: npxArgs,
85
+ };
86
+ }
87
+
88
+ export function getMcpPreset(name, options = []) {
89
+ if (isChromeDevtoolsPreset(name)) {
90
+ return createChromeDevtoolsMcpServer(options);
91
+ }
92
+ throw new Error(`Unknown MCP preset: ${name}`);
93
+ }
94
+
95
+ export function ensureMcpConfigShape(config = {}) {
96
+ config.mcp = config.mcp || { servers: [] };
97
+ config.mcp.servers = Array.isArray(config.mcp.servers) ? config.mcp.servers : [];
98
+ config.permissions = config.permissions || { allowlist: {} };
99
+ config.permissions.allowlist = config.permissions.allowlist || {};
100
+ config.permissions.allowlist.tools = config.permissions.allowlist.tools || [];
101
+ config.permissions.allowlist.commands = config.permissions.allowlist.commands || [];
102
+ config.permissions.allowlist.mcpServers = config.permissions.allowlist.mcpServers || [];
103
+ return config;
104
+ }
105
+
106
+ export function upsertMcpServer(config, server) {
107
+ ensureMcpConfigShape(config);
108
+ config.mcp.servers = config.mcp.servers.filter(item => item.name !== server.name);
109
+ config.mcp.servers.push(server);
110
+ config.permissions.allowlist.mcpServers = [
111
+ ...new Set([...(config.permissions.allowlist.mcpServers || []), server.name]),
112
+ ];
113
+ return config;
114
+ }
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { promises as fs } from 'fs';
7
7
  import path from 'path';
8
- import { exec, execFile } from 'child_process';
8
+ import { exec, execFile, spawn } from 'child_process';
9
9
  import { promisify } from 'util';
10
10
  import { diffLines } from 'diff';
11
11
  import { withRetry } from './retry.js';
@@ -207,11 +207,11 @@ export class ToolExecutor {
207
207
  {
208
208
  type: 'function',
209
209
  name: 'MCP',
210
- description: 'Call a configured MCP server tool by name. Use for external integrations and IDE-like tools. Discover available MCP tools via the MCP tool with server name and tool=list. Also, tools from MCP servers are exposed with mcp__<server>__<tool> naming for direct IDE integration (e.g. mcp__vscode__open_file).',
210
+ description: 'Call a configured MCP server tool by name. Use for external integrations and IDE-like tools. Discover available MCP tools via the MCP tool with server name and tool=list. For live Chrome debugging, use server chrome-devtools with tools such as new_page, navigate_page, take_snapshot, take_screenshot, evaluate_script, list_console_messages, list_network_requests, and performance trace tools. Also, tools from MCP servers are exposed with mcp__<server>__<tool> naming for direct IDE integration (e.g. mcp__vscode__open_file).',
211
211
  parameters: {
212
212
  type: 'object',
213
213
  properties: {
214
- server: { type: 'string', description: 'Configured MCP server name (e.g. vscode)' },
214
+ server: { type: 'string', description: 'Configured MCP server name (e.g. vscode, chrome-devtools)' },
215
215
  tool: { type: 'string', description: 'MCP tool name, or set to "list" to discover all tools from a server' },
216
216
  arguments: { type: 'object', description: 'Tool arguments' },
217
217
  },
@@ -388,6 +388,18 @@ export class ToolExecutor {
388
388
  required: ['url']
389
389
  }
390
390
  },
391
+ {
392
+ type: 'function',
393
+ name: 'OpenBrowser',
394
+ description: 'Open Chrome or the default browser visibly for the user. Use this for requests like "mở chrome", "open Chrome", or "open this URL in browser". Do not use Bash/Start-Process for this.',
395
+ parameters: {
396
+ type: 'object',
397
+ properties: {
398
+ url: { type: 'string', description: 'URL to open. Defaults to about:blank.' },
399
+ browser: { type: 'string', description: 'chrome or default. Defaults to chrome.' },
400
+ }
401
+ }
402
+ },
391
403
  {
392
404
  type: 'function',
393
405
  name: 'WebFetch',
@@ -532,6 +544,8 @@ export class ToolExecutor {
532
544
  return await this.parallelExecute(input.tools ?? input.calls ?? [], { cwd });
533
545
  case 'BrowserDebug':
534
546
  return await this.browserDebug(input.url ?? input.uri, input.action);
547
+ case 'OpenBrowser':
548
+ return await this.openBrowser(input.url ?? input.uri ?? input.href, input.browser);
535
549
  case 'WebFetch':
536
550
  return await this.webFetch(input.url ?? input.uri ?? input.href, input.prompt ?? input.query ?? input.extract);
537
551
  case 'WebSearch':
@@ -572,7 +586,7 @@ export class ToolExecutor {
572
586
  return {
573
587
  success: false,
574
588
  error: `Unknown tool: ${toolName}`,
575
- availableTools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'TaskCreate', 'TaskUpdate', 'TaskList', 'MCP', 'Parallel', 'BrowserDebug', 'WebFetch', 'WebSearch', 'WebArchive', 'HtmlEffectiveness', 'NotebookRead', 'NotebookEdit', 'TodoWrite', 'TodoList', 'ScheduleWakeup', 'AskUserQuestion', 'Agent', 'InsertText', 'StrReplaceAll'],
589
+ availableTools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'TaskCreate', 'TaskUpdate', 'TaskList', 'MCP', 'Parallel', 'OpenBrowser', 'BrowserDebug', 'WebFetch', 'WebSearch', 'WebArchive', 'HtmlEffectiveness', 'NotebookRead', 'NotebookEdit', 'TodoWrite', 'TodoList', 'ScheduleWakeup', 'AskUserQuestion', 'Agent', 'InsertText', 'StrReplaceAll'],
576
590
  recovery: 'Call one of the available tools. For file writes use Write with { "file_path": "...", "content": "..." }. For shell commands use Bash with { "command": "..." }.',
577
591
  };
578
592
  }
@@ -725,6 +739,12 @@ export class ToolExecutor {
725
739
  return { success: true, coerced: true, args: next };
726
740
  }
727
741
 
742
+ if (toolName === 'OpenBrowser') {
743
+ const url = pick('url', 'uri', 'href') || 'about:blank';
744
+ const browser = pick('browser', 'app') || 'chrome';
745
+ return { success: true, coerced: true, args: { ...args, url, browser } };
746
+ }
747
+
728
748
  if (toolName === 'WebSearch') {
729
749
  const query = pick('query', 'q', 'search', 'search_query', 'searchQuery');
730
750
  if (!query) {
@@ -752,6 +772,14 @@ export class ToolExecutor {
752
772
  const baseCommand = this.getBaseCommand(text);
753
773
  if (!baseCommand) return { success: true };
754
774
 
775
+ if (/^(?:get-command|start-process|start|open|xdg-open)$/i.test(baseCommand) && /\b(chrome|browser|google chrome)\b/i.test(text)) {
776
+ return {
777
+ success: false,
778
+ error: `Use OpenBrowser instead of shell command for browser launch: ${baseCommand}`,
779
+ recovery: 'Call OpenBrowser {"browser":"chrome","url":"about:blank"} for "mở chrome", or OpenBrowser {"browser":"chrome","url":"https://example.com"} for a specific URL.',
780
+ };
781
+ }
782
+
755
783
  const cfg = await this.getRuntimeConfig();
756
784
  const permissionCommands = cfg.permissions?.allowlist?.commands || [];
757
785
  const sandbox = cfg.sandbox || {};
@@ -974,6 +1002,11 @@ export class ToolExecutor {
974
1002
  searchweb: 'WebSearch',
975
1003
  internetsearch: 'WebSearch',
976
1004
  googlesearch: 'WebSearch',
1005
+ openbrowser: 'OpenBrowser',
1006
+ open_browser: 'OpenBrowser',
1007
+ browseropen: 'OpenBrowser',
1008
+ openchrome: 'OpenBrowser',
1009
+ launchchrome: 'OpenBrowser',
977
1010
  browserdebug: 'BrowserDebug',
978
1011
  browser: 'BrowserDebug',
979
1012
  browserinspect: 'BrowserDebug',
@@ -2076,6 +2109,72 @@ export class ToolExecutor {
2076
2109
  }
2077
2110
  }
2078
2111
 
2112
+ buildBrowserLaunchCommand(url = 'about:blank', browser = 'chrome', platform = process.platform) {
2113
+ const targetUrl = String(url || 'about:blank');
2114
+ const targetBrowser = String(browser || 'chrome').toLowerCase();
2115
+
2116
+ if (platform === 'win32') {
2117
+ if (targetBrowser === 'default') {
2118
+ return { command: 'cmd', args: ['/c', 'start', '', targetUrl] };
2119
+ }
2120
+ return { command: 'cmd', args: ['/c', 'start', '', 'chrome', targetUrl] };
2121
+ }
2122
+
2123
+ if (platform === 'darwin') {
2124
+ if (targetBrowser === 'default') {
2125
+ return { command: 'open', args: [targetUrl] };
2126
+ }
2127
+ return { command: 'open', args: ['-a', 'Google Chrome', targetUrl] };
2128
+ }
2129
+
2130
+ if (targetBrowser === 'default') {
2131
+ return { command: 'xdg-open', args: [targetUrl] };
2132
+ }
2133
+ return { command: 'google-chrome', args: [targetUrl] };
2134
+ }
2135
+
2136
+ async openBrowser(url = 'about:blank', browser = 'chrome') {
2137
+ const targetUrl = String(url || 'about:blank');
2138
+ const targetBrowser = String(browser || 'chrome').toLowerCase();
2139
+ const launch = this.buildBrowserLaunchCommand(targetUrl, targetBrowser);
2140
+
2141
+ return await new Promise(resolve => {
2142
+ let child;
2143
+ try {
2144
+ child = spawn(launch.command, launch.args, {
2145
+ detached: true,
2146
+ stdio: 'ignore',
2147
+ windowsHide: false,
2148
+ });
2149
+ } catch (error) {
2150
+ resolve({
2151
+ success: false,
2152
+ error: error.message,
2153
+ recovery: 'Install Chrome or retry with OpenBrowser {"browser":"default","url":"about:blank"}.',
2154
+ });
2155
+ return;
2156
+ }
2157
+
2158
+ child.once('error', error => {
2159
+ resolve({
2160
+ success: false,
2161
+ error: error.message,
2162
+ recovery: 'Install Chrome or retry with OpenBrowser {"browser":"default","url":"about:blank"}.',
2163
+ });
2164
+ });
2165
+ child.once('spawn', () => {
2166
+ child.unref();
2167
+ resolve({
2168
+ success: true,
2169
+ browser: targetBrowser,
2170
+ url: targetUrl,
2171
+ command: launch.command,
2172
+ args: launch.args,
2173
+ });
2174
+ });
2175
+ });
2176
+ }
2177
+
2079
2178
  async htmlEffectivenessCompile(input, cwd) {
2080
2179
  const inputPath = this.resolveInputPath(input.input_path ?? input.inputPath ?? input.input, cwd);
2081
2180
  const outputPath = this.resolveInputPath(input.output_path ?? input.outputPath ?? input.output, cwd);