@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,56 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,156 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
|
-
import { promisify } from 'node:util';
|
|
3
|
-
|
|
4
|
-
const execFileAsync = promisify(execFile);
|
|
5
|
-
|
|
6
|
-
export class MetadataCollector {
|
|
7
|
-
async getGitBranch(cwd: string): Promise<string | undefined> {
|
|
8
|
-
try {
|
|
9
|
-
const { stdout } = await execFileAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
10
|
-
cwd,
|
|
11
|
-
timeout: 3000,
|
|
12
|
-
});
|
|
13
|
-
const branch = stdout.trim();
|
|
14
|
-
return branch || undefined;
|
|
15
|
-
} catch {
|
|
16
|
-
return undefined;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async getListeningPorts(pid?: number): Promise<number[]> {
|
|
21
|
-
if (pid !== undefined && !(Number.isInteger(pid) && pid > 0)) {
|
|
22
|
-
return [];
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
// Use PowerShell to get listening TCP connections
|
|
26
|
-
const script = pid
|
|
27
|
-
? `Get-NetTCPConnection -State Listen -OwningProcess ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty LocalPort`
|
|
28
|
-
: `Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -ne 0 -and $_.OwningProcess -ne 4 } | Select-Object -ExpandProperty LocalPort | Sort-Object -Unique | Select-Object -First 20`;
|
|
29
|
-
|
|
30
|
-
const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-Command', script], {
|
|
31
|
-
timeout: 5000,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const ports = stdout
|
|
35
|
-
.trim()
|
|
36
|
-
.split(/\r?\n/)
|
|
37
|
-
.map((s) => parseInt(s.trim(), 10))
|
|
38
|
-
.filter((p) => !isNaN(p) && p > 0);
|
|
39
|
-
|
|
40
|
-
return [...new Set(ports)].sort((a, b) => a - b);
|
|
41
|
-
} catch {
|
|
42
|
-
return [];
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async collect(cwd?: string): Promise<{ gitBranch?: string; cwd?: string; listeningPorts?: number[] }> {
|
|
47
|
-
const [gitBranch, listeningPorts] = await Promise.all([
|
|
48
|
-
cwd ? this.getGitBranch(cwd) : Promise.resolve(undefined),
|
|
49
|
-
this.getListeningPorts(),
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
gitBranch,
|
|
54
|
-
cwd,
|
|
55
|
-
listeningPorts: listeningPorts.length > 0 ? listeningPorts : undefined,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { Notification, BrowserWindow } from 'electron';
|
|
2
|
-
|
|
3
|
-
export class ToastManager {
|
|
4
|
-
enabled = true;
|
|
5
|
-
|
|
6
|
-
show(title: string, body: string): void {
|
|
7
|
-
if (!this.enabled) return;
|
|
8
|
-
|
|
9
|
-
// Only show toast when app is not focused
|
|
10
|
-
const focusedWindow = BrowserWindow.getFocusedWindow();
|
|
11
|
-
if (focusedWindow) return;
|
|
12
|
-
|
|
13
|
-
if (!Notification.isSupported()) return;
|
|
14
|
-
|
|
15
|
-
const notification = new Notification({
|
|
16
|
-
title,
|
|
17
|
-
body,
|
|
18
|
-
silent: false,
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
notification.on('click', () => {
|
|
22
|
-
// Bring app to front when toast is clicked
|
|
23
|
-
const win = BrowserWindow.getAllWindows()[0];
|
|
24
|
-
if (win) {
|
|
25
|
-
if (win.isMinimized()) win.restore();
|
|
26
|
-
win.focus();
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
notification.show();
|
|
31
|
-
}
|
|
32
|
-
}
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import * as net from 'net';
|
|
2
|
-
import * as crypto from 'crypto';
|
|
3
|
-
import { getPipeName } from '../../shared/constants';
|
|
4
|
-
import type { RpcRequest } from '../../shared/rpc';
|
|
5
|
-
import { RpcRouter } from './RpcRouter';
|
|
6
|
-
|
|
7
|
-
const MAX_LINE_BUFFER = 1024 * 1024; // 1 MB — prevent OOM from malicious clients
|
|
8
|
-
|
|
9
|
-
export class PipeServer {
|
|
10
|
-
private server: net.Server | null = null;
|
|
11
|
-
private readonly router: RpcRouter;
|
|
12
|
-
private readonly connectedSockets = new Set<net.Socket>();
|
|
13
|
-
private readonly authToken: string;
|
|
14
|
-
private readonly rateLimits = new Map<net.Socket, { count: number; resetAt: number }>();
|
|
15
|
-
|
|
16
|
-
constructor(router: RpcRouter) {
|
|
17
|
-
this.router = router;
|
|
18
|
-
this.authToken = crypto.randomUUID();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
getAuthToken(): string {
|
|
22
|
-
return this.authToken;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
start(): void {
|
|
26
|
-
if (this.server) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
this.server = net.createServer((socket) => {
|
|
31
|
-
this.connectedSockets.add(socket);
|
|
32
|
-
socket.on('close', () => {
|
|
33
|
-
this.connectedSockets.delete(socket);
|
|
34
|
-
this.rateLimits.delete(socket);
|
|
35
|
-
});
|
|
36
|
-
this.handleConnection(socket);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
this.server.on('error', (err: NodeJS.ErrnoException) => {
|
|
40
|
-
if (err.code === 'EADDRINUSE') {
|
|
41
|
-
console.warn('[PipeServer] EADDRINUSE — retrying in 1s...');
|
|
42
|
-
setTimeout(() => {
|
|
43
|
-
if (this.server) {
|
|
44
|
-
this.server.close();
|
|
45
|
-
this.server.listen(getPipeName());
|
|
46
|
-
}
|
|
47
|
-
}, 1000);
|
|
48
|
-
} else {
|
|
49
|
-
console.error('[PipeServer] Server error:', err);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const pipeName = getPipeName();
|
|
54
|
-
this.server.listen(pipeName, () => {
|
|
55
|
-
console.log(`[PipeServer] Listening on ${pipeName}`);
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
stop(): void {
|
|
60
|
-
if (!this.server) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Destroy all connected sockets
|
|
65
|
-
for (const socket of this.connectedSockets) {
|
|
66
|
-
socket.destroy();
|
|
67
|
-
}
|
|
68
|
-
this.connectedSockets.clear();
|
|
69
|
-
|
|
70
|
-
this.server.close((err) => {
|
|
71
|
-
if (err) {
|
|
72
|
-
console.error('[PipeServer] Error closing server:', err);
|
|
73
|
-
} else {
|
|
74
|
-
console.log('[PipeServer] Server closed.');
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
this.server = null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
private handleConnection(socket: net.Socket): void {
|
|
82
|
-
console.log('[PipeServer] Client connected.');
|
|
83
|
-
|
|
84
|
-
let buffer = '';
|
|
85
|
-
|
|
86
|
-
socket.setEncoding('utf8');
|
|
87
|
-
|
|
88
|
-
socket.on('data', (chunk: string) => {
|
|
89
|
-
buffer += chunk;
|
|
90
|
-
|
|
91
|
-
// Security: prevent OOM from clients that never send newlines
|
|
92
|
-
if (buffer.length > MAX_LINE_BUFFER) {
|
|
93
|
-
console.warn('[PipeServer] Client exceeded max buffer size — disconnecting.');
|
|
94
|
-
socket.destroy();
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const lines = buffer.split('\n');
|
|
99
|
-
// 마지막 요소는 아직 완성되지 않은 부분 — 다음 청크를 기다림
|
|
100
|
-
buffer = lines.pop() ?? '';
|
|
101
|
-
|
|
102
|
-
for (const line of lines) {
|
|
103
|
-
const trimmed = line.trim();
|
|
104
|
-
if (!trimmed) {
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
this.processLine(socket, trimmed);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
socket.on('end', () => {
|
|
112
|
-
// 연결 종료 시 남은 버퍼 처리
|
|
113
|
-
const trimmed = buffer.trim();
|
|
114
|
-
if (trimmed) {
|
|
115
|
-
this.processLine(socket, trimmed);
|
|
116
|
-
}
|
|
117
|
-
buffer = '';
|
|
118
|
-
console.log('[PipeServer] Client disconnected.');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
socket.on('error', (err) => {
|
|
122
|
-
console.error('[PipeServer] Socket error:', err);
|
|
123
|
-
socket.destroy();
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
private processLine(socket: net.Socket, line: string): void {
|
|
128
|
-
let request: RpcRequest;
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
request = JSON.parse(line) as RpcRequest;
|
|
132
|
-
} catch {
|
|
133
|
-
const errorResponse = JSON.stringify({
|
|
134
|
-
id: null,
|
|
135
|
-
ok: false,
|
|
136
|
-
error: 'Invalid JSON',
|
|
137
|
-
});
|
|
138
|
-
socket.write(errorResponse + '\n');
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Rate limiting: max 50 requests per second per socket
|
|
143
|
-
const now = Date.now();
|
|
144
|
-
let limit = this.rateLimits.get(socket);
|
|
145
|
-
if (!limit || now > limit.resetAt) {
|
|
146
|
-
limit = { count: 0, resetAt: now + 1000 };
|
|
147
|
-
this.rateLimits.set(socket, limit);
|
|
148
|
-
}
|
|
149
|
-
limit.count++;
|
|
150
|
-
if (limit.count > 50) {
|
|
151
|
-
const rateLimitResponse = JSON.stringify({
|
|
152
|
-
id: request.id,
|
|
153
|
-
ok: false,
|
|
154
|
-
error: 'rate limited',
|
|
155
|
-
});
|
|
156
|
-
socket.write(rateLimitResponse + '\n');
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Authenticate: every request must carry a valid token
|
|
161
|
-
if (request.token !== this.authToken) {
|
|
162
|
-
const unauthorizedResponse = JSON.stringify({
|
|
163
|
-
id: request.id,
|
|
164
|
-
ok: false,
|
|
165
|
-
error: 'unauthorized',
|
|
166
|
-
});
|
|
167
|
-
socket.write(unauthorizedResponse + '\n');
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
this.router
|
|
172
|
-
.dispatch(request)
|
|
173
|
-
.then((response) => {
|
|
174
|
-
if (!socket.destroyed) {
|
|
175
|
-
socket.write(JSON.stringify(response) + '\n');
|
|
176
|
-
}
|
|
177
|
-
})
|
|
178
|
-
.catch((err: unknown) => {
|
|
179
|
-
console.error('[PipeServer] Dispatch error:', err);
|
|
180
|
-
if (!socket.destroyed) {
|
|
181
|
-
const errorResponse = JSON.stringify({
|
|
182
|
-
id: request.id,
|
|
183
|
-
ok: false,
|
|
184
|
-
error: 'Internal server error',
|
|
185
|
-
});
|
|
186
|
-
socket.write(errorResponse + '\n');
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import type { RpcMethod, RpcRequest, RpcResponse } from '../../shared/rpc';
|
|
2
|
-
|
|
3
|
-
type RpcHandler = (params: Record<string, unknown>) => Promise<unknown>;
|
|
4
|
-
|
|
5
|
-
export class RpcRouter {
|
|
6
|
-
private readonly handlers = new Map<RpcMethod, RpcHandler>();
|
|
7
|
-
|
|
8
|
-
register(method: RpcMethod, handler: RpcHandler): void {
|
|
9
|
-
this.handlers.set(method, handler);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async dispatch(request: RpcRequest): Promise<RpcResponse> {
|
|
13
|
-
if (!request || typeof request.id !== 'string' || typeof request.method !== 'string') {
|
|
14
|
-
return { id: (request as RpcRequest)?.id || '', ok: false, error: 'Invalid RPC request: missing id or method' };
|
|
15
|
-
}
|
|
16
|
-
if (request.params !== undefined && (typeof request.params !== 'object' || request.params === null)) {
|
|
17
|
-
return { id: request.id, ok: false, error: 'Invalid RPC request: params must be an object' };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const handler = this.handlers.get(request.method);
|
|
21
|
-
|
|
22
|
-
if (!handler) {
|
|
23
|
-
return {
|
|
24
|
-
id: request.id,
|
|
25
|
-
ok: false,
|
|
26
|
-
error: `Unknown method: ${request.method}`,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const result = await handler(request.params);
|
|
32
|
-
return {
|
|
33
|
-
id: request.id,
|
|
34
|
-
ok: true,
|
|
35
|
-
result,
|
|
36
|
-
};
|
|
37
|
-
} catch (err) {
|
|
38
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
39
|
-
return {
|
|
40
|
-
id: request.id,
|
|
41
|
-
ok: false,
|
|
42
|
-
error: message,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|