@hmduc16031996/claude-mb-bridge 2.3.6 → 2.4.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/dist/index.js CHANGED
@@ -6,7 +6,7 @@ const program = new Command();
6
6
  program
7
7
  .name('claude-mobile-bridge')
8
8
  .description('Bridge Claude Code CLI to mobile via WebView')
9
- .version('2.3.6')
9
+ .version('2.4.1')
10
10
  .option('--token <token>', 'Pairing token from mobile app')
11
11
  .option('--server <url>', 'Backend server URL', 'http://127.0.0.1:3110')
12
12
  .option('--path <path>', 'Working directory', process.cwd())
@@ -33,7 +33,7 @@ program
33
33
  // 1. Start local terminal server
34
34
  console.log('📦 Starting terminal server...');
35
35
  const localPort = parseInt(port, 10);
36
- const { server: terminalServer, actualPort } = await startTerminalServer(localPort, path, cleanup);
36
+ const { server: terminalServer, actualPort } = await startTerminalServer(localPort, path, token, cleanup);
37
37
  console.log(`✅ Terminal server started on port ${actualPort}`);
38
38
  // 2. Start Cloudflare Tunnel
39
39
  console.log('🌐 Establishing secure tunnel...');
package/dist/server.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare function startTerminalServer(port: number, workingDir: string, onDisconnect?: () => void): Promise<{
1
+ export declare function startTerminalServer(port: number, workingDir: string, terminalToken: string, onDisconnect?: () => void): Promise<{
2
2
  server: any;
3
3
  actualPort: number;
4
4
  }>;
package/dist/server.js CHANGED
@@ -1,81 +1,301 @@
1
1
  import express from 'express';
2
2
  import { createServer } from 'http';
3
- import { WebSocketServer } from 'ws';
3
+ import { WebSocketServer, WebSocket } from 'ws';
4
4
  import path from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { createRequire } from 'module';
7
+ import * as fs from 'fs';
8
+ import * as os from 'os';
9
+ import { randomUUID } from 'crypto';
7
10
  const require = createRequire(import.meta.url);
8
11
  const pty = require('node-pty');
9
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
- export function startTerminalServer(port, workingDir, onDisconnect) {
13
+ class Session {
14
+ term;
15
+ id;
16
+ cwd;
17
+ createdAt;
18
+ constructor(id, cwd, shell) {
19
+ this.id = id;
20
+ this.cwd = cwd;
21
+ this.createdAt = new Date().toISOString();
22
+ this.term = pty.spawn(shell, [], {
23
+ name: 'xterm-256color',
24
+ cols: 120,
25
+ rows: 40,
26
+ cwd: cwd,
27
+ env: {
28
+ ...process.env,
29
+ TERM: 'xterm-256color',
30
+ FORCE_COLOR: '1',
31
+ }
32
+ });
33
+ this.term.write('claude\r');
34
+ }
35
+ getInfo() {
36
+ return {
37
+ id: this.id,
38
+ cwd: this.cwd,
39
+ createdAt: this.createdAt,
40
+ status: 'running'
41
+ };
42
+ }
43
+ kill() {
44
+ try {
45
+ this.term.kill();
46
+ }
47
+ catch (e) {
48
+ console.warn(`Failed to kill pty ${this.id}:`, e);
49
+ }
50
+ }
51
+ }
52
+ class SessionManager {
53
+ sessions = new Map();
54
+ createSession(cwd) {
55
+ const id = randomUUID().slice(0, 8);
56
+ let shell = '/bin/bash';
57
+ if (process.platform === 'win32') {
58
+ shell = 'powershell.exe';
59
+ }
60
+ else if (process.platform === 'darwin') {
61
+ shell = fs.existsSync('/bin/zsh') ? '/bin/zsh' : '/bin/bash';
62
+ }
63
+ const session = new Session(id, cwd, shell);
64
+ this.sessions.set(id, session);
65
+ return session;
66
+ }
67
+ getSession(id) {
68
+ return this.sessions.get(id);
69
+ }
70
+ removeSession(id) {
71
+ const session = this.sessions.get(id);
72
+ if (session) {
73
+ session.kill();
74
+ this.sessions.delete(id);
75
+ }
76
+ }
77
+ listSessions() {
78
+ return Array.from(this.sessions.values()).map(s => s.getInfo());
79
+ }
80
+ }
81
+ export function startTerminalServer(port, workingDir, terminalToken, onDisconnect) {
11
82
  const app = express();
12
83
  const server = createServer(app);
13
84
  const wss = new WebSocketServer({ server });
85
+ const sessionManager = new SessionManager();
14
86
  const publicPath = path.join(__dirname, '../public');
15
87
  app.use(express.static(publicPath));
16
88
  app.get('/health', (req, res) => {
17
89
  res.json({ status: 'ok' });
18
90
  });
19
- wss.on('connection', (ws, req) => {
20
- console.log(`📡 New connection attempt from ${req.socket.remoteAddress}`);
21
- console.log('✅ Connection authorized');
22
- // Use absolute paths for shells to avoid posix_spawnp failure
23
- let shell = '/bin/bash';
24
- if (process.platform === 'win32') {
25
- shell = 'powershell.exe';
26
- }
27
- else if (process.platform === 'darwin') {
28
- // Prefer zsh on macOS as it's the modern default, fallback to bash
29
- shell = require('fs').existsSync('/bin/zsh') ? '/bin/zsh' : '/bin/bash';
30
- }
31
- console.log(`🚀 Spawning shell: ${shell} in ${workingDir}`);
32
- let term;
91
+ // Ports endpoint - returns empty list (no port detection in bridge mode)
92
+ app.get('/api/ports', (req, res) => {
93
+ res.json([]);
94
+ });
95
+ // Directory listing for autocomplete
96
+ app.get('/api/dirs', (req, res) => {
33
97
  try {
34
- // Ensure working directory exists
35
- if (!require('fs').existsSync(workingDir)) {
36
- console.error(`❌ Working directory does not exist: ${workingDir}`);
37
- ws.send('\r\n\x1b[31mError: Working directory does not exist\x1b[0m\r\n');
38
- ws.close();
39
- return;
98
+ let inputPath = req.query.path || '';
99
+ if (inputPath.startsWith('~/')) {
100
+ inputPath = inputPath.replace('~/', `${os.homedir()}/`);
40
101
  }
41
- term = pty.spawn(shell, [], {
42
- name: 'xterm-256color',
43
- cols: 80,
44
- rows: 24,
45
- cwd: workingDir,
46
- env: process.env
102
+ else if (inputPath === '~') {
103
+ inputPath = os.homedir();
104
+ }
105
+ let dirToRead;
106
+ let prefix;
107
+ if (inputPath.endsWith('/')) {
108
+ dirToRead = inputPath;
109
+ prefix = '';
110
+ }
111
+ else {
112
+ dirToRead = path.dirname(inputPath) || '/';
113
+ prefix = path.basename(inputPath).toLowerCase();
114
+ }
115
+ if (!path.isAbsolute(dirToRead)) {
116
+ dirToRead = path.resolve(process.cwd(), dirToRead);
117
+ }
118
+ const entries = fs.readdirSync(dirToRead, { withFileTypes: true });
119
+ const dirs = entries
120
+ .filter(e => e.isDirectory() && !e.name.startsWith('.'))
121
+ .filter(e => !prefix || e.name.toLowerCase().startsWith(prefix))
122
+ .slice(0, 20)
123
+ .map(e => {
124
+ const fullPath = path.join(dirToRead, e.name);
125
+ const displayPath = fullPath.startsWith(os.homedir())
126
+ ? fullPath.replace(os.homedir(), '~')
127
+ : fullPath;
128
+ return { name: e.name, path: displayPath + '/' };
47
129
  });
130
+ res.json(dirs);
48
131
  }
49
- catch (err) {
50
- console.error(`❌ Failed to spawn terminal: ${err.message}`);
51
- ws.send(`\r\n\x1b[31mError: Failed to spawn terminal (${err.message})\x1b[0m\r\n`);
52
- ws.close();
53
- return;
132
+ catch {
133
+ res.json([]);
54
134
  }
55
- term.write('claude\r');
56
- term.onData((data) => {
57
- ws.send(data);
58
- });
59
- ws.on('message', (msg) => {
60
- const data = msg.toString();
61
- try {
62
- const json = JSON.parse(data);
63
- if (json.type === 'resize' && json.cols && json.rows) {
64
- term.resize(json.cols, json.rows);
135
+ });
136
+ wss.on('connection', (ws, req) => {
137
+ console.log(`📡 New connection from ${req.socket.remoteAddress}`);
138
+ let authenticated = false;
139
+ let activeSessionId = null;
140
+ let outputHandler = null;
141
+ const sendControl = (message) => {
142
+ if (ws.readyState === WebSocket.OPEN) {
143
+ ws.send(Buffer.from(JSON.stringify(message)));
144
+ }
145
+ };
146
+ const attachToSession = (sessionId) => {
147
+ const session = sessionManager.getSession(sessionId);
148
+ if (!session) {
149
+ sendControl({ type: 'error', error: 'Session not found' });
150
+ return;
151
+ }
152
+ // Cleanup old handler
153
+ if (activeSessionId && outputHandler) {
154
+ const oldSession = sessionManager.getSession(activeSessionId);
155
+ if (oldSession) {
156
+ oldSession.term.removeListener('data', outputHandler);
65
157
  }
66
- else {
67
- term.write(data);
158
+ }
159
+ activeSessionId = sessionId;
160
+ outputHandler = (data) => {
161
+ if (ws.readyState === WebSocket.OPEN) {
162
+ ws.send(data); // Send as text
163
+ }
164
+ };
165
+ session.term.on('data', outputHandler);
166
+ sendControl({ type: 'session:attached', session: session.getInfo() });
167
+ };
168
+ ws.on('message', (msg, isBinary) => {
169
+ if (isBinary) {
170
+ let message;
171
+ try {
172
+ message = JSON.parse(msg.toString());
173
+ }
174
+ catch {
175
+ sendControl({ type: 'error', error: 'Invalid control message' });
176
+ return;
177
+ }
178
+ // Auth must happen first
179
+ if (message.type === 'auth') {
180
+ if (message.token === terminalToken) {
181
+ authenticated = true;
182
+ sendControl({ type: 'auth:success' });
183
+ // Auto-create initial session and attach
184
+ const initialSession = sessionManager.createSession(workingDir);
185
+ attachToSession(initialSession.id);
186
+ console.log('✅ Client authenticated');
187
+ }
188
+ else {
189
+ sendControl({ type: 'auth:failed', error: 'Invalid token' });
190
+ ws.close();
191
+ }
192
+ return;
193
+ }
194
+ if (!authenticated) {
195
+ sendControl({ type: 'auth:failed', error: 'Not authenticated' });
196
+ return;
197
+ }
198
+ switch (message.type) {
199
+ case 'session:create': {
200
+ let cwd = message.cwd || workingDir;
201
+ if (cwd.startsWith('~/')) {
202
+ cwd = cwd.replace('~/', `${os.homedir()}/`);
203
+ }
204
+ if (!fs.existsSync(cwd)) {
205
+ sendControl({ type: 'error', error: 'Directory not found' });
206
+ break;
207
+ }
208
+ const session = sessionManager.createSession(cwd);
209
+ sendControl({ type: 'session:created', session: session.getInfo() });
210
+ attachToSession(session.id);
211
+ break;
212
+ }
213
+ case 'session:attach': {
214
+ if (message.sessionId) {
215
+ attachToSession(message.sessionId);
216
+ }
217
+ break;
218
+ }
219
+ case 'session:list': {
220
+ sendControl({ type: 'session:list', sessions: sessionManager.listSessions() });
221
+ break;
222
+ }
223
+ case 'session:discover': {
224
+ // No external session discovery in bridge mode
225
+ sendControl({ type: 'session:discovered', sessions: [] });
226
+ break;
227
+ }
228
+ case 'resize': {
229
+ const session = activeSessionId ? sessionManager.getSession(activeSessionId) : null;
230
+ if (session && message.cols && message.rows) {
231
+ session.term.resize(message.cols, message.rows);
232
+ }
233
+ break;
234
+ }
235
+ case 'image:upload': {
236
+ const { data, mimeType, filename } = message;
237
+ if (!data)
238
+ break;
239
+ const ext = mimeType?.split('/')[1] || 'png';
240
+ const tempPath = path.join(os.tmpdir(), `claude-bridge-${Date.now()}-${filename || 'image'}.${ext}`);
241
+ try {
242
+ const buffer = Buffer.from(data, 'base64');
243
+ fs.writeFileSync(tempPath, buffer);
244
+ sendControl({ type: 'image:uploaded', path: tempPath });
245
+ }
246
+ catch (err) {
247
+ sendControl({ type: 'error', error: 'Failed to save image' });
248
+ }
249
+ break;
250
+ }
251
+ case 'session:destroy': {
252
+ if (message.sessionId) {
253
+ sessionManager.removeSession(message.sessionId);
254
+ sendControl({ type: 'session:destroyed', sessionId: message.sessionId });
255
+ }
256
+ break;
257
+ }
258
+ // Schedule stubs - not supported in bridge mode
259
+ case 'schedule:list': {
260
+ sendControl({ type: 'schedule:list', schedules: [] });
261
+ break;
262
+ }
263
+ case 'schedule:create': {
264
+ sendControl({ type: 'schedule:create_error', error: 'Schedules are not supported in bridge mode' });
265
+ break;
266
+ }
267
+ case 'schedule:update':
268
+ case 'schedule:delete':
269
+ case 'schedule:trigger':
270
+ case 'schedule:runs':
271
+ case 'schedule:log': {
272
+ // No-op stubs
273
+ break;
274
+ }
275
+ // Preferences - store in memory only (no persistence needed)
276
+ case 'preferences:set': {
277
+ // No-op: preferences are managed client-side via localStorage
278
+ break;
279
+ }
68
280
  }
69
281
  }
70
- catch {
71
- term.write(data);
282
+ else {
283
+ if (!authenticated)
284
+ return;
285
+ // Raw text -> forward to active pty
286
+ const session = activeSessionId ? sessionManager.getSession(activeSessionId) : null;
287
+ if (session) {
288
+ session.term.write(msg.toString());
289
+ }
72
290
  }
73
291
  });
74
- term.onExit(() => {
75
- ws.close();
76
- });
77
292
  ws.on('close', () => {
78
- term.kill();
293
+ if (activeSessionId && outputHandler) {
294
+ const session = sessionManager.getSession(activeSessionId);
295
+ if (session) {
296
+ session.term.removeListener('data', outputHandler);
297
+ }
298
+ }
79
299
  if (onDisconnect)
80
300
  onDisconnect();
81
301
  });
@@ -84,7 +304,7 @@ export function startTerminalServer(port, workingDir, onDisconnect) {
84
304
  server.on('error', (err) => {
85
305
  if (err.code === 'EADDRINUSE' && port !== 0) {
86
306
  console.warn(`⚠️ Port ${port} is busy, trying a random port...`);
87
- resolve(startTerminalServer(0, workingDir, onDisconnect));
307
+ resolve(startTerminalServer(0, workingDir, terminalToken, onDisconnect));
88
308
  }
89
309
  else {
90
310
  reject(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmduc16031996/claude-mb-bridge",
3
- "version": "2.3.6",
3
+ "version": "2.4.1",
4
4
  "description": "Bridge between Claude Code CLI and your mobile app via WebView",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",