@wong2kim/wmux 1.0.0 → 1.0.1
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/LICENSE +21 -0
- package/README.md +209 -157
- package/dist/cli/cli/client.js +1 -1
- package/dist/cli/cli/commands/browser.js +19 -19
- package/dist/cli/cli/index.js +58 -58
- package/dist/cli/shared/constants.js +17 -4
- package/dist/mcp/shared/constants.js +17 -4
- package/package.json +96 -84
- package/assets/icon.ico +0 -0
- package/assets/icon.svg +0 -6
- package/forge.config.ts +0 -61
- package/index.html +0 -12
- package/postcss.config.js +0 -6
- package/src/cli/client.ts +0 -76
- package/src/cli/commands/browser.ts +0 -128
- package/src/cli/commands/input.ts +0 -72
- package/src/cli/commands/notify.ts +0 -29
- package/src/cli/commands/pane.ts +0 -90
- package/src/cli/commands/surface.ts +0 -102
- package/src/cli/commands/system.ts +0 -95
- package/src/cli/commands/workspace.ts +0 -116
- package/src/cli/index.ts +0 -145
- package/src/cli/utils.ts +0 -44
- package/src/main/index.ts +0 -86
- package/src/main/ipc/handlers/clipboard.handler.ts +0 -20
- package/src/main/ipc/handlers/metadata.handler.ts +0 -56
- package/src/main/ipc/handlers/pty.handler.ts +0 -69
- package/src/main/ipc/handlers/session.handler.ts +0 -17
- package/src/main/ipc/handlers/shell.handler.ts +0 -11
- package/src/main/ipc/registerHandlers.ts +0 -31
- package/src/main/mcp/McpRegistrar.ts +0 -156
- package/src/main/metadata/MetadataCollector.ts +0 -58
- package/src/main/notification/ToastManager.ts +0 -32
- package/src/main/pipe/PipeServer.ts +0 -190
- package/src/main/pipe/RpcRouter.ts +0 -46
- package/src/main/pipe/handlers/_bridge.ts +0 -40
- package/src/main/pipe/handlers/browser.rpc.ts +0 -132
- package/src/main/pipe/handlers/input.rpc.ts +0 -120
- package/src/main/pipe/handlers/meta.rpc.ts +0 -59
- package/src/main/pipe/handlers/notify.rpc.ts +0 -53
- package/src/main/pipe/handlers/pane.rpc.ts +0 -39
- package/src/main/pipe/handlers/surface.rpc.ts +0 -43
- package/src/main/pipe/handlers/system.rpc.ts +0 -36
- package/src/main/pipe/handlers/workspace.rpc.ts +0 -52
- package/src/main/pty/AgentDetector.ts +0 -247
- package/src/main/pty/OscParser.ts +0 -81
- package/src/main/pty/PTYBridge.ts +0 -88
- package/src/main/pty/PTYManager.ts +0 -104
- package/src/main/pty/ShellDetector.ts +0 -63
- package/src/main/session/SessionManager.ts +0 -53
- package/src/main/updater/AutoUpdater.ts +0 -132
- package/src/main/window/createWindow.ts +0 -71
- package/src/mcp/README.md +0 -56
- package/src/mcp/index.ts +0 -153
- package/src/mcp/wmux-client.ts +0 -127
- package/src/preload/index.ts +0 -111
- package/src/preload/preload.ts +0 -108
- package/src/renderer/App.tsx +0 -5
- package/src/renderer/components/Browser/BrowserPanel.tsx +0 -219
- package/src/renderer/components/Browser/BrowserToolbar.tsx +0 -253
- package/src/renderer/components/Company/ApprovalDialog.tsx +0 -3
- package/src/renderer/components/Company/CompanyView.tsx +0 -7
- package/src/renderer/components/Company/MessageFeedPanel.tsx +0 -3
- package/src/renderer/components/Layout/AppLayout.tsx +0 -234
- package/src/renderer/components/Notification/NotificationPanel.tsx +0 -129
- package/src/renderer/components/Palette/CommandPalette.tsx +0 -409
- package/src/renderer/components/Palette/PaletteItem.tsx +0 -55
- package/src/renderer/components/Pane/Pane.tsx +0 -122
- package/src/renderer/components/Pane/PaneContainer.tsx +0 -41
- package/src/renderer/components/Pane/SurfaceTabs.tsx +0 -46
- package/src/renderer/components/Settings/SettingsPanel.tsx +0 -886
- package/src/renderer/components/Sidebar/MiniSidebar.tsx +0 -67
- package/src/renderer/components/Sidebar/Sidebar.tsx +0 -84
- package/src/renderer/components/Sidebar/WorkspaceItem.tsx +0 -241
- package/src/renderer/components/StatusBar/StatusBar.tsx +0 -93
- package/src/renderer/components/Terminal/SearchBar.tsx +0 -126
- package/src/renderer/components/Terminal/Terminal.tsx +0 -102
- package/src/renderer/components/Terminal/ViCopyMode.tsx +0 -104
- package/src/renderer/hooks/useKeyboard.ts +0 -310
- package/src/renderer/hooks/useNotificationListener.ts +0 -80
- package/src/renderer/hooks/useNotificationSound.ts +0 -75
- package/src/renderer/hooks/useRpcBridge.ts +0 -451
- package/src/renderer/hooks/useT.ts +0 -11
- package/src/renderer/hooks/useTerminal.ts +0 -349
- package/src/renderer/hooks/useViCopyMode.ts +0 -320
- package/src/renderer/i18n/index.ts +0 -69
- package/src/renderer/i18n/locales/en.ts +0 -157
- package/src/renderer/i18n/locales/ja.ts +0 -155
- package/src/renderer/i18n/locales/ko.ts +0 -155
- package/src/renderer/i18n/locales/zh.ts +0 -155
- package/src/renderer/index.tsx +0 -6
- package/src/renderer/stores/index.ts +0 -19
- package/src/renderer/stores/slices/notificationSlice.ts +0 -56
- package/src/renderer/stores/slices/paneSlice.ts +0 -141
- package/src/renderer/stores/slices/surfaceSlice.ts +0 -122
- package/src/renderer/stores/slices/uiSlice.ts +0 -247
- package/src/renderer/stores/slices/workspaceSlice.ts +0 -120
- package/src/renderer/styles/globals.css +0 -150
- package/src/renderer/themes.ts +0 -99
- package/src/shared/constants.ts +0 -53
- package/src/shared/electron.d.ts +0 -11
- package/src/shared/rpc.ts +0 -71
- package/src/shared/types.ts +0 -176
- package/tailwind.config.js +0 -11
- package/tsconfig.cli.json +0 -24
- package/tsconfig.json +0 -21
- package/tsconfig.mcp.json +0 -25
- package/vite.main.config.ts +0 -14
- package/vite.preload.config.ts +0 -9
- package/vite.renderer.config.ts +0 -6
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AutoUpdater
|
|
3
|
-
*
|
|
4
|
-
* Electron 내장 autoUpdater API 기반 자동 업데이트 시스템.
|
|
5
|
-
*
|
|
6
|
-
* 실제 배포 환경에서는:
|
|
7
|
-
* 1. electron-forge squirrel maker로 빌드
|
|
8
|
-
* 2. GitHub Releases (또는 S3)에 업데이트 파일 업로드
|
|
9
|
-
* 3. FEED_URL을 업데이트 서버 주소로 변경
|
|
10
|
-
*
|
|
11
|
-
* 개발 환경에서는 autoUpdater가 지원되지 않으므로 모두 no-op 처리.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { autoUpdater, type BrowserWindow, ipcMain } from 'electron';
|
|
15
|
-
import { IPC } from '../../shared/constants';
|
|
16
|
-
|
|
17
|
-
// GitHub Releases 또는 별도 업데이트 서버 URL
|
|
18
|
-
// 예: https://update.winmux.app/update/win32/${version}
|
|
19
|
-
const FEED_URL = '';
|
|
20
|
-
|
|
21
|
-
// 업데이트 자동 확인 간격 (30분)
|
|
22
|
-
const CHECK_INTERVAL_MS = 30 * 60 * 1000;
|
|
23
|
-
|
|
24
|
-
export class AutoUpdater {
|
|
25
|
-
private checkTimer: ReturnType<typeof setInterval> | null = null;
|
|
26
|
-
private getWindow: () => BrowserWindow | null;
|
|
27
|
-
private isChecking = false;
|
|
28
|
-
|
|
29
|
-
constructor(getWindow: () => BrowserWindow | null) {
|
|
30
|
-
this.getWindow = getWindow;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
start(): void {
|
|
34
|
-
if (!FEED_URL || process.env.NODE_ENV === 'development') {
|
|
35
|
-
// 업데이트 서버가 설정되지 않았거나 개발 모드 — 초기화 스킵
|
|
36
|
-
this.registerIpcHandlers();
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
autoUpdater.setFeedURL({ url: FEED_URL });
|
|
42
|
-
this.setupAutoUpdaterEvents();
|
|
43
|
-
this.registerIpcHandlers();
|
|
44
|
-
|
|
45
|
-
// 앱 시작 후 15초 뒤 첫 번째 확인 (시작 부하 방지)
|
|
46
|
-
setTimeout(() => this.check(), 15_000);
|
|
47
|
-
|
|
48
|
-
// 이후 주기적 확인
|
|
49
|
-
this.checkTimer = setInterval(() => this.check(), CHECK_INTERVAL_MS);
|
|
50
|
-
} catch (err) {
|
|
51
|
-
console.warn('[AutoUpdater] Failed to initialize:', err);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
stop(): void {
|
|
56
|
-
if (this.checkTimer !== null) {
|
|
57
|
-
clearInterval(this.checkTimer);
|
|
58
|
-
this.checkTimer = null;
|
|
59
|
-
}
|
|
60
|
-
// IPC 핸들러 정리
|
|
61
|
-
ipcMain.removeHandler(IPC.UPDATE_CHECK);
|
|
62
|
-
ipcMain.removeHandler(IPC.UPDATE_INSTALL);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
private check(): void {
|
|
66
|
-
if (this.isChecking) return;
|
|
67
|
-
try {
|
|
68
|
-
this.isChecking = true;
|
|
69
|
-
autoUpdater.checkForUpdates();
|
|
70
|
-
} catch (err) {
|
|
71
|
-
console.warn('[AutoUpdater] checkForUpdates error:', err);
|
|
72
|
-
this.isChecking = false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private setupAutoUpdaterEvents(): void {
|
|
77
|
-
autoUpdater.on('checking-for-update', () => {
|
|
78
|
-
this.sendToRenderer(IPC.UPDATE_CHECK, { status: 'checking' });
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
autoUpdater.on('update-available', () => {
|
|
82
|
-
this.isChecking = false;
|
|
83
|
-
this.sendToRenderer(IPC.UPDATE_AVAILABLE, { status: 'available' });
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
autoUpdater.on('update-not-available', () => {
|
|
87
|
-
this.isChecking = false;
|
|
88
|
-
this.sendToRenderer(IPC.UPDATE_NOT_AVAILABLE, { status: 'not-available' });
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
autoUpdater.on('error', (err: Error) => {
|
|
92
|
-
this.isChecking = false;
|
|
93
|
-
console.warn('[AutoUpdater] error:', err.message);
|
|
94
|
-
this.sendToRenderer(IPC.UPDATE_ERROR, { status: 'error', message: err.message });
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
autoUpdater.on('update-downloaded', (_event, releaseNotes, releaseName) => {
|
|
98
|
-
this.sendToRenderer(IPC.UPDATE_AVAILABLE, {
|
|
99
|
-
status: 'downloaded',
|
|
100
|
-
releaseName,
|
|
101
|
-
releaseNotes,
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private registerIpcHandlers(): void {
|
|
107
|
-
// Renderer가 수동으로 업데이트 확인 요청
|
|
108
|
-
ipcMain.handle(IPC.UPDATE_CHECK, () => {
|
|
109
|
-
if (!FEED_URL || process.env.NODE_ENV === 'development') {
|
|
110
|
-
return { status: 'not-available' };
|
|
111
|
-
}
|
|
112
|
-
this.check();
|
|
113
|
-
return { status: 'checking' };
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Renderer가 "지금 설치" 요청 → 앱 재시작 후 업데이트 적용
|
|
117
|
-
ipcMain.handle(IPC.UPDATE_INSTALL, () => {
|
|
118
|
-
try {
|
|
119
|
-
autoUpdater.quitAndInstall();
|
|
120
|
-
} catch (err) {
|
|
121
|
-
console.warn('[AutoUpdater] quitAndInstall error:', err);
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
private sendToRenderer(channel: string, data: Record<string, unknown>): void {
|
|
127
|
-
const win = this.getWindow();
|
|
128
|
-
if (win && !win.isDestroyed()) {
|
|
129
|
-
win.webContents.send(channel, data);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { BrowserWindow } from 'electron';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
export function createWindow(): BrowserWindow {
|
|
5
|
-
const mainWindow = new BrowserWindow({
|
|
6
|
-
width: 1280,
|
|
7
|
-
height: 800,
|
|
8
|
-
minWidth: 800,
|
|
9
|
-
minHeight: 600,
|
|
10
|
-
title: 'wmux',
|
|
11
|
-
icon: path.join(__dirname, '../../assets/icon.ico'),
|
|
12
|
-
backgroundColor: '#1e1e2e',
|
|
13
|
-
webPreferences: {
|
|
14
|
-
preload: path.join(__dirname, 'preload.js'),
|
|
15
|
-
contextIsolation: true,
|
|
16
|
-
nodeIntegration: false,
|
|
17
|
-
webviewTag: true,
|
|
18
|
-
},
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
|
22
|
-
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
|
|
23
|
-
} else {
|
|
24
|
-
mainWindow.loadFile(
|
|
25
|
-
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`),
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// CSP header — production only (dev needs full access for Vite HMR)
|
|
30
|
-
if (!MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
|
31
|
-
mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
|
32
|
-
callback({
|
|
33
|
-
responseHeaders: {
|
|
34
|
-
...details.responseHeaders,
|
|
35
|
-
'Content-Security-Policy': [
|
|
36
|
-
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'",
|
|
37
|
-
],
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Harden webview security: strip preload, enforce contextIsolation
|
|
44
|
-
mainWindow.webContents.on('will-attach-webview', (_event, webPreferences) => {
|
|
45
|
-
delete webPreferences.preload;
|
|
46
|
-
delete (webPreferences as Record<string, unknown>)['preloadURL'];
|
|
47
|
-
webPreferences.nodeIntegration = false;
|
|
48
|
-
webPreferences.contextIsolation = true;
|
|
49
|
-
webPreferences.sandbox = true;
|
|
50
|
-
// Ensure web security (same-origin policy) is not accidentally disabled
|
|
51
|
-
(webPreferences as Record<string, unknown>)['webSecurity'] = true;
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// Block webview navigations to dangerous URL schemes
|
|
55
|
-
mainWindow.webContents.on('will-navigate', (event, url) => {
|
|
56
|
-
const normalized = url.trim().toLowerCase();
|
|
57
|
-
if (
|
|
58
|
-
normalized.startsWith('javascript:') ||
|
|
59
|
-
normalized.startsWith('vbscript:') ||
|
|
60
|
-
normalized.startsWith('data:')
|
|
61
|
-
) {
|
|
62
|
-
event.preventDefault();
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
if (process.env.NODE_ENV === 'development') {
|
|
67
|
-
mainWindow.webContents.openDevTools();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return mainWindow;
|
|
71
|
-
}
|
package/src/mcp/README.md
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
# wmux MCP Server
|
|
2
|
-
|
|
3
|
-
MCP server that lets Claude Code control wmux's browser and terminal.
|
|
4
|
-
Supports multi-agent use — each agent can target its own browser via `surfaceId`.
|
|
5
|
-
|
|
6
|
-
## Setup
|
|
7
|
-
|
|
8
|
-
1. Build the MCP server:
|
|
9
|
-
```bash
|
|
10
|
-
npm run build:mcp
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
2. Add to your project's `.mcp.json`:
|
|
14
|
-
```json
|
|
15
|
-
{
|
|
16
|
-
"mcpServers": {
|
|
17
|
-
"wmux": {
|
|
18
|
-
"command": "node",
|
|
19
|
-
"args": ["<path-to-wmux>/dist/mcp/mcp/index.js"]
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
`WMUX_SOCKET_PATH` and `WMUX_AUTH_TOKEN` are automatically set in wmux
|
|
26
|
-
terminal sessions. When running Claude Code inside wmux, no extra env
|
|
27
|
-
config is needed.
|
|
28
|
-
|
|
29
|
-
## Available Tools
|
|
30
|
-
|
|
31
|
-
| Tool | Description |
|
|
32
|
-
|------|-------------|
|
|
33
|
-
| `browser_navigate` | Navigate browser to URL |
|
|
34
|
-
| `browser_snapshot` | Get page HTML |
|
|
35
|
-
| `browser_click` | Click element by CSS selector |
|
|
36
|
-
| `browser_fill` | Fill input by CSS selector |
|
|
37
|
-
| `browser_eval` | Execute JS in browser |
|
|
38
|
-
| `terminal_read` | Read terminal screen |
|
|
39
|
-
| `terminal_send` | Send text to terminal |
|
|
40
|
-
| `terminal_send_key` | Send key (enter, ctrl+c, etc.) |
|
|
41
|
-
| `workspace_list` | List workspaces |
|
|
42
|
-
| `surface_list` | List surfaces (terminals + browsers) |
|
|
43
|
-
| `pane_list` | List panes |
|
|
44
|
-
|
|
45
|
-
## Multi-Agent Usage
|
|
46
|
-
|
|
47
|
-
All browser tools accept an optional `surfaceId` parameter. Use `surface_list`
|
|
48
|
-
to discover available surfaces, then pass the browser surface's ID:
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
1. Call surface_list → find your browser surface ID
|
|
52
|
-
2. Call browser_navigate with surfaceId="<your-browser-id>"
|
|
53
|
-
3. Call browser_snapshot with surfaceId="<your-browser-id>"
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
When `surfaceId` is omitted, the currently active browser surface is used.
|
package/src/mcp/index.ts
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
import { sendRpc } from './wmux-client';
|
|
6
|
-
import type { RpcMethod } from '../shared/rpc';
|
|
7
|
-
|
|
8
|
-
const server = new McpServer({
|
|
9
|
-
name: 'wmux',
|
|
10
|
-
version: '1.0.0',
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
// Helper: wrap an RPC call as an MCP tool result
|
|
14
|
-
async function callRpc(
|
|
15
|
-
method: RpcMethod,
|
|
16
|
-
params: Record<string, unknown> = {},
|
|
17
|
-
): Promise<{ content: { type: 'text'; text: string }[] }> {
|
|
18
|
-
const result = await sendRpc(method, params);
|
|
19
|
-
const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
20
|
-
return { content: [{ type: 'text', text }] };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Optional surfaceId schema used by browser and terminal tools
|
|
24
|
-
const optionalSurfaceId = z.string().optional().describe(
|
|
25
|
-
'Target a specific surface by ID. Omit to use the active surface.',
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
// === Browser tools ===
|
|
29
|
-
|
|
30
|
-
server.tool(
|
|
31
|
-
'browser_open',
|
|
32
|
-
'Open a new browser panel in the active pane. Use this when no browser surface exists yet.',
|
|
33
|
-
{
|
|
34
|
-
url: z.string().optional().describe('Initial URL to load (defaults to google.com)'),
|
|
35
|
-
},
|
|
36
|
-
async ({ url }) =>
|
|
37
|
-
callRpc('browser.open', url ? { url } : {}),
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
server.tool(
|
|
41
|
-
'browser_navigate',
|
|
42
|
-
'Navigate the wmux browser panel to a URL',
|
|
43
|
-
{
|
|
44
|
-
url: z.string().describe('The URL to navigate to'),
|
|
45
|
-
surfaceId: optionalSurfaceId,
|
|
46
|
-
},
|
|
47
|
-
async ({ url, surfaceId }) =>
|
|
48
|
-
callRpc('browser.navigate', { url, ...(surfaceId && { surfaceId }) }),
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
server.tool(
|
|
52
|
-
'browser_snapshot',
|
|
53
|
-
'Get the full HTML content of the current page in the wmux browser panel',
|
|
54
|
-
{ surfaceId: optionalSurfaceId },
|
|
55
|
-
async ({ surfaceId }) =>
|
|
56
|
-
callRpc('browser.snapshot', surfaceId ? { surfaceId } : {}),
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
server.tool(
|
|
60
|
-
'browser_click',
|
|
61
|
-
'Click an element in the wmux browser panel by CSS selector',
|
|
62
|
-
{
|
|
63
|
-
selector: z.string().describe('CSS selector of the element to click'),
|
|
64
|
-
surfaceId: optionalSurfaceId,
|
|
65
|
-
},
|
|
66
|
-
async ({ selector, surfaceId }) =>
|
|
67
|
-
callRpc('browser.click', { selector, ...(surfaceId && { surfaceId }) }),
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
server.tool(
|
|
71
|
-
'browser_fill',
|
|
72
|
-
'Fill an input field in the wmux browser panel by CSS selector',
|
|
73
|
-
{
|
|
74
|
-
selector: z.string().describe('CSS selector of the input element'),
|
|
75
|
-
text: z.string().describe('Text to fill into the input'),
|
|
76
|
-
surfaceId: optionalSurfaceId,
|
|
77
|
-
},
|
|
78
|
-
async ({ selector, text, surfaceId }) =>
|
|
79
|
-
callRpc('browser.fill', { selector, text, ...(surfaceId && { surfaceId }) }),
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
server.tool(
|
|
83
|
-
'browser_eval',
|
|
84
|
-
'Execute JavaScript in the wmux browser panel and return the result',
|
|
85
|
-
{
|
|
86
|
-
code: z.string().describe('JavaScript code to execute in the browser context'),
|
|
87
|
-
surfaceId: optionalSurfaceId,
|
|
88
|
-
},
|
|
89
|
-
async ({ code, surfaceId }) =>
|
|
90
|
-
callRpc('browser.eval', { code, ...(surfaceId && { surfaceId }) }),
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
// === Terminal tools ===
|
|
94
|
-
|
|
95
|
-
server.tool(
|
|
96
|
-
'terminal_read',
|
|
97
|
-
'Read the current visible text from the active terminal in wmux',
|
|
98
|
-
{},
|
|
99
|
-
async () => callRpc('input.readScreen'),
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
server.tool(
|
|
103
|
-
'terminal_send',
|
|
104
|
-
'Send text to the active terminal in wmux',
|
|
105
|
-
{ text: z.string().describe('Text to send to the terminal') },
|
|
106
|
-
async ({ text }) => callRpc('input.send', { text }),
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
server.tool(
|
|
110
|
-
'terminal_send_key',
|
|
111
|
-
'Send a named key to the active terminal (enter, tab, ctrl+c, ctrl+d, ctrl+z, ctrl+l, escape, up, down, right, left)',
|
|
112
|
-
{
|
|
113
|
-
key: z.string().describe(
|
|
114
|
-
'Key name: enter, tab, ctrl+c, ctrl+d, ctrl+z, ctrl+l, escape, up, down, right, left',
|
|
115
|
-
),
|
|
116
|
-
},
|
|
117
|
-
async ({ key }) => callRpc('input.sendKey', { key }),
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
// === Workspace tools ===
|
|
121
|
-
|
|
122
|
-
server.tool(
|
|
123
|
-
'workspace_list',
|
|
124
|
-
'List all workspaces in wmux',
|
|
125
|
-
{},
|
|
126
|
-
async () => callRpc('workspace.list'),
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
server.tool(
|
|
130
|
-
'surface_list',
|
|
131
|
-
'List all surfaces (terminals and browsers) in the active workspace',
|
|
132
|
-
{},
|
|
133
|
-
async () => callRpc('surface.list'),
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
server.tool(
|
|
137
|
-
'pane_list',
|
|
138
|
-
'List all panes in the current workspace',
|
|
139
|
-
{},
|
|
140
|
-
async () => callRpc('pane.list'),
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
// === Start server ===
|
|
144
|
-
|
|
145
|
-
async function main(): Promise<void> {
|
|
146
|
-
const transport = new StdioServerTransport();
|
|
147
|
-
await server.connect(transport);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
main().catch((err) => {
|
|
151
|
-
console.error('wmux MCP server failed to start:', err);
|
|
152
|
-
process.exit(1);
|
|
153
|
-
});
|
package/src/mcp/wmux-client.ts
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import * as net from 'net';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as crypto from 'crypto';
|
|
4
|
-
import type { RpcMethod, RpcResponse } from '../shared/rpc';
|
|
5
|
-
import { getPipeName, getAuthTokenPath } from '../shared/constants';
|
|
6
|
-
|
|
7
|
-
const TIMEOUT_MS = 10000;
|
|
8
|
-
const RETRY_COUNT = 3;
|
|
9
|
-
const RETRY_DELAY_MS = 1000;
|
|
10
|
-
|
|
11
|
-
function readAuthToken(): string | undefined {
|
|
12
|
-
// Env var takes priority (when running inside wmux terminal)
|
|
13
|
-
if (process.env.WMUX_AUTH_TOKEN) return process.env.WMUX_AUTH_TOKEN;
|
|
14
|
-
// File fallback (when spawned by Claude Code as MCP server)
|
|
15
|
-
try {
|
|
16
|
-
return fs.readFileSync(getAuthTokenPath(), 'utf8').trim();
|
|
17
|
-
} catch {
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function attemptRpc(
|
|
23
|
-
pipePath: string,
|
|
24
|
-
token: string,
|
|
25
|
-
method: RpcMethod,
|
|
26
|
-
params: Record<string, unknown>,
|
|
27
|
-
): Promise<unknown> {
|
|
28
|
-
return new Promise((resolve, reject) => {
|
|
29
|
-
const id = crypto.randomUUID();
|
|
30
|
-
const request = JSON.stringify({ id, method, params, token }) + '\n';
|
|
31
|
-
|
|
32
|
-
const socket = net.connect(pipePath);
|
|
33
|
-
let buffer = '';
|
|
34
|
-
let settled = false;
|
|
35
|
-
|
|
36
|
-
const timer = setTimeout(() => {
|
|
37
|
-
if (!settled) {
|
|
38
|
-
settled = true;
|
|
39
|
-
socket.destroy();
|
|
40
|
-
reject(new Error(`RPC timeout: ${method} (${TIMEOUT_MS}ms)`));
|
|
41
|
-
}
|
|
42
|
-
}, TIMEOUT_MS);
|
|
43
|
-
|
|
44
|
-
socket.on('connect', () => {
|
|
45
|
-
socket.write(request);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
socket.on('data', (chunk: Buffer) => {
|
|
49
|
-
buffer += chunk.toString('utf8');
|
|
50
|
-
const lines = buffer.split('\n');
|
|
51
|
-
buffer = lines.pop() ?? '';
|
|
52
|
-
|
|
53
|
-
for (const line of lines) {
|
|
54
|
-
const trimmed = line.trim();
|
|
55
|
-
if (!trimmed) continue;
|
|
56
|
-
try {
|
|
57
|
-
const response = JSON.parse(trimmed) as RpcResponse;
|
|
58
|
-
if (response.id === id && !settled) {
|
|
59
|
-
settled = true;
|
|
60
|
-
clearTimeout(timer);
|
|
61
|
-
socket.destroy();
|
|
62
|
-
if (response.ok) {
|
|
63
|
-
resolve(response.result);
|
|
64
|
-
} else {
|
|
65
|
-
reject(new Error(response.error));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
} catch {
|
|
69
|
-
// ignore malformed lines
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
socket.on('error', (err: NodeJS.ErrnoException) => {
|
|
75
|
-
if (!settled) {
|
|
76
|
-
settled = true;
|
|
77
|
-
clearTimeout(timer);
|
|
78
|
-
if (err.code === 'ENOENT' || err.code === 'ECONNREFUSED') {
|
|
79
|
-
reject(new Error('wmux is not running. Start the app first.'));
|
|
80
|
-
} else {
|
|
81
|
-
reject(new Error(`Connection error: ${err.message}`));
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
socket.on('close', () => {
|
|
87
|
-
if (!settled) {
|
|
88
|
-
settled = true;
|
|
89
|
-
clearTimeout(timer);
|
|
90
|
-
reject(new Error('Connection closed before response was received.'));
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function sleep(ms: number): Promise<void> {
|
|
97
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export async function sendRpc(
|
|
101
|
-
method: RpcMethod,
|
|
102
|
-
params: Record<string, unknown> = {},
|
|
103
|
-
): Promise<unknown> {
|
|
104
|
-
const pipePath = process.env.WMUX_SOCKET_PATH || getPipeName();
|
|
105
|
-
|
|
106
|
-
for (let attempt = 0; attempt < RETRY_COUNT; attempt++) {
|
|
107
|
-
// Re-read token on every attempt (wmux may have restarted with new token)
|
|
108
|
-
const token = readAuthToken();
|
|
109
|
-
if (!token) {
|
|
110
|
-
throw new Error('wmux auth token not found. Is wmux running?');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
return await attemptRpc(pipePath, token, method, params);
|
|
115
|
-
} catch (err) {
|
|
116
|
-
const msg = (err as Error).message;
|
|
117
|
-
const isRetryable = msg.includes('not running') || msg.includes('unauthorized');
|
|
118
|
-
if (isRetryable && attempt < RETRY_COUNT - 1) {
|
|
119
|
-
await sleep(RETRY_DELAY_MS);
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
throw err;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
throw new Error('wmux is not running. Start the app first.');
|
|
127
|
-
}
|
package/src/preload/index.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { contextBridge, ipcRenderer } from 'electron';
|
|
2
|
-
import { IPC } from '../shared/constants';
|
|
3
|
-
|
|
4
|
-
const electronAPI = {
|
|
5
|
-
pty: {
|
|
6
|
-
create: (options?: { shell?: string; cwd?: string; cols?: number; rows?: number }) =>
|
|
7
|
-
ipcRenderer.invoke(IPC.PTY_CREATE, options),
|
|
8
|
-
write: (id: string, data: string) =>
|
|
9
|
-
ipcRenderer.invoke(IPC.PTY_WRITE, id, data),
|
|
10
|
-
resize: (id: string, cols: number, rows: number) =>
|
|
11
|
-
ipcRenderer.invoke(IPC.PTY_RESIZE, id, cols, rows),
|
|
12
|
-
dispose: (id: string) =>
|
|
13
|
-
ipcRenderer.invoke(IPC.PTY_DISPOSE, id),
|
|
14
|
-
onData: (callback: (id: string, data: string) => void) => {
|
|
15
|
-
const listener = (_event: Electron.IpcRendererEvent, id: string, data: string) => callback(id, data);
|
|
16
|
-
ipcRenderer.on(IPC.PTY_DATA, listener);
|
|
17
|
-
return () => { ipcRenderer.removeListener(IPC.PTY_DATA, listener); };
|
|
18
|
-
},
|
|
19
|
-
onExit: (callback: (id: string, exitCode: number) => void) => {
|
|
20
|
-
const listener = (_event: Electron.IpcRendererEvent, id: string, exitCode: number) => callback(id, exitCode);
|
|
21
|
-
ipcRenderer.on(IPC.PTY_EXIT, listener);
|
|
22
|
-
return () => { ipcRenderer.removeListener(IPC.PTY_EXIT, listener); };
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
shell: {
|
|
26
|
-
list: () => ipcRenderer.invoke(IPC.SHELL_LIST) as Promise<{ name: string; path: string; args?: string[] }[]>,
|
|
27
|
-
},
|
|
28
|
-
session: {
|
|
29
|
-
save: (data: unknown) => ipcRenderer.invoke(IPC.SESSION_SAVE, data),
|
|
30
|
-
load: () => ipcRenderer.invoke(IPC.SESSION_LOAD),
|
|
31
|
-
},
|
|
32
|
-
settings: {
|
|
33
|
-
setToastEnabled: (enabled: boolean) => ipcRenderer.send(IPC.TOAST_ENABLED, enabled),
|
|
34
|
-
},
|
|
35
|
-
notification: {
|
|
36
|
-
onNew: (callback: (ptyId: string, data: { type: string; title: string; body: string }) => void) => {
|
|
37
|
-
const listener = (_event: Electron.IpcRendererEvent, ptyId: string, data: { type: string; title: string; body: string }) =>
|
|
38
|
-
callback(ptyId, data);
|
|
39
|
-
ipcRenderer.on(IPC.NOTIFICATION, listener);
|
|
40
|
-
return () => { ipcRenderer.removeListener(IPC.NOTIFICATION, listener); };
|
|
41
|
-
},
|
|
42
|
-
onCwdChanged: (callback: (ptyId: string, cwd: string) => void) => {
|
|
43
|
-
const listener = (_event: Electron.IpcRendererEvent, ptyId: string, cwd: string) =>
|
|
44
|
-
callback(ptyId, cwd);
|
|
45
|
-
ipcRenderer.on(IPC.CWD_CHANGED, listener);
|
|
46
|
-
return () => { ipcRenderer.removeListener(IPC.CWD_CHANGED, listener); };
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
metadata: {
|
|
50
|
-
request: (ptyId: string) =>
|
|
51
|
-
ipcRenderer.invoke(IPC.METADATA_REQUEST, ptyId),
|
|
52
|
-
onUpdate: (callback: (ptyId: string, data: { gitBranch?: string; cwd?: string; listeningPorts?: number[] }) => void) => {
|
|
53
|
-
const listener = (_event: Electron.IpcRendererEvent, ptyId: string, data: { gitBranch?: string; cwd?: string; listeningPorts?: number[] }) =>
|
|
54
|
-
callback(ptyId, data);
|
|
55
|
-
ipcRenderer.on(IPC.METADATA_UPDATE, listener);
|
|
56
|
-
return () => { ipcRenderer.removeListener(IPC.METADATA_UPDATE, listener); };
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
rpc: {
|
|
60
|
-
onCommand: (
|
|
61
|
-
callback: (requestId: string, method: string, params: Record<string, unknown>) => void,
|
|
62
|
-
) => {
|
|
63
|
-
const listener = (
|
|
64
|
-
_event: Electron.IpcRendererEvent,
|
|
65
|
-
requestId: string,
|
|
66
|
-
method: string,
|
|
67
|
-
params: Record<string, unknown>,
|
|
68
|
-
) => callback(requestId, method, params);
|
|
69
|
-
ipcRenderer.on(IPC.RPC_COMMAND, listener);
|
|
70
|
-
return () => { ipcRenderer.removeListener(IPC.RPC_COMMAND, listener); };
|
|
71
|
-
},
|
|
72
|
-
respond: (requestId: string, result: unknown) =>
|
|
73
|
-
ipcRenderer.send(`${IPC.RPC_RESPONSE}:${requestId}`, result),
|
|
74
|
-
},
|
|
75
|
-
updater: {
|
|
76
|
-
checkForUpdates: () =>
|
|
77
|
-
ipcRenderer.invoke(IPC.UPDATE_CHECK) as Promise<{ status: string }>,
|
|
78
|
-
installUpdate: () =>
|
|
79
|
-
ipcRenderer.invoke(IPC.UPDATE_INSTALL),
|
|
80
|
-
onUpdateAvailable: (callback: (data: { status: string; releaseName?: string }) => void) => {
|
|
81
|
-
const listener = (_event: Electron.IpcRendererEvent, data: { status: string; releaseName?: string }) =>
|
|
82
|
-
callback(data);
|
|
83
|
-
ipcRenderer.on(IPC.UPDATE_AVAILABLE, listener);
|
|
84
|
-
return () => { ipcRenderer.removeListener(IPC.UPDATE_AVAILABLE, listener); };
|
|
85
|
-
},
|
|
86
|
-
onUpdateNotAvailable: (callback: (data: { status: string }) => void) => {
|
|
87
|
-
const listener = (_event: Electron.IpcRendererEvent, data: { status: string }) =>
|
|
88
|
-
callback(data);
|
|
89
|
-
ipcRenderer.on(IPC.UPDATE_NOT_AVAILABLE, listener);
|
|
90
|
-
return () => { ipcRenderer.removeListener(IPC.UPDATE_NOT_AVAILABLE, listener); };
|
|
91
|
-
},
|
|
92
|
-
onUpdateError: (callback: (data: { status: string; message: string }) => void) => {
|
|
93
|
-
const listener = (_event: Electron.IpcRendererEvent, data: { status: string; message: string }) =>
|
|
94
|
-
callback(data);
|
|
95
|
-
ipcRenderer.on(IPC.UPDATE_ERROR, listener);
|
|
96
|
-
return () => { ipcRenderer.removeListener(IPC.UPDATE_ERROR, listener); };
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
contextBridge.exposeInMainWorld('electronAPI', electronAPI);
|
|
102
|
-
|
|
103
|
-
// Expose clipboard via IPC for reliable copy/paste in terminal
|
|
104
|
-
const clipboardAPI = {
|
|
105
|
-
writeText: (text: string) => ipcRenderer.invoke(IPC.CLIPBOARD_WRITE, text),
|
|
106
|
-
readText: () => ipcRenderer.invoke(IPC.CLIPBOARD_READ) as Promise<string>,
|
|
107
|
-
};
|
|
108
|
-
contextBridge.exposeInMainWorld('clipboardAPI', clipboardAPI);
|
|
109
|
-
|
|
110
|
-
export type ElectronAPI = typeof electronAPI;
|
|
111
|
-
export type ClipboardAPI = typeof clipboardAPI;
|