mobilcoder-mcp 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.
@@ -0,0 +1,294 @@
1
+ import * as crypto from 'crypto';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ // Security configuration
6
+ export const SECURITY_CONFIG = {
7
+ maxFileSize: 10 * 1024 * 1024, // 10MB
8
+ maxRequestsPerMinute: 60,
9
+ maxRequestsPerHour: 1000,
10
+ allowedFileExtensions: ['.ts', '.js', '.jsx', '.tsx', '.json', '.md', '.txt', '.yml', '.yaml', '.env.example'],
11
+ blockedPaths: [
12
+ '.git',
13
+ 'node_modules',
14
+ '.env',
15
+ '.env.local',
16
+ '.env.development',
17
+ '.env.production',
18
+ 'dist',
19
+ 'build',
20
+ '.next',
21
+ '.nuxt',
22
+ '.cache',
23
+ 'tmp',
24
+ 'temp'
25
+ ],
26
+ blockedFilePatterns: [
27
+ /\.key$/,
28
+ /\.pem$/,
29
+ /\.crt$/,
30
+ /\.p12$/,
31
+ /private/i,
32
+ /secret/i,
33
+ /password/i,
34
+ /token/i,
35
+ /\.log$/,
36
+ /\.pid$/,
37
+ /\.lock$/,
38
+ ]
39
+ };
40
+
41
+ // Rate limiting
42
+ class RateLimiter {
43
+ private requests = new Map<string, { count: number; resetTime: number; lastReset: number }>();
44
+
45
+ constructor(private maxRequests: number, private windowMs: number) {}
46
+
47
+ isAllowed(identifier: string): boolean {
48
+ const now = Date.now();
49
+ const entry = this.requests.get(identifier);
50
+
51
+ if (!entry) {
52
+ this.requests.set(identifier, {
53
+ count: 1,
54
+ resetTime: now + this.windowMs,
55
+ lastReset: now
56
+ });
57
+ return true;
58
+ }
59
+
60
+ // Reset if window expired
61
+ if (now > entry.resetTime) {
62
+ entry.count = 1;
63
+ entry.resetTime = now + this.windowMs;
64
+ entry.lastReset = now;
65
+ return true;
66
+ }
67
+
68
+ if (entry.count >= this.maxRequests) {
69
+ return false;
70
+ }
71
+
72
+ entry.count++;
73
+ return true;
74
+ }
75
+
76
+ cleanup(): void {
77
+ const now = Date.now();
78
+ for (const [key, entry] of this.requests.entries()) {
79
+ if (now > entry.resetTime) {
80
+ this.requests.delete(key);
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ export const rateLimiters = {
87
+ perMinute: new RateLimiter(SECURITY_CONFIG.maxRequestsPerMinute, 60 * 1000),
88
+ perHour: new RateLimiter(SECURITY_CONFIG.maxRequestsPerHour, 60 * 60 * 1000),
89
+ fileOperations: new RateLimiter(30, 60 * 1000),
90
+ commands: new RateLimiter(10, 60 * 1000)
91
+ };
92
+
93
+ // Input validation
94
+ export function validatePath(filePath: string, cwd: string): { valid: boolean; error?: string } {
95
+ // Normalize path
96
+ const normalizedPath = path.normalize(filePath);
97
+ const fullPath = path.resolve(cwd, normalizedPath);
98
+
99
+ // Check for path traversal
100
+ if (!fullPath.startsWith(path.resolve(cwd))) {
101
+ return { valid: false, error: 'Path traversal detected' };
102
+ }
103
+
104
+ // Check for blocked paths
105
+ const pathParts = normalizedPath.split(path.sep);
106
+ for (const part of pathParts) {
107
+ if (SECURITY_CONFIG.blockedPaths.includes(part)) {
108
+ return { valid: false, error: `Access to ${part} is not allowed` };
109
+ }
110
+ }
111
+
112
+ // Check for blocked file patterns
113
+ for (const pattern of SECURITY_CONFIG.blockedFilePatterns) {
114
+ if (pattern.test(normalizedPath)) {
115
+ return { valid: false, error: 'Access to sensitive files is not allowed' };
116
+ }
117
+ }
118
+
119
+ return { valid: true };
120
+ }
121
+
122
+ export function validateFile(filePath: string, cwd: string): { valid: boolean; error?: string } {
123
+ const pathValidation = validatePath(filePath, cwd);
124
+ if (!pathValidation.valid) {
125
+ return pathValidation;
126
+ }
127
+
128
+ // Check file extension
129
+ const ext = path.extname(filePath).toLowerCase();
130
+ if (!SECURITY_CONFIG.allowedFileExtensions.includes(ext)) {
131
+ return { valid: false, error: `File type ${ext} is not allowed` };
132
+ }
133
+
134
+ // Check file size if exists
135
+ const fullPath = path.resolve(cwd, filePath);
136
+ try {
137
+ const stats = fs.statSync(fullPath);
138
+ if (stats.size > SECURITY_CONFIG.maxFileSize) {
139
+ return { valid: false, error: 'File too large' };
140
+ }
141
+ } catch {
142
+ // File doesn't exist, that's ok
143
+ }
144
+
145
+ return { valid: true };
146
+ }
147
+
148
+ export function validateCommand(command: string): { valid: boolean; error?: string } {
149
+ // Check for dangerous commands
150
+ const dangerousPatterns = [
151
+ /\brm\s+-rf\b/i,
152
+ /\bsudo\b/i,
153
+ /\bsu\s/i,
154
+ /\bchmod\s+777\b/i,
155
+ /\bwget\b|\bcurl\b/i,
156
+ /\bnc\s|\bnetcat\b/i,
157
+ /\bssh\b/i,
158
+ /\bscp\b/i,
159
+ /\brsync\b/i,
160
+ /\bdd\s+if=/i,
161
+ /\bmkfs\b/i,
162
+ /\bfdisk\b/i,
163
+ /\bmount\b/i,
164
+ /\bumount\b/i,
165
+ /\bpasswd\b/i,
166
+ /\bshadow\b/i,
167
+ /\bcrontab\b/i,
168
+ /\bsystemctl\b/i,
169
+ /\bservice\s/i,
170
+ /\bkill\s+-9\b/i,
171
+ /\bkillall\b/i,
172
+ />\s*\/dev\/null/,
173
+ />\s*\/dev\/(zero|random|urandom)/,
174
+ ];
175
+
176
+ for (const pattern of dangerousPatterns) {
177
+ if (pattern.test(command)) {
178
+ return { valid: false, error: 'Dangerous command detected' };
179
+ }
180
+ }
181
+
182
+ return { valid: true };
183
+ }
184
+
185
+ // Sanitization
186
+ export function sanitizeInput(input: string): string {
187
+ return input
188
+ .replace(/[<>]/g, '') // Remove HTML tags
189
+ .replace(/[\x00-\x1f\x7f]/g, '') // Remove control characters
190
+ .replace(/[\r\n\t]/g, ' ') // Replace newlines and tabs
191
+ .trim()
192
+ .substring(0, 1000); // Limit length
193
+ }
194
+
195
+ export function sanitizePath(input: string): string {
196
+ return input
197
+ .replace(/\.\./g, '') // Remove parent directory references
198
+ .replace(/^\//, '') // Remove leading slashes
199
+ .replace(/\/$/, '') // Remove trailing slashes
200
+ .replace(/[<>:"|?*]/g, '') // Remove invalid characters
201
+ .trim();
202
+ }
203
+
204
+ // Security logging
205
+ export class SecurityLogger {
206
+ private static instance: SecurityLogger;
207
+ private logFile: string;
208
+
209
+ private constructor() {
210
+ this.logFile = path.join(process.cwd(), '.security.log');
211
+ }
212
+
213
+ static getInstance(): SecurityLogger {
214
+ if (!SecurityLogger.instance) {
215
+ SecurityLogger.instance = new SecurityLogger();
216
+ }
217
+ return SecurityLogger.instance;
218
+ }
219
+
220
+ log(event: string, details: any, severity: 'low' | 'medium' | 'high' = 'medium'): void {
221
+ const logEntry = {
222
+ timestamp: new Date().toISOString(),
223
+ event,
224
+ details,
225
+ severity,
226
+ pid: process.pid,
227
+ user: process.env.USER || 'unknown'
228
+ };
229
+
230
+ const logLine = JSON.stringify(logEntry) + '\n';
231
+
232
+ try {
233
+ fs.appendFileSync(this.logFile, logLine);
234
+ } catch (error) {
235
+ console.error('Failed to write security log:', error);
236
+ }
237
+
238
+ // Also log to console for immediate visibility
239
+ if (severity === 'high') {
240
+ console.error('🚨 SECURITY ALERT:', logEntry);
241
+ } else if (severity === 'medium') {
242
+ console.warn('⚠️ SECURITY WARNING:', logEntry);
243
+ } else {
244
+ console.log('ℹ️ SECURITY INFO:', logEntry);
245
+ }
246
+ }
247
+
248
+ logBlockedCommand(command: string, reason: string): void {
249
+ this.log('blocked_command', { command, reason }, 'high');
250
+ }
251
+
252
+ logPathTraversal(attemptedPath: string, resolvedPath: string): void {
253
+ this.log('path_traversal', { attemptedPath, resolvedPath }, 'high');
254
+ }
255
+
256
+ logRateLimitExceeded(identifier: string, operation: string): void {
257
+ this.log('rate_limit_exceeded', { identifier, operation }, 'medium');
258
+ }
259
+
260
+ logSuspiciousActivity(activity: string, details: any): void {
261
+ this.log('suspicious_activity', { activity, details }, 'medium');
262
+ }
263
+ }
264
+
265
+ export const securityLogger = SecurityLogger.getInstance();
266
+
267
+ // Cleanup old rate limit entries periodically
268
+ setInterval(() => {
269
+ rateLimiters.perMinute.cleanup();
270
+ rateLimiters.perHour.cleanup();
271
+ rateLimiters.fileOperations.cleanup();
272
+ rateLimiters.commands.cleanup();
273
+ }, 5 * 60 * 1000); // Every 5 minutes
274
+
275
+ // Generate secure tokens
276
+ export function generateSecureToken(length: number = 32): string {
277
+ return crypto.randomBytes(length).toString('hex');
278
+ }
279
+
280
+ // Validate session tokens
281
+ export function validateSessionToken(token: string): boolean {
282
+ // Basic token validation
283
+ if (!token || typeof token !== 'string') {
284
+ return false;
285
+ }
286
+
287
+ // Check length
288
+ if (token.length < 16 || token.length > 128) {
289
+ return false;
290
+ }
291
+
292
+ // Check format (hex)
293
+ return /^[a-f0-9]+$/.test(token);
294
+ }
@@ -0,0 +1,110 @@
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 ADDED
@@ -0,0 +1,156 @@
1
+ import SimplePeer from 'simple-peer';
2
+
3
+ export class WebRTCConnection {
4
+ private peer: SimplePeer.Instance | null = null;
5
+ private code: string;
6
+ private signalingUrl: string;
7
+ private onMessageCallback?: (message: any) => void;
8
+ private onConnectCallback?: () => void;
9
+ private onDisconnectCallback?: () => void;
10
+ private isConnected: boolean = false;
11
+ private pollingInterval?: NodeJS.Timeout;
12
+
13
+ constructor(code: string, signalingUrl: string) {
14
+ this.code = code;
15
+ this.signalingUrl = signalingUrl;
16
+ }
17
+
18
+ async connect(): Promise<void> {
19
+ return new Promise((resolve, reject) => {
20
+ try {
21
+ // Desktop acts as answerer (not initiator)
22
+ this.peer = new SimplePeer({
23
+ initiator: false,
24
+ trickle: false,
25
+ config: {
26
+ iceServers: [
27
+ { urls: 'stun:stun.l.google.com:19302' },
28
+ { urls: 'stun:stun1.l.google.com:19302' }
29
+ ]
30
+ }
31
+ });
32
+
33
+ this.peer.on('signal', async (signal: any) => {
34
+ // Send answer to signaling server
35
+ try {
36
+ await fetch(`${this.signalingUrl}/answer`, {
37
+ method: 'POST',
38
+ headers: { 'Content-Type': 'application/json' },
39
+ body: JSON.stringify({ code: this.code, signal })
40
+ });
41
+ } catch (error) {
42
+ console.error('Failed to send answer:', error);
43
+ }
44
+ });
45
+
46
+ this.peer.on('connect', () => {
47
+ console.log('✅ Connected to mobile device!');
48
+ this.isConnected = true;
49
+ if (this.onConnectCallback) {
50
+ this.onConnectCallback();
51
+ }
52
+ resolve();
53
+ });
54
+
55
+ this.peer.on('data', (data: any) => {
56
+ try {
57
+ const message = JSON.parse(data.toString());
58
+ if (this.onMessageCallback) {
59
+ this.onMessageCallback(message);
60
+ }
61
+ } catch (error) {
62
+ console.error('Failed to parse message:', error);
63
+ }
64
+ });
65
+
66
+ this.peer.on('error', (error: any) => {
67
+ console.error('WebRTC error:', error);
68
+ this.isConnected = false;
69
+ if (this.onDisconnectCallback) {
70
+ this.onDisconnectCallback();
71
+ }
72
+ reject(error);
73
+ });
74
+
75
+ this.peer.on('close', () => {
76
+ console.log('❌ Connection closed');
77
+ this.isConnected = false;
78
+ if (this.onDisconnectCallback) {
79
+ this.onDisconnectCallback();
80
+ }
81
+ });
82
+
83
+ // Start polling for offer from mobile
84
+ this.startPollingForOffer();
85
+ } catch (error) {
86
+ reject(error);
87
+ }
88
+ });
89
+ }
90
+
91
+ private async startPollingForOffer(): Promise<void> {
92
+ const poll = async () => {
93
+ try {
94
+ const response = await fetch(`${this.signalingUrl}/poll?code=${this.code}`);
95
+ if (response.ok) {
96
+ const data = await response.json() as { signal?: any };
97
+ if (data.signal && this.peer) {
98
+ // Received offer from mobile, signal the peer
99
+ this.peer.signal(data.signal);
100
+ // Stop polling once we got the offer
101
+ if (this.pollingInterval) {
102
+ clearInterval(this.pollingInterval);
103
+ }
104
+ }
105
+ }
106
+ } catch (error) {
107
+ // Silently handle polling errors
108
+ }
109
+ };
110
+
111
+ // Poll every 2 seconds
112
+ this.pollingInterval = setInterval(poll, 2000);
113
+ // Initial poll
114
+ await poll();
115
+ }
116
+
117
+ send(message: any): void {
118
+ if (this.peer && this.isConnected) {
119
+ try {
120
+ this.peer.send(JSON.stringify(message));
121
+ } catch (error) {
122
+ console.error('Failed to send message:', error);
123
+ }
124
+ } else {
125
+ console.warn('Cannot send message: not connected');
126
+ }
127
+ }
128
+
129
+ onMessage(callback: (message: any) => void): void {
130
+ this.onMessageCallback = callback;
131
+ }
132
+
133
+ onConnect(callback: () => void): void {
134
+ this.onConnectCallback = callback;
135
+ }
136
+
137
+ onDisconnect(callback: () => void): void {
138
+ this.onDisconnectCallback = callback;
139
+ }
140
+
141
+ disconnect(): void {
142
+ if (this.pollingInterval) {
143
+ clearInterval(this.pollingInterval);
144
+ }
145
+ if (this.peer) {
146
+ this.peer.destroy();
147
+ this.peer = null;
148
+ }
149
+ this.isConnected = false;
150
+ }
151
+
152
+ getConnected(): boolean {
153
+ return this.isConnected;
154
+ }
155
+ }
156
+
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "lib": ["ES2022"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "moduleResolution": "node",
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }
21
+