@wong2kim/wmux 1.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 +157 -0
- package/assets/icon.ico +0 -0
- package/assets/icon.svg +6 -0
- package/dist/cli/cli/client.js +102 -0
- package/dist/cli/cli/commands/browser.js +137 -0
- package/dist/cli/cli/commands/input.js +80 -0
- package/dist/cli/cli/commands/notify.js +28 -0
- package/dist/cli/cli/commands/pane.js +88 -0
- package/dist/cli/cli/commands/surface.js +98 -0
- package/dist/cli/cli/commands/system.js +98 -0
- package/dist/cli/cli/commands/workspace.js +117 -0
- package/dist/cli/cli/index.js +140 -0
- package/dist/cli/cli/utils.js +47 -0
- package/dist/cli/shared/constants.js +54 -0
- package/dist/cli/shared/rpc.js +33 -0
- package/dist/cli/shared/types.js +79 -0
- package/dist/mcp/mcp/index.js +60 -0
- package/dist/mcp/mcp/wmux-client.js +146 -0
- package/dist/mcp/shared/constants.js +54 -0
- package/dist/mcp/shared/rpc.js +33 -0
- package/dist/mcp/shared/types.js +79 -0
- package/forge.config.ts +61 -0
- package/index.html +12 -0
- package/package.json +84 -0
- package/postcss.config.js +6 -0
- package/src/cli/client.ts +76 -0
- package/src/cli/commands/browser.ts +128 -0
- package/src/cli/commands/input.ts +72 -0
- package/src/cli/commands/notify.ts +29 -0
- package/src/cli/commands/pane.ts +90 -0
- package/src/cli/commands/surface.ts +102 -0
- package/src/cli/commands/system.ts +95 -0
- package/src/cli/commands/workspace.ts +116 -0
- package/src/cli/index.ts +145 -0
- package/src/cli/utils.ts +44 -0
- package/src/main/index.ts +86 -0
- package/src/main/ipc/handlers/clipboard.handler.ts +20 -0
- package/src/main/ipc/handlers/metadata.handler.ts +56 -0
- package/src/main/ipc/handlers/pty.handler.ts +69 -0
- package/src/main/ipc/handlers/session.handler.ts +17 -0
- package/src/main/ipc/handlers/shell.handler.ts +11 -0
- package/src/main/ipc/registerHandlers.ts +31 -0
- package/src/main/mcp/McpRegistrar.ts +156 -0
- package/src/main/metadata/MetadataCollector.ts +58 -0
- package/src/main/notification/ToastManager.ts +32 -0
- package/src/main/pipe/PipeServer.ts +190 -0
- package/src/main/pipe/RpcRouter.ts +46 -0
- package/src/main/pipe/handlers/_bridge.ts +40 -0
- package/src/main/pipe/handlers/browser.rpc.ts +132 -0
- package/src/main/pipe/handlers/input.rpc.ts +120 -0
- package/src/main/pipe/handlers/meta.rpc.ts +59 -0
- package/src/main/pipe/handlers/notify.rpc.ts +53 -0
- package/src/main/pipe/handlers/pane.rpc.ts +39 -0
- package/src/main/pipe/handlers/surface.rpc.ts +43 -0
- package/src/main/pipe/handlers/system.rpc.ts +36 -0
- package/src/main/pipe/handlers/workspace.rpc.ts +52 -0
- package/src/main/pty/AgentDetector.ts +247 -0
- package/src/main/pty/OscParser.ts +81 -0
- package/src/main/pty/PTYBridge.ts +88 -0
- package/src/main/pty/PTYManager.ts +104 -0
- package/src/main/pty/ShellDetector.ts +63 -0
- package/src/main/session/SessionManager.ts +53 -0
- package/src/main/updater/AutoUpdater.ts +132 -0
- package/src/main/window/createWindow.ts +71 -0
- package/src/mcp/README.md +56 -0
- package/src/mcp/index.ts +153 -0
- package/src/mcp/wmux-client.ts +127 -0
- package/src/preload/index.ts +111 -0
- package/src/preload/preload.ts +108 -0
- package/src/renderer/App.tsx +5 -0
- package/src/renderer/components/Browser/BrowserPanel.tsx +219 -0
- package/src/renderer/components/Browser/BrowserToolbar.tsx +253 -0
- package/src/renderer/components/Company/ApprovalDialog.tsx +3 -0
- package/src/renderer/components/Company/CompanyView.tsx +7 -0
- package/src/renderer/components/Company/MessageFeedPanel.tsx +3 -0
- package/src/renderer/components/Layout/AppLayout.tsx +234 -0
- package/src/renderer/components/Notification/NotificationPanel.tsx +129 -0
- package/src/renderer/components/Palette/CommandPalette.tsx +409 -0
- package/src/renderer/components/Palette/PaletteItem.tsx +55 -0
- package/src/renderer/components/Pane/Pane.tsx +122 -0
- package/src/renderer/components/Pane/PaneContainer.tsx +41 -0
- package/src/renderer/components/Pane/SurfaceTabs.tsx +46 -0
- package/src/renderer/components/Settings/SettingsPanel.tsx +886 -0
- package/src/renderer/components/Sidebar/MiniSidebar.tsx +67 -0
- package/src/renderer/components/Sidebar/Sidebar.tsx +84 -0
- package/src/renderer/components/Sidebar/WorkspaceItem.tsx +241 -0
- package/src/renderer/components/StatusBar/StatusBar.tsx +93 -0
- package/src/renderer/components/Terminal/SearchBar.tsx +126 -0
- package/src/renderer/components/Terminal/Terminal.tsx +102 -0
- package/src/renderer/components/Terminal/ViCopyMode.tsx +104 -0
- package/src/renderer/hooks/useKeyboard.ts +310 -0
- package/src/renderer/hooks/useNotificationListener.ts +80 -0
- package/src/renderer/hooks/useNotificationSound.ts +75 -0
- package/src/renderer/hooks/useRpcBridge.ts +451 -0
- package/src/renderer/hooks/useT.ts +11 -0
- package/src/renderer/hooks/useTerminal.ts +349 -0
- package/src/renderer/hooks/useViCopyMode.ts +320 -0
- package/src/renderer/i18n/index.ts +69 -0
- package/src/renderer/i18n/locales/en.ts +157 -0
- package/src/renderer/i18n/locales/ja.ts +155 -0
- package/src/renderer/i18n/locales/ko.ts +155 -0
- package/src/renderer/i18n/locales/zh.ts +155 -0
- package/src/renderer/index.tsx +6 -0
- package/src/renderer/stores/index.ts +19 -0
- package/src/renderer/stores/slices/notificationSlice.ts +56 -0
- package/src/renderer/stores/slices/paneSlice.ts +141 -0
- package/src/renderer/stores/slices/surfaceSlice.ts +122 -0
- package/src/renderer/stores/slices/uiSlice.ts +247 -0
- package/src/renderer/stores/slices/workspaceSlice.ts +120 -0
- package/src/renderer/styles/globals.css +150 -0
- package/src/renderer/themes.ts +99 -0
- package/src/shared/constants.ts +53 -0
- package/src/shared/electron.d.ts +11 -0
- package/src/shared/rpc.ts +71 -0
- package/src/shared/types.ts +176 -0
- package/tailwind.config.js +11 -0
- package/tsconfig.cli.json +24 -0
- package/tsconfig.json +21 -0
- package/tsconfig.mcp.json +25 -0
- package/vite.main.config.ts +14 -0
- package/vite.preload.config.ts +9 -0
- package/vite.renderer.config.ts +6 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as pty from 'node-pty';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { getPipeName, ENV_KEYS } from '../../shared/constants';
|
|
4
|
+
|
|
5
|
+
export interface PTYInstance {
|
|
6
|
+
id: string;
|
|
7
|
+
process: pty.IPty;
|
|
8
|
+
shell: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class PTYManager {
|
|
12
|
+
private instances = new Map<string, PTYInstance>();
|
|
13
|
+
private nextId = 0;
|
|
14
|
+
private _authToken: string | undefined;
|
|
15
|
+
|
|
16
|
+
setAuthToken(token: string): void {
|
|
17
|
+
this._authToken = token;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
create(options?: {
|
|
21
|
+
shell?: string;
|
|
22
|
+
cwd?: string;
|
|
23
|
+
cols?: number;
|
|
24
|
+
rows?: number;
|
|
25
|
+
workspaceId?: string;
|
|
26
|
+
surfaceId?: string;
|
|
27
|
+
authToken?: string;
|
|
28
|
+
}): PTYInstance {
|
|
29
|
+
const id = `pty-${++this.nextId}`;
|
|
30
|
+
const shell = options?.shell || this.getDefaultShell();
|
|
31
|
+
const cwd = options?.cwd || os.homedir();
|
|
32
|
+
|
|
33
|
+
// Filter out ELECTRON_ variables to prevent leaking internal state to child processes
|
|
34
|
+
const env: Record<string, string> = {};
|
|
35
|
+
for (const [key, value] of Object.entries(globalThis.process.env)) {
|
|
36
|
+
if (value === undefined) continue;
|
|
37
|
+
if (key.startsWith('ELECTRON_')) continue;
|
|
38
|
+
env[key] = value;
|
|
39
|
+
}
|
|
40
|
+
env[ENV_KEYS.SOCKET_PATH] = getPipeName();
|
|
41
|
+
if (options?.workspaceId) env[ENV_KEYS.WORKSPACE_ID] = options.workspaceId;
|
|
42
|
+
if (options?.surfaceId) env[ENV_KEYS.SURFACE_ID] = options.surfaceId;
|
|
43
|
+
const authToken = options?.authToken || this._authToken;
|
|
44
|
+
if (authToken) env[ENV_KEYS.AUTH_TOKEN] = authToken;
|
|
45
|
+
|
|
46
|
+
const process = pty.spawn(shell, [], {
|
|
47
|
+
name: 'xterm-256color',
|
|
48
|
+
cols: options?.cols || 80,
|
|
49
|
+
rows: options?.rows || 24,
|
|
50
|
+
cwd,
|
|
51
|
+
env,
|
|
52
|
+
useConpty: true,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const instance: PTYInstance = { id, process, shell };
|
|
56
|
+
this.instances.set(id, instance);
|
|
57
|
+
return instance;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
write(id: string, data: string): void {
|
|
61
|
+
const instance = this.instances.get(id);
|
|
62
|
+
if (instance) {
|
|
63
|
+
instance.process.write(data);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
resize(id: string, cols: number, rows: number): void {
|
|
68
|
+
const instance = this.instances.get(id);
|
|
69
|
+
if (instance) {
|
|
70
|
+
instance.process.resize(cols, rows);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
dispose(id: string): void {
|
|
75
|
+
const instance = this.instances.get(id);
|
|
76
|
+
if (instance) {
|
|
77
|
+
try { instance.process.kill(); } catch { /* already dead */ }
|
|
78
|
+
this.instances.delete(id);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Remove an entry from the map without killing — use when the process has already exited. */
|
|
83
|
+
remove(id: string): void {
|
|
84
|
+
this.instances.delete(id);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get(id: string): PTYInstance | undefined {
|
|
88
|
+
return this.instances.get(id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
disposeAll(): void {
|
|
92
|
+
for (const [id] of this.instances) {
|
|
93
|
+
this.dispose(id);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private getDefaultShell(): string {
|
|
98
|
+
if (process.platform === 'win32') {
|
|
99
|
+
// PowerShell 우선 (cd로 드라이브 전환 자동 지원)
|
|
100
|
+
return 'powershell.exe';
|
|
101
|
+
}
|
|
102
|
+
return process.env.SHELL || '/bin/bash';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface ShellInfo {
|
|
5
|
+
name: string;
|
|
6
|
+
path: string;
|
|
7
|
+
args?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ShellDetector {
|
|
11
|
+
detect(): ShellInfo[] {
|
|
12
|
+
const shells: ShellInfo[] = [];
|
|
13
|
+
|
|
14
|
+
// PowerShell 7+ (pwsh)
|
|
15
|
+
const pwshPaths = [
|
|
16
|
+
'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
|
|
17
|
+
path.join(process.env.LOCALAPPDATA || '', 'Microsoft\\WindowsApps\\pwsh.exe'),
|
|
18
|
+
];
|
|
19
|
+
for (const p of pwshPaths) {
|
|
20
|
+
if (fs.existsSync(p)) {
|
|
21
|
+
shells.push({ name: 'PowerShell 7', path: p });
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Windows PowerShell 5.1
|
|
27
|
+
const ps5 = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32\\WindowsPowerShell\\v1.0\\powershell.exe');
|
|
28
|
+
if (fs.existsSync(ps5)) {
|
|
29
|
+
shells.push({ name: 'Windows PowerShell', path: ps5 });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Git Bash
|
|
33
|
+
const gitBashPaths = [
|
|
34
|
+
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
35
|
+
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
|
36
|
+
];
|
|
37
|
+
for (const p of gitBashPaths) {
|
|
38
|
+
if (fs.existsSync(p)) {
|
|
39
|
+
shells.push({ name: 'Git Bash', path: p, args: ['--login', '-i'] });
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// WSL
|
|
45
|
+
const wslPath = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32\\wsl.exe');
|
|
46
|
+
if (fs.existsSync(wslPath)) {
|
|
47
|
+
shells.push({ name: 'WSL', path: wslPath });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// cmd.exe
|
|
51
|
+
const cmd = process.env.COMSPEC || path.join(process.env.SystemRoot || 'C:\\Windows', 'System32\\cmd.exe');
|
|
52
|
+
if (fs.existsSync(cmd)) {
|
|
53
|
+
shells.push({ name: 'Command Prompt', path: cmd });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return shells;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getDefault(): string {
|
|
60
|
+
const shells = this.detect();
|
|
61
|
+
return shells.length > 0 ? shells[0].path : 'powershell.exe';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { app } from 'electron';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import type { SessionData } from '../../shared/types';
|
|
5
|
+
|
|
6
|
+
export class SessionManager {
|
|
7
|
+
private filePath: string;
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
this.filePath = path.join(app.getPath('userData'), 'session.json');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
save(data: SessionData): void {
|
|
14
|
+
try {
|
|
15
|
+
const dir = path.dirname(this.filePath);
|
|
16
|
+
if (!fs.existsSync(dir)) {
|
|
17
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error('Failed to save session:', err);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
load(): SessionData | null {
|
|
26
|
+
try {
|
|
27
|
+
if (!fs.existsSync(this.filePath)) return null;
|
|
28
|
+
const raw = fs.readFileSync(this.filePath, 'utf-8');
|
|
29
|
+
|
|
30
|
+
// Guard against prototype pollution via JSON reviver
|
|
31
|
+
const parsed: unknown = JSON.parse(raw, (key, value) => {
|
|
32
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') return undefined;
|
|
33
|
+
return value;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Basic schema validation
|
|
37
|
+
if (
|
|
38
|
+
typeof parsed !== 'object' ||
|
|
39
|
+
parsed === null ||
|
|
40
|
+
!Array.isArray((parsed as Record<string, unknown>)['workspaces']) ||
|
|
41
|
+
typeof (parsed as Record<string, unknown>)['activeWorkspaceId'] !== 'string'
|
|
42
|
+
) {
|
|
43
|
+
console.warn('[SessionManager] Session file failed schema validation — discarding.');
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return parsed as SessionData;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error('Failed to load session:', err);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
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
|
+
});
|