mobilecoder-mcp 1.0.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,110 +0,0 @@
1
- import { exec } from 'child_process';
2
- import { promisify } from 'util';
3
- import * as fs from 'fs';
4
- import * as path from 'path';
5
- import * as os from 'os';
6
-
7
- const execAsync = promisify(exec);
8
-
9
- export interface DetectedTool {
10
- id: string;
11
- name: string;
12
- version: string;
13
- path: string;
14
- isInstalled: boolean;
15
- }
16
-
17
- export class ToolDetector {
18
- async detectAll(): Promise<DetectedTool[]> {
19
- const tools = [
20
- this.checkClaude(),
21
- this.checkGemini(),
22
- this.checkQoder(),
23
- this.checkKiro(),
24
- this.checkAider(),
25
- this.checkCursor()
26
- ];
27
-
28
- const results = await Promise.all(tools);
29
- return results.filter(t => t.isInstalled);
30
- }
31
-
32
- private async checkCommand(command: string): Promise<string | null> {
33
- try {
34
- const { stdout } = await execAsync(`${command} --version`);
35
- return stdout.trim();
36
- } catch {
37
- return null;
38
- }
39
- }
40
-
41
- private async checkClaude(): Promise<DetectedTool> {
42
- const version = await this.checkCommand('claude');
43
- return {
44
- id: 'claude',
45
- name: 'Claude Code',
46
- version: version || '',
47
- path: '',
48
- isInstalled: !!version
49
- };
50
- }
51
-
52
- private async checkGemini(): Promise<DetectedTool> {
53
- const version = await this.checkCommand('gemini');
54
- return {
55
- id: 'gemini',
56
- name: 'Gemini CLI',
57
- version: version || '',
58
- path: '',
59
- isInstalled: !!version
60
- };
61
- }
62
-
63
- private async checkQoder(): Promise<DetectedTool> {
64
- const version = await this.checkCommand('qoder');
65
- return {
66
- id: 'qoder',
67
- name: 'Qoder',
68
- version: version || '',
69
- path: '',
70
- isInstalled: !!version
71
- };
72
- }
73
-
74
- private async checkKiro(): Promise<DetectedTool> {
75
- const version = await this.checkCommand('kiro');
76
- return {
77
- id: 'kiro',
78
- name: 'Kiro',
79
- version: version || '',
80
- path: '',
81
- isInstalled: !!version
82
- };
83
- }
84
-
85
- private async checkAider(): Promise<DetectedTool> {
86
- const version = await this.checkCommand('aider');
87
- return {
88
- id: 'aider',
89
- name: 'Aider',
90
- version: version || '',
91
- path: '',
92
- isInstalled: !!version
93
- };
94
- }
95
-
96
- private async checkCursor(): Promise<DetectedTool> {
97
- // Cursor is an app, not typically a CLI command for version checking in the same way
98
- // We check for config file existence as a proxy for "installed/configured"
99
- const configPath = path.join(os.homedir(), '.cursor', 'mcp.json');
100
- const isInstalled = fs.existsSync(configPath);
101
-
102
- return {
103
- id: 'mcp', // Maps to 'mcp' in ToolSelector
104
- name: 'Cursor',
105
- version: 'App',
106
- path: configPath,
107
- isInstalled
108
- };
109
- }
110
- }
package/src/webrtc.ts DELETED
@@ -1,387 +0,0 @@
1
- import SimplePeer from 'simple-peer';
2
- import { createRequire } from 'module';
3
- import { createHash } from 'crypto';
4
- const require = createRequire(import.meta.url);
5
-
6
- // wrtc modülü Node.js ortamında WebRTC desteği sağlar
7
- let wrtc: any;
8
- try {
9
- // Önce @roamhq/wrtc dene, sonra wrtc
10
- wrtc = require('@roamhq/wrtc');
11
- } catch (e) {
12
- try {
13
- wrtc = require('wrtc');
14
- } catch (e2) {
15
- console.warn('⚠️ wrtc module not available, WebRTC may not work in Node.js environment');
16
- }
17
- }
18
-
19
- // Simple HMAC implementation for Node.js
20
- function generateHMAC(data: string, secret: string): string {
21
- return createHash('sha256').update(data + secret).digest('hex');
22
- }
23
-
24
- // Validate SDP for security
25
- function validateSDP(sdp: string): boolean {
26
- if (!sdp || typeof sdp !== 'string') return false;
27
-
28
- // Check for DTLS fingerprint (critical for encryption)
29
- if (!sdp.includes('a=fingerprint:')) {
30
- console.error('❌ [MCP] Invalid SDP: missing DTLS fingerprint');
31
- return false;
32
- }
33
-
34
- // Check for DTLS setup
35
- if (!sdp.includes('a=setup:')) {
36
- console.error('❌ [MCP] Invalid SDP: missing DTLS setup');
37
- return false;
38
- }
39
-
40
- return true;
41
- }
42
-
43
- export class WebRTCConnection {
44
- private peer: SimplePeer.Instance | null = null;
45
- private code: string;
46
- private signalingUrl: string;
47
- private isConnected: boolean = false;
48
- private pollingInterval?: NodeJS.Timeout;
49
- private onMessageCallback?: (message: any) => void;
50
- private onConnectCallback?: () => void;
51
- private onDisconnectCallback?: () => void;
52
- private debug: boolean;
53
- private hmacSecret: string;
54
- private cleanupInterval?: NodeJS.Timeout;
55
- private messageNonce = new Set<string>();
56
-
57
- constructor(code: string, signalingUrl: string, debug: boolean = false) {
58
- this.code = code;
59
- this.signalingUrl = signalingUrl;
60
- this.debug = debug;
61
-
62
- // Generate HMAC secret for this session
63
- this.hmacSecret = createHash('sha256')
64
- .update(code + Date.now() + Math.random())
65
- .digest('hex');
66
-
67
- // Start cleanup interval
68
- this.startCleanup();
69
-
70
- // Validate signaling URL
71
- try {
72
- const url = new URL(signalingUrl);
73
- if (!url.protocol.startsWith('https') && url.hostname !== 'localhost' && url.hostname !== '127.0.0.1') {
74
- console.warn('⚠️ [MCP] Warning: Signaling URL should use HTTPS in production');
75
- }
76
- } catch (e) {
77
- console.error('❌ [MCP] Invalid signaling URL:', signalingUrl);
78
- }
79
- }
80
-
81
- private startCleanup(): void {
82
- this.cleanupInterval = setInterval(() => {
83
- // Clean up old nonces (prevent memory leak)
84
- if (this.messageNonce.size > 1000) {
85
- this.messageNonce.clear();
86
- }
87
- }, 60000); // Every minute
88
- }
89
-
90
-
91
- private async sendAnswerWithRetry(signal: any, attempts = 3): Promise<void> {
92
- const timestamp = new Date().toLocaleTimeString();
93
- const signalType = signal.type || (signal.candidate ? 'candidate' : 'answer');
94
-
95
- // Use trickle endpoint for ICE candidates, standard answer endpoint for SDP answer
96
- const endpoint = signal.candidate ? '/signaling/answer' : '/signaling/answer';
97
-
98
- console.log(`[${timestamp}] 📤 [Desktop] Sending ${signalType} to server...`);
99
-
100
- for (let i = 0; i < attempts; i++) {
101
- try {
102
- const response = await fetch(`${this.signalingUrl}${endpoint}`, {
103
- method: 'POST',
104
- headers: { 'Content-Type': 'application/json' },
105
- body: JSON.stringify({ code: this.code, signal })
106
- });
107
-
108
- if (!response.ok) {
109
- throw new Error(`Server returned ${response.status}`);
110
- }
111
-
112
- console.log(`[${timestamp}] ✅ [Desktop] ${signalType} sent. Waiting for P2P handshake...`);
113
- return;
114
- } catch (error) {
115
- console.error(`[${timestamp}] ❌ [Desktop] Failed to send ${signalType} (Attempt ${i + 1}/${attempts}):`, error);
116
- if (i < attempts - 1) {
117
- await new Promise(r => setTimeout(r, 1000)); // Wait 1s
118
- }
119
- }
120
- }
121
- console.error(`[${timestamp}] ❌ [Desktop] Failed to send ${signalType} after all attempts.`);
122
- }
123
-
124
- async connect(): Promise<void> {
125
- const timestamp = new Date().toLocaleTimeString();
126
- console.log(`[${timestamp}] 📡 Connecting to signaling server: ${this.signalingUrl}`);
127
- console.log(`[${timestamp}] 🔑 Using connection code: ${this.code}`);
128
-
129
- return new Promise((resolve, reject) => {
130
- try {
131
- if (!wrtc) {
132
- console.warn(`[${timestamp}] ⚠️ WebRTC initialized without Node.js native support (wrtc). This might fail.`);
133
- }
134
-
135
- // Fetch dynamic ICE servers from signaling backend
136
- fetch(`${this.signalingUrl}/signaling/ice-servers`)
137
- .then(res => res.json())
138
- .then((data: any) => {
139
- const rtcConfig = {
140
- iceServers: data.iceServers || [
141
- { urls: "stun:stun.l.google.com:19302" }
142
- ],
143
- iceCandidatePoolSize: 10,
144
- };
145
-
146
- this.peer = new SimplePeer({
147
- initiator: false,
148
- trickle: true,
149
- wrtc: wrtc,
150
- config: rtcConfig
151
- });
152
-
153
- this.setupPeerListeners(resolve, reject);
154
- })
155
- .catch(err => {
156
- console.error(`[${timestamp}] ❌ Failed to fetch ICE servers:`, err);
157
- // Fallback to basic STUN
158
- this.peer = new SimplePeer({
159
- initiator: false,
160
- trickle: true,
161
- wrtc: wrtc,
162
- config: { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] }
163
- });
164
- this.setupPeerListeners(resolve, reject);
165
- });
166
-
167
- console.log(`[${timestamp}] ⌛ Waiting for mobile device to initiate connection...`);
168
- // Start polling for offer from mobile
169
- this.startPollingForOffer();
170
- } catch (error) {
171
- console.error(`[${timestamp}] ❌ Initialization error:`, error);
172
- reject(error);
173
- }
174
- });
175
- }
176
-
177
- private setupPeerListeners(resolve: any, reject: any) {
178
- if (!this.peer) return;
179
-
180
- // Monitor ICE connection state
181
- this.peer.on('iceConnectionStateChange', () => {
182
- const state = (this.peer as any)._pc?.iceConnectionState;
183
- const timestamp = new Date().toLocaleTimeString();
184
- console.log(`[${timestamp}] 🔍 ICE Connection State: ${state}`);
185
-
186
- if (state === 'connected' || state === 'completed') {
187
- const pc = (this.peer as any)._pc;
188
- if (pc) {
189
- // Validate SDP for security
190
- const remoteDescription = pc.remoteDescription;
191
- if (remoteDescription && !validateSDP(remoteDescription.sdp)) {
192
- console.error('❌ [MCP] Remote SDP validation failed');
193
- this.disconnect();
194
- return;
195
- }
196
-
197
- pc.getStats().then((stats: any) => {
198
- let usingRelay = false;
199
- stats.forEach((report: any) => {
200
- if (report.type === 'candidate-pair' && report.selected) {
201
- const localCandidate = stats.get(report.localCandidateId);
202
- if (localCandidate && localCandidate.candidateType === 'relay') usingRelay = true;
203
- }
204
- });
205
- if (usingRelay) console.log(`[${timestamp}] 🌐 TURN relay is being used (IP leak protected)`);
206
- else console.log(`[${timestamp}] ✅ Direct P2P connection established`);
207
- });
208
- }
209
- } else if (state === 'failed' || state === 'disconnected') {
210
- console.error(`[${timestamp}] ❌ ICE connection failed: ${state}`);
211
- this.isConnected = false;
212
- }
213
- });
214
-
215
- this.peer.on('signal', async (signal: any) => {
216
- const timestamp = new Date().toLocaleTimeString();
217
- const signalType = signal.type || (signal.candidate ? 'candidate' : 'unknown');
218
- console.log(`[${timestamp}] 📡 [Desktop] Signal generated: ${signalType}`);
219
-
220
- // Validate SDP before sending
221
- if (signal.sdp && !validateSDP(signal.sdp)) {
222
- console.error('❌ [MCP] Local SDP validation failed, not sending signal');
223
- return;
224
- }
225
-
226
- // Send answer and trickle ICE candidates
227
- await this.sendAnswerWithRetry(signal);
228
- });
229
-
230
- this.peer.on('connect', () => {
231
- const timestamp = new Date().toLocaleTimeString();
232
- console.log(`\n[${timestamp}] ✨ [ONLINE] Connected to mobile device!`);
233
- this.isConnected = true;
234
- if (this.onConnectCallback) this.onConnectCallback();
235
- resolve();
236
- });
237
-
238
- this.peer.on('data', (data: any) => {
239
- try {
240
- const message = JSON.parse(data.toString());
241
- this.handleIncomingMessage(message);
242
- } catch (e) {
243
- // Silent fail for invalid JSON
244
- }
245
- });
246
-
247
- this.peer.on('error', (err: any) => {
248
- console.error('❌ WebRTC error:', err);
249
- this.isConnected = false;
250
- });
251
-
252
- this.peer.on('close', () => {
253
- console.log('❌ Connection closed');
254
- this.isConnected = false;
255
- });
256
- }
257
-
258
- private handleIncomingMessage(message: any): void {
259
- // Validate message structure
260
- if (!message || typeof message !== 'object') {
261
- console.warn('⚠️ [MCP] Invalid message structure');
262
- return;
263
- }
264
-
265
- // Check for nonce to prevent replay attacks
266
- if (message.nonce) {
267
- if (this.messageNonce.has(message.nonce)) {
268
- console.warn('⚠️ [MCP] Duplicate message detected (replay attack)');
269
- return;
270
- }
271
- this.messageNonce.add(message.nonce);
272
- }
273
-
274
- // Verify HMAC if present
275
- if (message.hmac) {
276
- const data = JSON.stringify({
277
- type: message.type,
278
- text: message.text,
279
- command: message.command,
280
- timestamp: message.timestamp,
281
- nonce: message.nonce
282
- });
283
- const expectedHMAC = generateHMAC(data, this.hmacSecret);
284
-
285
- if (message.hmac !== expectedHMAC) {
286
- console.error('❌ [MCP] HMAC verification failed');
287
- return;
288
- }
289
- }
290
-
291
- if (this.onMessageCallback) {
292
- this.onMessageCallback(message);
293
- }
294
- }
295
-
296
- private async startPollingForOffer(): Promise<void> {
297
- let pollCount = 0;
298
- const poll = async () => {
299
- try {
300
- // Stop if peer is connected or destroyed
301
- if (this.peer && (this.peer as any).connected) {
302
- if (this.pollingInterval) {
303
- clearInterval(this.pollingInterval);
304
- this.pollingInterval = undefined;
305
- }
306
- return;
307
- }
308
-
309
- pollCount++;
310
- const response = await fetch(`${this.signalingUrl}/signaling/poll?code=${this.code}`);
311
- const timestamp = new Date().toLocaleTimeString();
312
-
313
- if (response.ok) {
314
- const data = await response.json() as { signal?: any };
315
- if (data.signal && this.peer) {
316
- console.log(`[${timestamp}] 📥 Received signal from mobile device, applying...`);
317
- this.peer.signal(data.signal);
318
- // Don't clear interval, keep polling for trickle candidates
319
- } else if (this.debug && pollCount % 10 === 0) {
320
- console.log(`[${timestamp}] ⌛ Still waiting for signals (Poll #${pollCount})...`);
321
- }
322
- }
323
- } catch (error) {
324
- if (this.debug) {
325
- console.error('❌ Error during polling:', error);
326
- }
327
- }
328
- };
329
-
330
- // Poll every 1.5 seconds
331
- this.pollingInterval = setInterval(poll, 1500);
332
- poll();
333
- }
334
-
335
- send(message: any): void {
336
- if (this.peer && this.isConnected) {
337
- try {
338
- this.peer.send(JSON.stringify(message));
339
- } catch (error) {
340
- console.error('Failed to send message:', error);
341
- }
342
- } else {
343
- console.warn('Cannot send message: not connected');
344
- }
345
- }
346
-
347
- onMessage(callback: (message: any) => void): void {
348
- this.onMessageCallback = callback;
349
- }
350
-
351
- onConnect(callback: () => void): void {
352
- this.onConnectCallback = callback;
353
- }
354
-
355
- onDisconnect(callback: () => void): void {
356
- this.onDisconnectCallback = callback;
357
- }
358
-
359
- disconnect(): void {
360
- if (this.pollingInterval) {
361
- clearInterval(this.pollingInterval);
362
- }
363
- if (this.cleanupInterval) {
364
- clearInterval(this.cleanupInterval);
365
- }
366
- if (this.peer) {
367
- this.peer.removeAllListeners();
368
- this.peer.destroy();
369
- this.peer = null;
370
- }
371
- this.isConnected = false;
372
- this.messageNonce.clear();
373
- }
374
-
375
- // Cleanup method for proper resource disposal
376
- destroy(): void {
377
- this.disconnect();
378
- this.onMessageCallback = undefined;
379
- this.onConnectCallback = undefined;
380
- this.onDisconnectCallback = undefined;
381
- }
382
-
383
- getConnected(): boolean {
384
- return this.isConnected;
385
- }
386
- }
387
-