@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.
Files changed (122) hide show
  1. package/README.md +157 -0
  2. package/assets/icon.ico +0 -0
  3. package/assets/icon.svg +6 -0
  4. package/dist/cli/cli/client.js +102 -0
  5. package/dist/cli/cli/commands/browser.js +137 -0
  6. package/dist/cli/cli/commands/input.js +80 -0
  7. package/dist/cli/cli/commands/notify.js +28 -0
  8. package/dist/cli/cli/commands/pane.js +88 -0
  9. package/dist/cli/cli/commands/surface.js +98 -0
  10. package/dist/cli/cli/commands/system.js +98 -0
  11. package/dist/cli/cli/commands/workspace.js +117 -0
  12. package/dist/cli/cli/index.js +140 -0
  13. package/dist/cli/cli/utils.js +47 -0
  14. package/dist/cli/shared/constants.js +54 -0
  15. package/dist/cli/shared/rpc.js +33 -0
  16. package/dist/cli/shared/types.js +79 -0
  17. package/dist/mcp/mcp/index.js +60 -0
  18. package/dist/mcp/mcp/wmux-client.js +146 -0
  19. package/dist/mcp/shared/constants.js +54 -0
  20. package/dist/mcp/shared/rpc.js +33 -0
  21. package/dist/mcp/shared/types.js +79 -0
  22. package/forge.config.ts +61 -0
  23. package/index.html +12 -0
  24. package/package.json +84 -0
  25. package/postcss.config.js +6 -0
  26. package/src/cli/client.ts +76 -0
  27. package/src/cli/commands/browser.ts +128 -0
  28. package/src/cli/commands/input.ts +72 -0
  29. package/src/cli/commands/notify.ts +29 -0
  30. package/src/cli/commands/pane.ts +90 -0
  31. package/src/cli/commands/surface.ts +102 -0
  32. package/src/cli/commands/system.ts +95 -0
  33. package/src/cli/commands/workspace.ts +116 -0
  34. package/src/cli/index.ts +145 -0
  35. package/src/cli/utils.ts +44 -0
  36. package/src/main/index.ts +86 -0
  37. package/src/main/ipc/handlers/clipboard.handler.ts +20 -0
  38. package/src/main/ipc/handlers/metadata.handler.ts +56 -0
  39. package/src/main/ipc/handlers/pty.handler.ts +69 -0
  40. package/src/main/ipc/handlers/session.handler.ts +17 -0
  41. package/src/main/ipc/handlers/shell.handler.ts +11 -0
  42. package/src/main/ipc/registerHandlers.ts +31 -0
  43. package/src/main/mcp/McpRegistrar.ts +156 -0
  44. package/src/main/metadata/MetadataCollector.ts +58 -0
  45. package/src/main/notification/ToastManager.ts +32 -0
  46. package/src/main/pipe/PipeServer.ts +190 -0
  47. package/src/main/pipe/RpcRouter.ts +46 -0
  48. package/src/main/pipe/handlers/_bridge.ts +40 -0
  49. package/src/main/pipe/handlers/browser.rpc.ts +132 -0
  50. package/src/main/pipe/handlers/input.rpc.ts +120 -0
  51. package/src/main/pipe/handlers/meta.rpc.ts +59 -0
  52. package/src/main/pipe/handlers/notify.rpc.ts +53 -0
  53. package/src/main/pipe/handlers/pane.rpc.ts +39 -0
  54. package/src/main/pipe/handlers/surface.rpc.ts +43 -0
  55. package/src/main/pipe/handlers/system.rpc.ts +36 -0
  56. package/src/main/pipe/handlers/workspace.rpc.ts +52 -0
  57. package/src/main/pty/AgentDetector.ts +247 -0
  58. package/src/main/pty/OscParser.ts +81 -0
  59. package/src/main/pty/PTYBridge.ts +88 -0
  60. package/src/main/pty/PTYManager.ts +104 -0
  61. package/src/main/pty/ShellDetector.ts +63 -0
  62. package/src/main/session/SessionManager.ts +53 -0
  63. package/src/main/updater/AutoUpdater.ts +132 -0
  64. package/src/main/window/createWindow.ts +71 -0
  65. package/src/mcp/README.md +56 -0
  66. package/src/mcp/index.ts +153 -0
  67. package/src/mcp/wmux-client.ts +127 -0
  68. package/src/preload/index.ts +111 -0
  69. package/src/preload/preload.ts +108 -0
  70. package/src/renderer/App.tsx +5 -0
  71. package/src/renderer/components/Browser/BrowserPanel.tsx +219 -0
  72. package/src/renderer/components/Browser/BrowserToolbar.tsx +253 -0
  73. package/src/renderer/components/Company/ApprovalDialog.tsx +3 -0
  74. package/src/renderer/components/Company/CompanyView.tsx +7 -0
  75. package/src/renderer/components/Company/MessageFeedPanel.tsx +3 -0
  76. package/src/renderer/components/Layout/AppLayout.tsx +234 -0
  77. package/src/renderer/components/Notification/NotificationPanel.tsx +129 -0
  78. package/src/renderer/components/Palette/CommandPalette.tsx +409 -0
  79. package/src/renderer/components/Palette/PaletteItem.tsx +55 -0
  80. package/src/renderer/components/Pane/Pane.tsx +122 -0
  81. package/src/renderer/components/Pane/PaneContainer.tsx +41 -0
  82. package/src/renderer/components/Pane/SurfaceTabs.tsx +46 -0
  83. package/src/renderer/components/Settings/SettingsPanel.tsx +886 -0
  84. package/src/renderer/components/Sidebar/MiniSidebar.tsx +67 -0
  85. package/src/renderer/components/Sidebar/Sidebar.tsx +84 -0
  86. package/src/renderer/components/Sidebar/WorkspaceItem.tsx +241 -0
  87. package/src/renderer/components/StatusBar/StatusBar.tsx +93 -0
  88. package/src/renderer/components/Terminal/SearchBar.tsx +126 -0
  89. package/src/renderer/components/Terminal/Terminal.tsx +102 -0
  90. package/src/renderer/components/Terminal/ViCopyMode.tsx +104 -0
  91. package/src/renderer/hooks/useKeyboard.ts +310 -0
  92. package/src/renderer/hooks/useNotificationListener.ts +80 -0
  93. package/src/renderer/hooks/useNotificationSound.ts +75 -0
  94. package/src/renderer/hooks/useRpcBridge.ts +451 -0
  95. package/src/renderer/hooks/useT.ts +11 -0
  96. package/src/renderer/hooks/useTerminal.ts +349 -0
  97. package/src/renderer/hooks/useViCopyMode.ts +320 -0
  98. package/src/renderer/i18n/index.ts +69 -0
  99. package/src/renderer/i18n/locales/en.ts +157 -0
  100. package/src/renderer/i18n/locales/ja.ts +155 -0
  101. package/src/renderer/i18n/locales/ko.ts +155 -0
  102. package/src/renderer/i18n/locales/zh.ts +155 -0
  103. package/src/renderer/index.tsx +6 -0
  104. package/src/renderer/stores/index.ts +19 -0
  105. package/src/renderer/stores/slices/notificationSlice.ts +56 -0
  106. package/src/renderer/stores/slices/paneSlice.ts +141 -0
  107. package/src/renderer/stores/slices/surfaceSlice.ts +122 -0
  108. package/src/renderer/stores/slices/uiSlice.ts +247 -0
  109. package/src/renderer/stores/slices/workspaceSlice.ts +120 -0
  110. package/src/renderer/styles/globals.css +150 -0
  111. package/src/renderer/themes.ts +99 -0
  112. package/src/shared/constants.ts +53 -0
  113. package/src/shared/electron.d.ts +11 -0
  114. package/src/shared/rpc.ts +71 -0
  115. package/src/shared/types.ts +176 -0
  116. package/tailwind.config.js +11 -0
  117. package/tsconfig.cli.json +24 -0
  118. package/tsconfig.json +21 -0
  119. package/tsconfig.mcp.json +25 -0
  120. package/vite.main.config.ts +14 -0
  121. package/vite.preload.config.ts +9 -0
  122. package/vite.renderer.config.ts +6 -0
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+
3
+ process.on('SIGINT', () => {
4
+ process.exit(130);
5
+ });
6
+
7
+ import { hasFlag } from './utils';
8
+ import { handleWorkspace } from './commands/workspace';
9
+ import { handleSurface } from './commands/surface';
10
+ import { handlePane } from './commands/pane';
11
+ import { handleInput } from './commands/input';
12
+ import { handleNotify } from './commands/notify';
13
+ import { handleSystem } from './commands/system';
14
+ import { handleBrowser } from './commands/browser';
15
+
16
+ const HELP_TEXT = `
17
+ wmux CLI
18
+
19
+ USAGE
20
+ wmux <command> [options]
21
+
22
+ WORKSPACE COMMANDS
23
+ list-workspaces List all workspaces
24
+ new-workspace [--name <name>] Create a new workspace
25
+ focus-workspace <id> Focus a workspace by ID
26
+ close-workspace <id> Close a workspace by ID
27
+ current-workspace Show the active workspace
28
+
29
+ SURFACE COMMANDS
30
+ list-surfaces List all surfaces in the active workspace
31
+ new-surface Open a new surface (terminal tab)
32
+ focus-surface <id> Focus a surface by ID
33
+ close-surface <id> Close a surface by ID
34
+
35
+ PANE COMMANDS
36
+ list-panes List all panes in the active workspace
37
+ focus-pane <id> Focus a pane by ID
38
+ split [--direction right|down] Split the active pane (default: right)
39
+
40
+ INPUT COMMANDS
41
+ send <text> Send text to the active terminal
42
+ send-key <keystroke> Send a key (e.g. Enter, ctrl-c, Tab)
43
+ read-screen Read the current terminal screen content
44
+
45
+ NOTIFICATION COMMANDS
46
+ notify --title <title> --body <body> Show a notification in wmux
47
+
48
+ SYSTEM COMMANDS
49
+ set-status <text> Set a status message on the active workspace
50
+ set-progress <0-100> Set a progress value on the active workspace
51
+ identify Show wmux app info
52
+ capabilities List all supported RPC methods
53
+
54
+ BROWSER COMMANDS
55
+ browser snapshot Return the full page HTML of the active browser surface
56
+ browser click <selector> Click an element by CSS selector
57
+ browser fill <selector> <text> Fill an input field by CSS selector
58
+ browser eval <code> Execute JavaScript in the browser context
59
+ browser navigate <url> Navigate the browser surface to a URL
60
+
61
+ GLOBAL FLAGS
62
+ --json Output raw JSON (useful for scripting)
63
+ --help Show this help text
64
+
65
+ EXAMPLES
66
+ wmux list-workspaces
67
+ wmux new-workspace --name dev
68
+ wmux send "echo hello"
69
+ wmux notify --title "Done" --body "Build finished"
70
+ wmux identify --json
71
+ wmux browser snapshot
72
+ wmux browser navigate "https://example.com"
73
+ wmux browser click "#login-btn"
74
+ `.trimStart();
75
+
76
+ const WORKSPACE_CMDS = new Set([
77
+ 'list-workspaces',
78
+ 'new-workspace',
79
+ 'focus-workspace',
80
+ 'close-workspace',
81
+ 'current-workspace',
82
+ ]);
83
+
84
+ const SURFACE_CMDS = new Set([
85
+ 'list-surfaces',
86
+ 'new-surface',
87
+ 'focus-surface',
88
+ 'close-surface',
89
+ ]);
90
+
91
+ const PANE_CMDS = new Set(['list-panes', 'focus-pane', 'split']);
92
+
93
+ const INPUT_CMDS = new Set(['send', 'send-key', 'read-screen']);
94
+
95
+ const SYSTEM_CMDS = new Set([
96
+ 'identify',
97
+ 'capabilities',
98
+ 'set-status',
99
+ 'set-progress',
100
+ ]);
101
+
102
+ async function main(): Promise<void> {
103
+ // process.argv = ['node', 'index.js', ...userArgs]
104
+ const argv = process.argv.slice(2);
105
+
106
+ if (argv.length === 0 || hasFlag(argv, '--help') || hasFlag(argv, '-h')) {
107
+ process.stdout.write(HELP_TEXT);
108
+ process.exit(0);
109
+ }
110
+
111
+ const jsonMode = hasFlag(argv, '--json');
112
+
113
+ // Strip global flags so commands see clean args
114
+ const args = argv.filter((a) => a !== '--json' && a !== '--help' && a !== '-h');
115
+
116
+ const cmd = args[0];
117
+ const rest = args.slice(1);
118
+
119
+ try {
120
+ if (WORKSPACE_CMDS.has(cmd)) {
121
+ await handleWorkspace(cmd, rest, jsonMode);
122
+ } else if (SURFACE_CMDS.has(cmd)) {
123
+ await handleSurface(cmd, rest, jsonMode);
124
+ } else if (PANE_CMDS.has(cmd)) {
125
+ await handlePane(cmd, rest, jsonMode);
126
+ } else if (INPUT_CMDS.has(cmd)) {
127
+ await handleInput(cmd, rest, jsonMode);
128
+ } else if (cmd === 'notify') {
129
+ await handleNotify(rest, jsonMode);
130
+ } else if (SYSTEM_CMDS.has(cmd)) {
131
+ await handleSystem(cmd, rest, jsonMode);
132
+ } else if (cmd === 'browser') {
133
+ await handleBrowser(rest, jsonMode);
134
+ } else {
135
+ console.error(`Unknown command: "${cmd}". Run 'wmux --help' for usage.`);
136
+ process.exit(1);
137
+ }
138
+ } catch (err: unknown) {
139
+ const message = err instanceof Error ? err.message : String(err);
140
+ console.error(message);
141
+ process.exit(1);
142
+ }
143
+ }
144
+
145
+ main();
@@ -0,0 +1,44 @@
1
+ import type { RpcResponse } from '../shared/rpc';
2
+
3
+ /**
4
+ * Print the result field of a successful RPC response as JSON.
5
+ * If the response contains an error, the error is printed to stderr and
6
+ * the process exits with code 1.
7
+ */
8
+ export function printResult(response: RpcResponse): void {
9
+ if (!response.ok) {
10
+ printError(response);
11
+ return;
12
+ }
13
+ console.log(JSON.stringify(response.result, null, 2));
14
+ }
15
+
16
+ /**
17
+ * Print the error field of a failed RPC response to stderr and exit with 1.
18
+ */
19
+ export function printError(response: RpcResponse): void {
20
+ const msg = !response.ok ? response.error : 'Unknown error from wmux';
21
+ console.error(`Error: ${msg}`);
22
+ process.exit(1);
23
+ }
24
+
25
+ /**
26
+ * Parse a named flag value from an argv array.
27
+ * e.g. parseFlag(['--name', 'dev'], '--name') => 'dev'
28
+ * Returns undefined when the flag is not present.
29
+ */
30
+ export function parseFlag(args: string[], flag: string): string | undefined {
31
+ const idx = args.indexOf(flag);
32
+ if (idx === -1) return undefined;
33
+ const value = args[idx + 1];
34
+ if (value === undefined || value.startsWith('-')) return undefined;
35
+ return value;
36
+ }
37
+
38
+ /**
39
+ * Check whether a bare flag is present in argv.
40
+ * e.g. hasFlag(['--json', 'identify'], '--json') => true
41
+ */
42
+ export function hasFlag(args: string[], flag: string): boolean {
43
+ return args.includes(flag);
44
+ }
@@ -0,0 +1,86 @@
1
+ process.on('unhandledRejection', (reason) => {
2
+ console.error('[Main] Unhandled rejection:', reason);
3
+ });
4
+ process.on('uncaughtException', (err) => {
5
+ console.error('[Main] Uncaught exception:', err);
6
+ });
7
+
8
+ import { app, BrowserWindow } from 'electron';
9
+ import started from 'electron-squirrel-startup';
10
+ import { createWindow } from './window/createWindow';
11
+ import { PTYManager } from './pty/PTYManager';
12
+ import { PTYBridge } from './pty/PTYBridge';
13
+ import { registerAllHandlers } from './ipc/registerHandlers';
14
+ import { RpcRouter } from './pipe/RpcRouter';
15
+ import { PipeServer } from './pipe/PipeServer';
16
+ import { registerWorkspaceRpc } from './pipe/handlers/workspace.rpc';
17
+ import { registerSurfaceRpc } from './pipe/handlers/surface.rpc';
18
+ import { registerPaneRpc } from './pipe/handlers/pane.rpc';
19
+ import { registerInputRpc } from './pipe/handlers/input.rpc';
20
+ import { registerNotifyRpc } from './pipe/handlers/notify.rpc';
21
+ import { registerMetaRpc } from './pipe/handlers/meta.rpc';
22
+ import { registerSystemRpc } from './pipe/handlers/system.rpc';
23
+ import { registerBrowserRpc } from './pipe/handlers/browser.rpc';
24
+ import { AutoUpdater } from './updater/AutoUpdater';
25
+ import { McpRegistrar } from './mcp/McpRegistrar';
26
+
27
+ if (started) {
28
+ app.quit();
29
+ }
30
+
31
+ const ptyManager = new PTYManager();
32
+ let mainWindow: BrowserWindow | null = null;
33
+ const ptyBridge = new PTYBridge(ptyManager, () => mainWindow);
34
+ const autoUpdater = new AutoUpdater(() => mainWindow);
35
+
36
+ const rpcRouter = new RpcRouter();
37
+ const pipeServer = new PipeServer(rpcRouter);
38
+ const mcpRegistrar = new McpRegistrar();
39
+
40
+ const cleanupHandlers = registerAllHandlers(ptyManager, ptyBridge, () => mainWindow);
41
+ registerWorkspaceRpc(rpcRouter, () => mainWindow);
42
+ registerSurfaceRpc(rpcRouter, () => mainWindow);
43
+ registerPaneRpc(rpcRouter, () => mainWindow);
44
+ registerInputRpc(rpcRouter, ptyManager, () => mainWindow);
45
+ registerNotifyRpc(rpcRouter, () => mainWindow);
46
+ registerMetaRpc(rpcRouter, () => mainWindow);
47
+ registerSystemRpc(rpcRouter);
48
+ registerBrowserRpc(rpcRouter, () => mainWindow);
49
+
50
+ app.on('ready', () => {
51
+ console.log('[Main] App ready, creating window...');
52
+ mainWindow = createWindow();
53
+ console.log('[Main] Window created:', !!mainWindow);
54
+ mainWindow.on('closed', () => {
55
+ mainWindow = null;
56
+ });
57
+ mainWindow.webContents.on('did-fail-load', (_e, code, desc) => {
58
+ console.error('[Main] Page failed to load:', code, desc);
59
+ });
60
+ mainWindow.webContents.on('did-finish-load', () => {
61
+ console.log('[Main] Page loaded successfully');
62
+ });
63
+ pipeServer.start();
64
+ const authToken = pipeServer.getAuthToken();
65
+ ptyManager.setAuthToken(authToken);
66
+ mcpRegistrar.register(authToken);
67
+ autoUpdater.start();
68
+ });
69
+
70
+ app.on('window-all-closed', () => {
71
+ app.quit();
72
+ });
73
+
74
+ app.on('before-quit', () => {
75
+ cleanupHandlers();
76
+ ptyManager.disposeAll();
77
+ pipeServer.stop();
78
+ mcpRegistrar.unregister();
79
+ autoUpdater.stop();
80
+ });
81
+
82
+ app.on('activate', () => {
83
+ if (BrowserWindow.getAllWindows().length === 0) {
84
+ mainWindow = createWindow();
85
+ }
86
+ });
@@ -0,0 +1,20 @@
1
+ import { ipcMain, clipboard } from 'electron';
2
+ import { IPC } from '../../../shared/constants';
3
+
4
+ export function registerClipboardHandlers(): void {
5
+ // Remove any previously registered handlers before re-registering.
6
+ // ipcMain.handle() throws if the same channel is registered twice (e.g.
7
+ // during dev HMR reloads), which silently kills clipboard IPC.
8
+ ipcMain.removeHandler(IPC.CLIPBOARD_WRITE);
9
+ ipcMain.removeHandler(IPC.CLIPBOARD_READ);
10
+
11
+ ipcMain.handle(IPC.CLIPBOARD_WRITE, (_event, text: string) => {
12
+ if (typeof text !== 'string') return;
13
+ if (text.length > 1_000_000) return; // 1MB limit
14
+ clipboard.writeText(text);
15
+ });
16
+
17
+ ipcMain.handle(IPC.CLIPBOARD_READ, () => {
18
+ return clipboard.readText();
19
+ });
20
+ }
@@ -0,0 +1,56 @@
1
+ import { ipcMain, BrowserWindow } from 'electron';
2
+ import { IPC } from '../../../shared/constants';
3
+ import { MetadataCollector } from '../../metadata/MetadataCollector';
4
+ import { PTYManager } from '../../pty/PTYManager';
5
+
6
+ const collector = new MetadataCollector();
7
+
8
+ // Track CWD per ptyId (updated via OSC 7 or polling)
9
+ const cwdMap = new Map<string, string>();
10
+
11
+ export function registerMetadataHandlers(
12
+ ptyManager: PTYManager,
13
+ getWindow: () => BrowserWindow | null,
14
+ ): () => void {
15
+ // Handle metadata request from renderer
16
+ ipcMain.handle(IPC.METADATA_REQUEST, async (_event, ptyId: string) => {
17
+ const cwd = cwdMap.get(ptyId);
18
+ return collector.collect(cwd);
19
+ });
20
+
21
+ // Listen for CWD changes from PTYBridge (via OscParser)
22
+ ipcMain.on(IPC.CWD_CHANGED, (_event, ptyId: string, cwd: string) => {
23
+ cwdMap.set(ptyId, cwd);
24
+ });
25
+
26
+ // Periodic metadata polling (every 5 seconds)
27
+ const pollingInterval = setInterval(async () => {
28
+ const win = getWindow();
29
+ if (!win || win.isDestroyed()) return;
30
+
31
+ for (const [ptyId] of cwdMap) {
32
+ const instance = ptyManager.get(ptyId);
33
+ if (!instance) {
34
+ cwdMap.delete(ptyId);
35
+ continue;
36
+ }
37
+
38
+ const cwd = cwdMap.get(ptyId);
39
+ if (cwd) {
40
+ const metadata = await collector.collect(cwd);
41
+ win.webContents.send(IPC.METADATA_UPDATE, ptyId, metadata);
42
+ }
43
+ }
44
+ }, 5000);
45
+
46
+ // cleanup 함수 반환 — 앱 종료 시 호출
47
+ return () => {
48
+ clearInterval(pollingInterval);
49
+ ipcMain.removeHandler(IPC.METADATA_REQUEST);
50
+ ipcMain.removeAllListeners(IPC.CWD_CHANGED);
51
+ };
52
+ }
53
+
54
+ export function updateCwd(ptyId: string, cwd: string): void {
55
+ cwdMap.set(ptyId, cwd);
56
+ }
@@ -0,0 +1,69 @@
1
+ import { ipcMain } from 'electron';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { PTYManager } from '../../pty/PTYManager';
5
+ import { PTYBridge } from '../../pty/PTYBridge';
6
+ import { IPC } from '../../../shared/constants';
7
+
8
+ /**
9
+ * Allowed shell basenames (case-insensitive on Windows).
10
+ * Only these executables may be spawned via IPC.
11
+ */
12
+ const ALLOWED_SHELLS = new Set([
13
+ 'powershell.exe',
14
+ 'pwsh.exe',
15
+ 'cmd.exe',
16
+ 'bash.exe',
17
+ 'wsl.exe',
18
+ 'git-bash.exe',
19
+ 'sh.exe',
20
+ ]);
21
+
22
+ function isAllowedShell(shell: string): boolean {
23
+ const basename = path.basename(shell).toLowerCase();
24
+ return ALLOWED_SHELLS.has(basename);
25
+ }
26
+
27
+ export function registerPTYHandlers(ptyManager: PTYManager, ptyBridge: PTYBridge): void {
28
+ ipcMain.handle(IPC.PTY_CREATE, (_event, options?: { shell?: string; cwd?: string; cols?: number; rows?: number }) => {
29
+ if (options?.shell !== undefined && !isAllowedShell(options.shell)) {
30
+ throw new Error(`PTY_CREATE: shell not allowed: ${options.shell}`);
31
+ }
32
+
33
+ // Validate workDir to block UNC paths and non-existent directories
34
+ let safeCwd: string | undefined;
35
+ if (options?.cwd) {
36
+ const resolved = path.resolve(options.cwd);
37
+ // Block UNC paths (e.g. \\server\share)
38
+ if (!resolved.startsWith('\\\\') && fs.existsSync(resolved)) {
39
+ const stat = fs.statSync(resolved);
40
+ if (stat.isDirectory()) {
41
+ safeCwd = resolved;
42
+ }
43
+ }
44
+ }
45
+
46
+ const instance = ptyManager.create(safeCwd !== undefined ? { ...options, cwd: safeCwd } : { ...options, cwd: undefined });
47
+ ptyBridge.setupDataForwarding(instance.id);
48
+ return { id: instance.id, shell: instance.shell };
49
+ });
50
+
51
+ ipcMain.handle(IPC.PTY_WRITE, (_event, id: string, data: string) => {
52
+ // 세션 복원 시 이전 ptyId가 남아있을 수 있음 — 조용히 무시
53
+ if (!ptyManager.get(id)) return;
54
+ if (typeof data !== 'string') return;
55
+ if (data.length > 100_000) return; // prevent mega-writes
56
+ ptyManager.write(id, data);
57
+ });
58
+
59
+ ipcMain.handle(IPC.PTY_RESIZE, (_event, id: string, cols: number, rows: number) => {
60
+ if (!Number.isInteger(cols) || cols <= 0 || !Number.isInteger(rows) || rows <= 0) {
61
+ throw new Error(`PTY_RESIZE: cols and rows must be positive integers (got cols=${cols}, rows=${rows})`);
62
+ }
63
+ ptyManager.resize(id, cols, rows);
64
+ });
65
+
66
+ ipcMain.handle(IPC.PTY_DISPOSE, (_event, id: string) => {
67
+ ptyManager.dispose(id);
68
+ });
69
+ }
@@ -0,0 +1,17 @@
1
+ import { ipcMain } from 'electron';
2
+ import { IPC } from '../../../shared/constants';
3
+ import { SessionManager } from '../../session/SessionManager';
4
+ import type { SessionData } from '../../../shared/types';
5
+
6
+ const sessionManager = new SessionManager();
7
+
8
+ export function registerSessionHandlers(): void {
9
+ ipcMain.handle(IPC.SESSION_SAVE, (_event, data: SessionData) => {
10
+ sessionManager.save(data);
11
+ return { success: true };
12
+ });
13
+
14
+ ipcMain.handle(IPC.SESSION_LOAD, () => {
15
+ return sessionManager.load();
16
+ });
17
+ }
@@ -0,0 +1,11 @@
1
+ import { ipcMain } from 'electron';
2
+ import { ShellDetector } from '../../pty/ShellDetector';
3
+ import { IPC } from '../../../shared/constants';
4
+
5
+ export function registerShellHandlers(): void {
6
+ const detector = new ShellDetector();
7
+
8
+ ipcMain.handle(IPC.SHELL_LIST, () => {
9
+ return detector.detect();
10
+ });
11
+ }
@@ -0,0 +1,31 @@
1
+ import { ipcMain, type BrowserWindow } from 'electron';
2
+ import { PTYManager } from '../pty/PTYManager';
3
+ import { PTYBridge } from '../pty/PTYBridge';
4
+ import { registerPTYHandlers } from './handlers/pty.handler';
5
+ import { registerSessionHandlers } from './handlers/session.handler';
6
+ import { registerShellHandlers } from './handlers/shell.handler';
7
+ import { registerMetadataHandlers } from './handlers/metadata.handler';
8
+ import { registerClipboardHandlers } from './handlers/clipboard.handler';
9
+ import { IPC } from '../../shared/constants';
10
+ import { toastManager } from '../pipe/handlers/notify.rpc';
11
+
12
+ export function registerAllHandlers(
13
+ ptyManager: PTYManager,
14
+ ptyBridge: PTYBridge,
15
+ getWindow: () => BrowserWindow | null,
16
+ ): () => void {
17
+ registerPTYHandlers(ptyManager, ptyBridge);
18
+ registerSessionHandlers();
19
+ registerShellHandlers();
20
+ const cleanupMetadata = registerMetadataHandlers(ptyManager, getWindow);
21
+ registerClipboardHandlers();
22
+
23
+ // Sync toast setting from renderer
24
+ ipcMain.on(IPC.TOAST_ENABLED, (_event, enabled: boolean) => {
25
+ toastManager.enabled = enabled;
26
+ });
27
+
28
+ return () => {
29
+ cleanupMetadata();
30
+ };
31
+ }
@@ -0,0 +1,156 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { app } from 'electron';
4
+ import { getAuthTokenPath } from '../../shared/constants';
5
+
6
+ /**
7
+ * Registers/unregisters the wmux MCP server in Claude Code's config files
8
+ * and writes the auth token to a well-known file so the MCP server can read it.
9
+ *
10
+ * The MCP server uses:
11
+ * - Fixed pipe path: \\.\pipe\wmux (from shared/constants)
12
+ * - Auth token file: ~/.wmux-auth-token (written here, read by MCP)
13
+ *
14
+ * Config files written:
15
+ * 1. ~/.claude/settings.json (global settings — mcpServers key)
16
+ * 2. ~/.claude/.mcp.json (user-level MCP config)
17
+ */
18
+ export class McpRegistrar {
19
+ private readonly settingsPath: string;
20
+ private readonly mcpJsonPath: string;
21
+ private readonly authTokenPath: string;
22
+ private registered = false;
23
+
24
+ constructor() {
25
+ const home = app.getPath('home');
26
+ this.settingsPath = path.join(home, '.claude', 'settings.json');
27
+ this.mcpJsonPath = path.join(home, '.claude', '.mcp.json');
28
+ this.authTokenPath = getAuthTokenPath();
29
+ }
30
+
31
+ /**
32
+ * Write auth token to file and register MCP server in Claude Code configs.
33
+ * Must be called after PipeServer.start().
34
+ */
35
+ register(authToken: string): void {
36
+ try {
37
+ // Write auth token to file so MCP server can read it
38
+ fs.writeFileSync(this.authTokenPath, authToken, { encoding: 'utf8', mode: 0o600 });
39
+ console.log(`[McpRegistrar] Auth token written to ${this.authTokenPath}`);
40
+
41
+ const mcpScript = this.getMcpScriptPath();
42
+ if (!mcpScript) {
43
+ console.warn('[McpRegistrar] Could not determine MCP script path — skipping registration.');
44
+ return;
45
+ }
46
+
47
+ // Use absolute node path to avoid PATH resolution issues
48
+ const mcpEntry = {
49
+ command: process.execPath,
50
+ args: [mcpScript],
51
+ };
52
+
53
+ this.registerInSettings(mcpEntry);
54
+ this.registerInMcpJson(mcpEntry);
55
+
56
+ this.registered = true;
57
+ console.log(`[McpRegistrar] Registered wmux MCP → ${mcpScript}`);
58
+ } catch (err) {
59
+ console.error('[McpRegistrar] Failed to register:', err);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Remove wmux MCP server entry and auth token file.
65
+ */
66
+ unregister(): void {
67
+ // Always clean up auth token file
68
+ try {
69
+ if (fs.existsSync(this.authTokenPath)) {
70
+ fs.unlinkSync(this.authTokenPath);
71
+ }
72
+ } catch { /* ignore */ }
73
+
74
+ if (!this.registered) return;
75
+
76
+ try {
77
+ this.unregisterFromSettings();
78
+ this.unregisterFromMcpJson();
79
+ console.log('[McpRegistrar] Unregistered wmux MCP.');
80
+ } catch (err) {
81
+ console.error('[McpRegistrar] Failed to unregister:', err);
82
+ }
83
+ }
84
+
85
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
+ private registerInSettings(mcpEntry: Record<string, any>): void {
87
+ const settings = this.readJson(this.settingsPath);
88
+ if (!settings.mcpServers) settings.mcpServers = {};
89
+ settings.mcpServers['wmux'] = mcpEntry;
90
+ this.writeJson(this.settingsPath, settings);
91
+ }
92
+
93
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
+ private registerInMcpJson(mcpEntry: Record<string, any>): void {
95
+ const mcpConfig = this.readJson(this.mcpJsonPath);
96
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
97
+ mcpConfig.mcpServers['wmux'] = mcpEntry;
98
+ this.writeJson(this.mcpJsonPath, mcpConfig);
99
+ }
100
+
101
+ private unregisterFromSettings(): void {
102
+ const settings = this.readJson(this.settingsPath);
103
+ if (settings.mcpServers?.['wmux']) {
104
+ delete settings.mcpServers['wmux'];
105
+ if (Object.keys(settings.mcpServers).length === 0) delete settings.mcpServers;
106
+ this.writeJson(this.settingsPath, settings);
107
+ }
108
+ }
109
+
110
+ private unregisterFromMcpJson(): void {
111
+ const mcpConfig = this.readJson(this.mcpJsonPath);
112
+ if (mcpConfig.mcpServers?.['wmux']) {
113
+ delete mcpConfig.mcpServers['wmux'];
114
+ if (Object.keys(mcpConfig.mcpServers).length === 0) delete mcpConfig.mcpServers;
115
+ if (Object.keys(mcpConfig).length === 0) {
116
+ try { fs.unlinkSync(this.mcpJsonPath); } catch { /* ignore */ }
117
+ } else {
118
+ this.writeJson(this.mcpJsonPath, mcpConfig);
119
+ }
120
+ }
121
+ }
122
+
123
+ private getMcpScriptPath(): string | null {
124
+ if (app.isPackaged) {
125
+ const resourcePath = path.join(process.resourcesPath, 'mcp', 'mcp', 'index.js');
126
+ if (fs.existsSync(resourcePath)) return resourcePath;
127
+ return null;
128
+ }
129
+
130
+ const appPath = app.getAppPath();
131
+ const devPath = path.join(appPath, 'dist', 'mcp', 'mcp', 'index.js');
132
+ if (fs.existsSync(devPath)) return devPath;
133
+
134
+ return null;
135
+ }
136
+
137
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
+ private readJson(filePath: string): Record<string, any> {
139
+ try {
140
+ if (fs.existsSync(filePath)) {
141
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
142
+ }
143
+ } catch { /* corrupted — start fresh */ }
144
+
145
+ const dir = path.dirname(filePath);
146
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
147
+ return {};
148
+ }
149
+
150
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
151
+ private writeJson(filePath: string, data: Record<string, any>): void {
152
+ const dir = path.dirname(filePath);
153
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
154
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
155
+ }
156
+ }