@wong2kim/wmux 1.1.1 → 2.0.0
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 +76 -27
- package/dist/cli/cli/commands/browser.js +101 -77
- package/dist/cli/cli/index.js +6 -6
- package/dist/cli/shared/constants.js +3 -0
- package/dist/cli/shared/rpc.js +21 -4
- package/dist/cli/shared/types.js +108 -4
- package/dist/mcp/mcp/index.js +41 -21
- package/dist/mcp/mcp/playwright/PlaywrightEngine.js +293 -0
- package/dist/mcp/mcp/playwright/anti-detection.js +63 -0
- package/dist/mcp/mcp/playwright/dom-intelligence.js +171 -0
- package/dist/mcp/mcp/playwright/human-typing.js +48 -0
- package/dist/mcp/mcp/playwright/markdown-extractor.js +520 -0
- package/dist/mcp/mcp/playwright/security.js +29 -0
- package/dist/mcp/mcp/playwright/snapshot.js +261 -0
- package/dist/mcp/mcp/playwright/tools/extraction.js +143 -0
- package/dist/mcp/mcp/playwright/tools/file.js +274 -0
- package/dist/mcp/mcp/playwright/tools/inspection.js +459 -0
- package/dist/mcp/mcp/playwright/tools/interaction.js +457 -0
- package/dist/mcp/mcp/playwright/tools/navigation.js +206 -0
- package/dist/mcp/mcp/playwright/tools/state.js +410 -0
- package/dist/mcp/mcp/playwright/tools/utility.js +167 -0
- package/dist/mcp/mcp/playwright/tools/wait.js +119 -0
- package/dist/mcp/shared/constants.js +3 -0
- package/dist/mcp/shared/rpc.js +21 -4
- package/dist/mcp/shared/types.js +108 -4
- package/package.json +11 -7
package/README.md
CHANGED
|
@@ -2,25 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
**AI Agent Terminal for Windows**
|
|
4
4
|
|
|
5
|
-
Run Claude Code, Codex, Gemini CLI side by side — with built-in browser, smart notifications, and MCP integration.
|
|
5
|
+
Run Claude Code, Codex, Gemini CLI side by side — with built-in browser automation, smart notifications, and MCP integration.
|
|
6
6
|
|
|
7
7
|
Inspired by [cmux](https://github.com/manaflow-ai/cmux) (macOS), wmux brings the same philosophy to Windows: **a primitive, not a solution.** Composable building blocks for multi-agent workflows.
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|

|
|
11
|
+

|
|
11
12
|

|
|
12
13
|
|
|
13
14
|
---
|
|
14
15
|
|
|
15
16
|
## Install
|
|
16
17
|
|
|
17
|
-
**Download:** [wmux-
|
|
18
|
+
**Download:** [wmux-2.0.0 Setup.exe](https://github.com/openwong2kim/wmux/releases/latest)
|
|
18
19
|
|
|
19
20
|
Or build from source:
|
|
20
21
|
```powershell
|
|
21
22
|
irm https://raw.githubusercontent.com/openwong2kim/wmux/main/install.ps1 | iex
|
|
22
23
|
```
|
|
23
24
|
|
|
25
|
+
**npm (CLI + MCP server only):**
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g @wong2kim/wmux
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## What's New in v2.0.0
|
|
33
|
+
|
|
34
|
+
- **Browser automation via CDP** — Click, fill, type, screenshot directly through Chrome DevTools Protocol. Works with React inputs, CJK text, and controlled components.
|
|
35
|
+
- **Security hardening** — Token auth on all pipes, SSRF protection, input sanitization, randomized CDP ports, memory pressure watchdog.
|
|
36
|
+
- **Workspace reset** — One-click reset in Settings to clean all workspaces and start fresh.
|
|
37
|
+
- **Daemon process** — Background session management with suspend/resume, scrollback persistence, and auto-recovery.
|
|
38
|
+
|
|
24
39
|
---
|
|
25
40
|
|
|
26
41
|
## Why wmux?
|
|
@@ -28,7 +43,7 @@ irm https://raw.githubusercontent.com/openwong2kim/wmux/main/install.ps1 | iex
|
|
|
28
43
|
| Problem | wmux |
|
|
29
44
|
|---------|------|
|
|
30
45
|
| Windows has no cmux | Native Windows terminal multiplexer for AI agents |
|
|
31
|
-
| Agents can't
|
|
46
|
+
| Agents can't control the browser | Built-in browser with CDP — Claude clicks, fills, types, screenshots |
|
|
32
47
|
| "Is it done yet?" | Smart activity-based notifications + taskbar flash |
|
|
33
48
|
| Can't compare agents | Multiview — Ctrl+click workspaces to view side by side |
|
|
34
49
|
| Hard to describe UI elements to LLM | Inspector — click any element, LLM-friendly context copied |
|
|
@@ -45,27 +60,25 @@ irm https://raw.githubusercontent.com/openwong2kim/wmux/main/install.ps1 | iex
|
|
|
45
60
|
- **Vi copy mode** — `Ctrl+Shift+X`
|
|
46
61
|
- **Search** — `Ctrl+F`
|
|
47
62
|
- **Unlimited scrollback** — 999,999 lines default
|
|
63
|
+
- **Scrollback persistence** — terminal content saved to disk, restored on restart
|
|
48
64
|
|
|
49
65
|
### Workspaces
|
|
50
66
|
- Sidebar with drag-and-drop reordering
|
|
51
67
|
- `Ctrl+1` ~ `Ctrl+9` quick switch
|
|
52
68
|
- **Multiview** — `Ctrl+click` workspaces to split-view them simultaneously
|
|
53
|
-
-
|
|
54
|
-
-
|
|
69
|
+
- **Session persistence** — workspace layout, tabs, cwd, and terminal scrollback all restored on restart
|
|
70
|
+
- **One-click reset** — Settings > General > Reset to clean all workspaces
|
|
55
71
|
|
|
56
|
-
### Browser
|
|
72
|
+
### Browser + CDP Automation
|
|
57
73
|
- Built-in browser panel — `Ctrl+Shift+L`
|
|
58
74
|
- Navigation bar, DevTools, back/forward
|
|
59
|
-
- **Element Inspector** —
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
parent: div.RNNXgb > siblings: button"Google Search", button"I'm Feeling Lucky"
|
|
67
|
-
```
|
|
68
|
-
- Paste directly into Claude — it understands the element immediately
|
|
75
|
+
- **Element Inspector** — hover to highlight, click to copy LLM-friendly context
|
|
76
|
+
- **Full CDP automation via MCP:**
|
|
77
|
+
- Click elements by ref or CSS selector
|
|
78
|
+
- Fill forms with real keyboard input (handles React, CJK)
|
|
79
|
+
- Take screenshots via CDP `Page.captureScreenshot`
|
|
80
|
+
- Evaluate JavaScript with user gesture context
|
|
81
|
+
- Navigate, go back, press keys
|
|
69
82
|
|
|
70
83
|
### Notifications
|
|
71
84
|
- **Activity-based detection** — monitors output throughput, no fragile pattern matching
|
|
@@ -82,10 +95,16 @@ wmux automatically registers its MCP server when launched. Claude Code can:
|
|
|
82
95
|
|------|-------------|
|
|
83
96
|
| `browser_open` | Open a new browser panel |
|
|
84
97
|
| `browser_navigate` | Go to URL |
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
98
|
+
| `browser_screenshot` | Capture page as PNG (CDP) |
|
|
99
|
+
| `browser_snapshot` | Get page structure with interactive element refs |
|
|
100
|
+
| `browser_click` | Click element by ref number |
|
|
101
|
+
| `browser_fill` | Fill form fields by ref |
|
|
102
|
+
| `browser_type` | Type text into element (CDP keyboard input) |
|
|
103
|
+
| `browser_press_key` | Press keyboard key (Enter, Tab, etc.) |
|
|
104
|
+
| `browser_evaluate` | Execute JavaScript in page context |
|
|
105
|
+
| `browser_hover` | Hover over element |
|
|
106
|
+
| `browser_select` | Select dropdown options |
|
|
107
|
+
| `browser_scroll_into_view` | Scroll element into viewport |
|
|
89
108
|
| `terminal_read` | Read terminal screen |
|
|
90
109
|
| `terminal_send` | Send text to terminal |
|
|
91
110
|
| `terminal_send_key` | Send key (enter, ctrl+c, etc.) |
|
|
@@ -95,12 +114,26 @@ wmux automatically registers its MCP server when launched. Claude Code can:
|
|
|
95
114
|
|
|
96
115
|
**Multi-agent:** All browser tools accept `surfaceId` — each Claude Code session controls its own browser independently.
|
|
97
116
|
|
|
117
|
+
### Security
|
|
118
|
+
- **Token authentication** on all IPC pipes (named pipe + session pipes)
|
|
119
|
+
- **SSRF protection** — URL validation blocks private IPs, file://, javascript: schemes
|
|
120
|
+
- **Input sanitization** — PTY command injection prevention
|
|
121
|
+
- **CDP port randomization** — no fixed debug port
|
|
122
|
+
- **Memory pressure watchdog** — auto-reaps dead sessions at 750MB, blocks new at 1GB
|
|
123
|
+
- **Electron Fuses** — RunAsNode disabled, cookie encryption enabled
|
|
124
|
+
|
|
98
125
|
### Agent Status Detection
|
|
99
126
|
Gate-based detection for AI coding agents:
|
|
100
127
|
- Claude Code, Cursor, Aider, Codex CLI, Gemini CLI, OpenCode, GitHub Copilot CLI
|
|
101
|
-
- Detects agent startup
|
|
128
|
+
- Detects agent startup, monitors activity
|
|
102
129
|
- Critical action warnings (git push --force, rm -rf, DROP TABLE, etc.)
|
|
103
130
|
|
|
131
|
+
### Daemon Process
|
|
132
|
+
- Background session management (survives app restart)
|
|
133
|
+
- Suspend/resume with scrollback buffer dump
|
|
134
|
+
- Auto-recovery of sessions on daemon restart
|
|
135
|
+
- Dead session TTL reaping (24h default)
|
|
136
|
+
|
|
104
137
|
### Themes
|
|
105
138
|
Catppuccin, Tokyo Night, Dracula, Nord, Gruvbox, Solarized, One Dark, and more.
|
|
106
139
|
|
|
@@ -171,32 +204,48 @@ The `install.ps1` script auto-installs Python and VS Build Tools if missing.
|
|
|
171
204
|
|
|
172
205
|
```
|
|
173
206
|
Electron Main Process
|
|
174
|
-
├── PTYManager (node-pty)
|
|
207
|
+
├── PTYManager (node-pty / ConPTY)
|
|
175
208
|
├── PTYBridge (data forwarding + ActivityMonitor)
|
|
176
209
|
├── AgentDetector (gate-based agent status)
|
|
177
|
-
├──
|
|
210
|
+
├── SessionManager (atomic save with .bak recovery)
|
|
211
|
+
├── ScrollbackPersistence (dump/load terminal buffers)
|
|
212
|
+
├── PipeServer (Named Pipe JSON-RPC + token auth)
|
|
178
213
|
├── McpRegistrar (auto-registers MCP in ~/.claude.json)
|
|
214
|
+
├── WebviewCdpManager (CDP proxy to <webview> via debugger)
|
|
215
|
+
├── DaemonClient (optional daemon mode connector)
|
|
179
216
|
└── ToastManager (OS notifications + taskbar flash)
|
|
180
217
|
|
|
181
218
|
Renderer Process (React 19 + Zustand)
|
|
182
219
|
├── PaneContainer (recursive split layout)
|
|
183
|
-
├── Terminal (xterm.js + WebGL)
|
|
184
|
-
├── BrowserPanel (webview + Inspector)
|
|
220
|
+
├── Terminal (xterm.js + WebGL + scrollback restore)
|
|
221
|
+
├── BrowserPanel (webview + Inspector + CDP)
|
|
185
222
|
├── NotificationPanel
|
|
223
|
+
├── SettingsPanel (workspace reset)
|
|
186
224
|
└── Multiview grid
|
|
187
225
|
|
|
226
|
+
Daemon Process (optional, standalone)
|
|
227
|
+
├── DaemonSessionManager (ConPTY lifecycle)
|
|
228
|
+
├── RingBuffer (circular scrollback buffer)
|
|
229
|
+
├── StateWriter (session suspend/resume)
|
|
230
|
+
├── ProcessMonitor (external process watchdog)
|
|
231
|
+
├── Watchdog (memory pressure escalation)
|
|
232
|
+
└── DaemonPipeServer (Named Pipe RPC + token auth)
|
|
233
|
+
|
|
188
234
|
MCP Server (stdio)
|
|
189
|
-
|
|
235
|
+
├── PlaywrightEngine (CDP connection, fast-fail)
|
|
236
|
+
├── CDP RPC fallback (browser.screenshot, browser.evaluate, etc.)
|
|
237
|
+
└── Bridges Claude Code <-> wmux via Named Pipe RPC
|
|
190
238
|
```
|
|
191
239
|
|
|
192
240
|
---
|
|
193
241
|
|
|
194
242
|
## Acknowledgments
|
|
195
243
|
|
|
196
|
-
- [cmux](https://github.com/manaflow-ai/cmux) — The macOS AI agent terminal that inspired wmux
|
|
244
|
+
- [cmux](https://github.com/manaflow-ai/cmux) — The macOS AI agent terminal that inspired wmux
|
|
197
245
|
- [xterm.js](https://xtermjs.org/) — Terminal rendering
|
|
198
246
|
- [node-pty](https://github.com/microsoft/node-pty) — Pseudo-terminal
|
|
199
247
|
- [Electron](https://www.electronjs.org/) — Desktop framework
|
|
248
|
+
- [Playwright](https://playwright.dev/) — Browser automation engine
|
|
200
249
|
|
|
201
250
|
---
|
|
202
251
|
|
|
@@ -4,24 +4,25 @@ exports.handleBrowser = handleBrowser;
|
|
|
4
4
|
const client_1 = require("../client");
|
|
5
5
|
const utils_1 = require("../utils");
|
|
6
6
|
const BROWSER_HELP = `
|
|
7
|
-
wmux browser —
|
|
7
|
+
wmux browser — Browser Commands
|
|
8
8
|
|
|
9
9
|
USAGE
|
|
10
10
|
wmux browser <subcommand> [args]
|
|
11
11
|
|
|
12
12
|
SUBCOMMANDS
|
|
13
|
-
snapshot Return the full page HTML (document.documentElement.outerHTML)
|
|
14
|
-
click <selector> Click the first element matching the CSS selector
|
|
15
|
-
fill <selector> <text> Set the value of an input matching the CSS selector
|
|
16
|
-
eval <code> Execute arbitrary JavaScript in the page context
|
|
17
13
|
navigate <url> Navigate the active browser surface to a URL
|
|
14
|
+
close Close the browser panel
|
|
15
|
+
session start [--profile <name>] Start a browser session
|
|
16
|
+
session stop Stop the active browser session
|
|
17
|
+
session status Show active session status
|
|
18
|
+
session list List available profiles
|
|
18
19
|
|
|
19
20
|
EXAMPLES
|
|
20
|
-
wmux browser snapshot
|
|
21
|
-
wmux browser click "#submit-btn"
|
|
22
|
-
wmux browser fill "input[name=email]" "user@example.com"
|
|
23
|
-
wmux browser eval "document.title"
|
|
24
21
|
wmux browser navigate "https://example.com"
|
|
22
|
+
wmux browser close
|
|
23
|
+
wmux browser session start --profile login
|
|
24
|
+
wmux browser session status
|
|
25
|
+
wmux browser session list
|
|
25
26
|
`.trimStart();
|
|
26
27
|
async function handleBrowser(args, jsonMode) {
|
|
27
28
|
const sub = args[0];
|
|
@@ -32,30 +33,14 @@ async function handleBrowser(args, jsonMode) {
|
|
|
32
33
|
}
|
|
33
34
|
let response;
|
|
34
35
|
switch (sub) {
|
|
35
|
-
// ── browser
|
|
36
|
-
case '
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
(
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
if (!response.ok) {
|
|
43
|
-
(0, utils_1.printError)(response);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
const r = response.result;
|
|
47
|
-
process.stdout.write(r?.html ?? '');
|
|
48
|
-
}
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
// ── browser click <selector> ─────────────────────────────────────────────
|
|
52
|
-
case 'click': {
|
|
53
|
-
const selector = rest[0];
|
|
54
|
-
if (!selector) {
|
|
55
|
-
console.error('Error: browser click requires <selector>');
|
|
36
|
+
// ── browser navigate <url> ───────────────────────────────────────────────
|
|
37
|
+
case 'navigate': {
|
|
38
|
+
const url = rest[0];
|
|
39
|
+
if (!url) {
|
|
40
|
+
console.error('Error: browser navigate requires <url>');
|
|
56
41
|
process.exit(1);
|
|
57
42
|
}
|
|
58
|
-
response = await (0, client_1.sendRequest)('browser.
|
|
43
|
+
response = await (0, client_1.sendRequest)('browser.navigate', { url });
|
|
59
44
|
if (jsonMode) {
|
|
60
45
|
(0, utils_1.printResult)(response);
|
|
61
46
|
}
|
|
@@ -64,19 +49,13 @@ async function handleBrowser(args, jsonMode) {
|
|
|
64
49
|
(0, utils_1.printError)(response);
|
|
65
50
|
return;
|
|
66
51
|
}
|
|
67
|
-
console.log(`
|
|
52
|
+
console.log(`Navigated to: ${url}`);
|
|
68
53
|
}
|
|
69
54
|
break;
|
|
70
55
|
}
|
|
71
|
-
// ── browser
|
|
72
|
-
case '
|
|
73
|
-
|
|
74
|
-
const text = rest.slice(1).join(' ');
|
|
75
|
-
if (!selector) {
|
|
76
|
-
console.error('Error: browser fill requires <selector> <text>');
|
|
77
|
-
process.exit(1);
|
|
78
|
-
}
|
|
79
|
-
response = await (0, client_1.sendRequest)('browser.fill', { selector, text });
|
|
56
|
+
// ── browser close ────────────────────────────────────────────────────────
|
|
57
|
+
case 'close': {
|
|
58
|
+
response = await (0, client_1.sendRequest)('browser.close', {});
|
|
80
59
|
if (jsonMode) {
|
|
81
60
|
(0, utils_1.printResult)(response);
|
|
82
61
|
}
|
|
@@ -85,48 +64,93 @@ async function handleBrowser(args, jsonMode) {
|
|
|
85
64
|
(0, utils_1.printError)(response);
|
|
86
65
|
return;
|
|
87
66
|
}
|
|
88
|
-
console.log(
|
|
67
|
+
console.log('Browser panel closed.');
|
|
89
68
|
}
|
|
90
69
|
break;
|
|
91
70
|
}
|
|
92
|
-
// ── browser
|
|
93
|
-
case '
|
|
94
|
-
const
|
|
95
|
-
if (!
|
|
96
|
-
console.
|
|
97
|
-
process.exit(
|
|
98
|
-
}
|
|
99
|
-
response = await (0, client_1.sendRequest)('browser.eval', { code });
|
|
100
|
-
if (jsonMode) {
|
|
101
|
-
(0, utils_1.printResult)(response);
|
|
71
|
+
// ── browser session <action> ─────────────────────────────────────────────
|
|
72
|
+
case 'session': {
|
|
73
|
+
const action = rest[0];
|
|
74
|
+
if (!action || action === '--help' || action === '-h') {
|
|
75
|
+
console.log('Usage: wmux browser session <start|stop|status|list>');
|
|
76
|
+
process.exit(0);
|
|
102
77
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
78
|
+
switch (action) {
|
|
79
|
+
case 'start': {
|
|
80
|
+
const profileIdx = rest.indexOf('--profile');
|
|
81
|
+
const profile = profileIdx !== -1 ? rest[profileIdx + 1] : undefined;
|
|
82
|
+
const params = {};
|
|
83
|
+
if (profile)
|
|
84
|
+
params['profile'] = profile;
|
|
85
|
+
response = await (0, client_1.sendRequest)('browser.session.start', params);
|
|
86
|
+
if (jsonMode) {
|
|
87
|
+
(0, utils_1.printResult)(response);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
(0, utils_1.printError)(response);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const r = response.result;
|
|
95
|
+
console.log(`Session started with profile: ${r['profile'] ?? 'default'}`);
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
107
98
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (jsonMode) {
|
|
122
|
-
(0, utils_1.printResult)(response);
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
if (!response.ok) {
|
|
126
|
-
(0, utils_1.printError)(response);
|
|
127
|
-
return;
|
|
99
|
+
case 'stop': {
|
|
100
|
+
response = await (0, client_1.sendRequest)('browser.session.stop', {});
|
|
101
|
+
if (jsonMode) {
|
|
102
|
+
(0, utils_1.printResult)(response);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
(0, utils_1.printError)(response);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
console.log('Session stopped.');
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
128
112
|
}
|
|
129
|
-
|
|
113
|
+
case 'status': {
|
|
114
|
+
response = await (0, client_1.sendRequest)('browser.session.status', {});
|
|
115
|
+
if (jsonMode) {
|
|
116
|
+
(0, utils_1.printResult)(response);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
(0, utils_1.printError)(response);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const r = response.result;
|
|
124
|
+
console.log(`Active profile: ${r['profile'] ?? 'none'}`);
|
|
125
|
+
console.log(`CDP port: ${r['port'] ?? 'none'}`);
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case 'list': {
|
|
130
|
+
response = await (0, client_1.sendRequest)('browser.session.list', {});
|
|
131
|
+
if (jsonMode) {
|
|
132
|
+
(0, utils_1.printResult)(response);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
(0, utils_1.printError)(response);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const profiles = response.result['profiles'];
|
|
140
|
+
if (!profiles || profiles.length === 0) {
|
|
141
|
+
console.log('No profiles found.');
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
for (const p of profiles) {
|
|
145
|
+
console.log(` ${p['name']} (partition: ${p['partition']}, persistent: ${p['persistent']})`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
default:
|
|
152
|
+
console.error(`Unknown session action: "${action}". Use start, stop, status, or list.`);
|
|
153
|
+
process.exit(1);
|
|
130
154
|
}
|
|
131
155
|
break;
|
|
132
156
|
}
|
package/dist/cli/cli/index.js
CHANGED
|
@@ -51,11 +51,12 @@ SYSTEM COMMANDS
|
|
|
51
51
|
capabilities List all supported RPC methods
|
|
52
52
|
|
|
53
53
|
BROWSER COMMANDS
|
|
54
|
-
browser snapshot Return the full page HTML of the active browser surface
|
|
55
|
-
browser click <selector> Click an element by CSS selector
|
|
56
|
-
browser fill <selector> <text> Fill an input field by CSS selector
|
|
57
|
-
browser eval <code> Execute JavaScript in the browser context
|
|
58
54
|
browser navigate <url> Navigate the browser surface to a URL
|
|
55
|
+
browser close Close the browser panel
|
|
56
|
+
browser session start [--profile <name>] Start a browser session
|
|
57
|
+
browser session stop Stop the active browser session
|
|
58
|
+
browser session status Show active session status
|
|
59
|
+
browser session list List available profiles
|
|
59
60
|
|
|
60
61
|
GLOBAL FLAGS
|
|
61
62
|
--json Output raw JSON (useful for scripting)
|
|
@@ -67,9 +68,8 @@ EXAMPLES
|
|
|
67
68
|
wmux send "echo hello"
|
|
68
69
|
wmux notify --title "Done" --body "Build finished"
|
|
69
70
|
wmux identify --json
|
|
70
|
-
wmux browser snapshot
|
|
71
71
|
wmux browser navigate "https://example.com"
|
|
72
|
-
wmux browser
|
|
72
|
+
wmux browser close
|
|
73
73
|
`.trimStart();
|
|
74
74
|
const WORKSPACE_CMDS = new Set([
|
|
75
75
|
'list-workspaces',
|
|
@@ -45,6 +45,9 @@ exports.IPC = {
|
|
|
45
45
|
FS_WATCH: 'fs:watch',
|
|
46
46
|
FS_UNWATCH: 'fs:unwatch',
|
|
47
47
|
FS_CHANGED: 'fs:changed',
|
|
48
|
+
// Scrollback persistence
|
|
49
|
+
SCROLLBACK_DUMP: 'scrollback:dump',
|
|
50
|
+
SCROLLBACK_LOAD: 'scrollback:load',
|
|
48
51
|
};
|
|
49
52
|
// Named Pipe / Unix socket path for wmux API
|
|
50
53
|
// Fixed name so MCP clients (e.g. Claude Code) can reconnect across wmux restarts
|
package/dist/cli/shared/rpc.js
CHANGED
|
@@ -25,9 +25,26 @@ exports.ALL_RPC_METHODS = [
|
|
|
25
25
|
'system.identify',
|
|
26
26
|
'system.capabilities',
|
|
27
27
|
'browser.open',
|
|
28
|
-
'browser.snapshot',
|
|
29
|
-
'browser.click',
|
|
30
|
-
'browser.fill',
|
|
31
|
-
'browser.eval',
|
|
32
28
|
'browser.navigate',
|
|
29
|
+
'browser.close',
|
|
30
|
+
'browser.session.start',
|
|
31
|
+
'browser.session.stop',
|
|
32
|
+
'browser.session.status',
|
|
33
|
+
'browser.session.list',
|
|
34
|
+
'browser.type.humanlike',
|
|
35
|
+
'browser.cdp.target',
|
|
36
|
+
'browser.cdp.info',
|
|
37
|
+
'browser.cdp.send',
|
|
38
|
+
'browser.screenshot',
|
|
39
|
+
'browser.evaluate',
|
|
40
|
+
'browser.type.cdp',
|
|
41
|
+
'browser.click.cdp',
|
|
42
|
+
'browser.press.cdp',
|
|
43
|
+
'daemon.createSession',
|
|
44
|
+
'daemon.destroySession',
|
|
45
|
+
'daemon.attachSession',
|
|
46
|
+
'daemon.detachSession',
|
|
47
|
+
'daemon.resizeSession',
|
|
48
|
+
'daemon.listSessions',
|
|
49
|
+
'daemon.ping',
|
|
33
50
|
];
|
package/dist/cli/shared/types.js
CHANGED
|
@@ -7,19 +7,22 @@ exports.validateMessage = validateMessage;
|
|
|
7
7
|
exports.createSurface = createSurface;
|
|
8
8
|
exports.createLeafPane = createLeafPane;
|
|
9
9
|
exports.createWorkspace = createWorkspace;
|
|
10
|
+
exports.validateNavigationUrl = validateNavigationUrl;
|
|
10
11
|
// === Utility: generate unique IDs ===
|
|
11
12
|
function generateId(prefix) {
|
|
12
13
|
return `${prefix}-${crypto.randomUUID()}`;
|
|
13
14
|
}
|
|
14
15
|
// === Security: sanitize text before PTY write ===
|
|
15
16
|
/**
|
|
16
|
-
* Strips control characters
|
|
17
|
-
*
|
|
17
|
+
* Strips dangerous control characters from text before writing to a PTY.
|
|
18
|
+
* Removes: NULL byte (\x00) and C1 control characters (\x80-\x9f).
|
|
19
|
+
* Preserves: CR (\r), LF (\n), Tab (\t), ESC sequences (\x1b[...),
|
|
20
|
+
* and other standard terminal control characters needed for normal operation.
|
|
18
21
|
*/
|
|
19
22
|
function sanitizePtyText(text) {
|
|
20
|
-
// Remove
|
|
23
|
+
// Remove NULL byte and C1 control characters (U+0080–U+009F)
|
|
21
24
|
// eslint-disable-next-line no-control-regex
|
|
22
|
-
return text.replace(/[\x00
|
|
25
|
+
return text.replace(/[\x00\u0080-\u009f]/g, '');
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
25
28
|
* Validates and clamps a user-supplied name string.
|
|
@@ -77,3 +80,104 @@ function createWorkspace(name) {
|
|
|
77
80
|
activePaneId: rootPane.id,
|
|
78
81
|
};
|
|
79
82
|
}
|
|
83
|
+
// === Security: URL validation for SSRF prevention ===
|
|
84
|
+
/**
|
|
85
|
+
* Validates a URL for safe navigation. Blocks dangerous schemes and private
|
|
86
|
+
* network addresses to prevent SSRF attacks from AI agent-driven browsing.
|
|
87
|
+
*
|
|
88
|
+
* Allows localhost/127.0.0.1/[::1] for local development servers.
|
|
89
|
+
*
|
|
90
|
+
* NOTE (v1 limitation): This is string-based validation only. DNS-resolved IPs
|
|
91
|
+
* are not checked, so DNS rebinding attacks are not mitigated. A future version
|
|
92
|
+
* should resolve hostnames and re-validate the resolved IP.
|
|
93
|
+
*/
|
|
94
|
+
function validateNavigationUrl(url) {
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = new URL(url);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return { valid: false, reason: 'Invalid URL' };
|
|
101
|
+
}
|
|
102
|
+
// Only allow http and https schemes
|
|
103
|
+
const scheme = parsed.protocol.toLowerCase();
|
|
104
|
+
if (scheme !== 'http:' && scheme !== 'https:') {
|
|
105
|
+
return { valid: false, reason: `Blocked URL scheme: ${scheme}` };
|
|
106
|
+
}
|
|
107
|
+
// Extract hostname (strip brackets from IPv6)
|
|
108
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
109
|
+
// Allow localhost and IPv4/IPv6 loopback
|
|
110
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {
|
|
111
|
+
return { valid: true };
|
|
112
|
+
}
|
|
113
|
+
// Block IPv6 private/link-local ranges
|
|
114
|
+
if (hostname.startsWith('[') || hostname.includes(':')) {
|
|
115
|
+
// Hostname is an IPv6 address (URL parser strips brackets in .hostname)
|
|
116
|
+
const addr = hostname;
|
|
117
|
+
// Block fc00::/7 (unique local) — starts with fc or fd
|
|
118
|
+
if (addr.startsWith('fc') || addr.startsWith('fd')) {
|
|
119
|
+
return { valid: false, reason: 'Blocked private IPv6 address (fc00::/7)' };
|
|
120
|
+
}
|
|
121
|
+
// Block fe80::/10 (link-local) — starts with fe8, fe9, fea, feb
|
|
122
|
+
if (/^fe[89ab]/.test(addr)) {
|
|
123
|
+
return { valid: false, reason: 'Blocked link-local IPv6 address (fe80::/10)' };
|
|
124
|
+
}
|
|
125
|
+
// ::1 already allowed above; block any other loopback representation
|
|
126
|
+
// Normalize: collapse :: and check
|
|
127
|
+
if (addr === '0:0:0:0:0:0:0:1' || addr === '0000:0000:0000:0000:0000:0000:0000:0001') {
|
|
128
|
+
return { valid: true };
|
|
129
|
+
}
|
|
130
|
+
// Block null IPv6 address (:: or 0:0:0:0:0:0:0:0) — equivalent to 0.0.0.0
|
|
131
|
+
if (addr === '::' || addr === '0:0:0:0:0:0:0:0' || addr === '0000:0000:0000:0000:0000:0000:0000:0000') {
|
|
132
|
+
return { valid: false, reason: 'Blocked null IPv6 address (equivalent to 0.0.0.0)' };
|
|
133
|
+
}
|
|
134
|
+
// Block IPv4-mapped IPv6 (::ffff:x.x.x.x) and IPv4-compatible IPv6 (::x.x.x.x)
|
|
135
|
+
// These resolve to their embedded IPv4 address, bypassing IPv4 private IP checks.
|
|
136
|
+
const v4MappedMatch = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(addr);
|
|
137
|
+
const v4CompatMatch = !v4MappedMatch ? /^::(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(addr) : null;
|
|
138
|
+
const embeddedV4 = v4MappedMatch?.[1] ?? v4CompatMatch?.[1];
|
|
139
|
+
if (embeddedV4) {
|
|
140
|
+
// Recursively validate the embedded IPv4 through the same checks
|
|
141
|
+
const embeddedResult = validateNavigationUrl(`http://${embeddedV4}/`);
|
|
142
|
+
if (!embeddedResult.valid) {
|
|
143
|
+
return { valid: false, reason: `Blocked IPv4-mapped/compatible IPv6: embedded ${embeddedV4} — ${embeddedResult.reason}` };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { valid: true };
|
|
147
|
+
}
|
|
148
|
+
// Check for IPv4 addresses
|
|
149
|
+
const ipv4Match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
150
|
+
if (ipv4Match) {
|
|
151
|
+
const octets = [
|
|
152
|
+
parseInt(ipv4Match[1], 10),
|
|
153
|
+
parseInt(ipv4Match[2], 10),
|
|
154
|
+
parseInt(ipv4Match[3], 10),
|
|
155
|
+
parseInt(ipv4Match[4], 10),
|
|
156
|
+
];
|
|
157
|
+
// 127.0.0.1 already allowed above; block other 127.x.x.x
|
|
158
|
+
if (octets[0] === 127) {
|
|
159
|
+
return { valid: false, reason: 'Blocked loopback address' };
|
|
160
|
+
}
|
|
161
|
+
// Block 10.0.0.0/8
|
|
162
|
+
if (octets[0] === 10) {
|
|
163
|
+
return { valid: false, reason: 'Blocked private IP address (10.0.0.0/8)' };
|
|
164
|
+
}
|
|
165
|
+
// Block 172.16.0.0/12 (172.16.x.x – 172.31.x.x)
|
|
166
|
+
if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) {
|
|
167
|
+
return { valid: false, reason: 'Blocked private IP address (172.16.0.0/12)' };
|
|
168
|
+
}
|
|
169
|
+
// Block 192.168.0.0/16
|
|
170
|
+
if (octets[0] === 192 && octets[1] === 168) {
|
|
171
|
+
return { valid: false, reason: 'Blocked private IP address (192.168.0.0/16)' };
|
|
172
|
+
}
|
|
173
|
+
// Block 169.254.0.0/16 (link-local, includes cloud metadata 169.254.169.254)
|
|
174
|
+
if (octets[0] === 169 && octets[1] === 254) {
|
|
175
|
+
return { valid: false, reason: 'Blocked link-local/cloud metadata address (169.254.0.0/16)' };
|
|
176
|
+
}
|
|
177
|
+
// Block 0.0.0.0
|
|
178
|
+
if (octets.every((o) => o === 0)) {
|
|
179
|
+
return { valid: false, reason: 'Blocked null address (0.0.0.0)' };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { valid: true };
|
|
183
|
+
}
|