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 +19 -0
- package/package.json +1 -1
- package/src/agent/agent-definitions.js +4 -4
- package/src/ai/prompts/system-prompt.js +2 -0
- package/src/cli/commands.js +25 -1
- package/src/cli/config.js +4 -3
- package/src/cli/prompt-builder.js +7 -5
- package/src/cli/repl.js +207 -10
- package/src/cli/slash-commands.js +1 -1
- package/src/mcp/client.js +8 -6
- package/src/mcp/presets.js +114 -0
- package/src/tools/executor.js +103 -4
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
|
@@ -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
|
'',
|
package/src/cli/commands.js
CHANGED
|
@@ -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
|
-
`
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 => {
|
|
@@ -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
|
+
}
|
package/src/tools/executor.js
CHANGED
|
@@ -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);
|