@hmduc16031996/claude-mb-bridge 2.4.8 → 2.4.9
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 +31 -42
- package/dist/tunnel.d.ts +1 -10
- package/dist/tunnel.js +12 -54
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,89 +2,78 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { CloudflareTunnel } from './tunnel.js';
|
|
4
4
|
import { startTerminalServer } from './server.js';
|
|
5
|
-
// ─── Built-in Cloudflare named tunnel (mobileideforusers.kanddlabs.com) ───────
|
|
6
|
-
// These are embedded at publish time. End-users don't need to supply them.
|
|
7
|
-
const BUILTIN_CF_TOKEN = 'eyJhIjoiZmM5YTQ4ZTU3NjY4NjI3MjY2OWE3YzBiMTliNTcyYzgiLCJ0IjoiMjgzM2E5NjMtNTBiYi00YThjLWJhMGMtNDI4NWE4ZTJkZmJjIiwicyI6IllqYzVaakF3T1RJdFpEWXdZeTAwWkRFNUxUZzVNR0l0WXpBeE1UazJNamc1T1dZeCJ9';
|
|
8
|
-
const BUILTIN_TUNNEL_URL = 'https://mobileideforusers.kanddlabs.com';
|
|
9
|
-
// ──────────────────────────────────────────────────────────────────────────────
|
|
10
5
|
const program = new Command();
|
|
6
|
+
const CLOUDFLARE_TUNNEL_TOKEN = 'eyJhIjoiZmM5YTQ4ZTU3NjY4NjI3MjY2OWE3YzBiMTliNTcyYzgiLCJ0IjoiMjgzM2E5NjMtNTBiYi00YThjLWJhMGMtNDI4NWE4ZTJkZmJjIiwicyI6IllqYzVaakF3T1RJdFpEWXdZeTAwWkRFNUxUZzVNR0l0WXpBeE1UazJNamc1T1dZeCJ9';
|
|
7
|
+
const CLOUDFLARE_PUBLIC_URL = 'https://mobileideforusers.kanddlabs.com';
|
|
11
8
|
program
|
|
12
9
|
.name('claude-mobile-bridge')
|
|
13
10
|
.description('Bridge Claude Code CLI to mobile via WebView')
|
|
14
|
-
.version('2.4.
|
|
11
|
+
.version('2.4.1')
|
|
15
12
|
.option('--token <token>', 'Pairing token from mobile app')
|
|
16
|
-
.option('--server <url>', 'Backend server URL', '
|
|
13
|
+
.option('--server <url>', 'Backend server URL', 'https://mobileide.kanddlabs.com')
|
|
17
14
|
.option('--path <path>', 'Working directory', process.cwd())
|
|
18
15
|
.option('--port <port>', 'Local port for terminal server', '38473')
|
|
19
|
-
.option('--ide <type>', 'IDE type: claude_code
|
|
20
|
-
|
|
21
|
-
.option('--
|
|
22
|
-
.option('--tunnel-url <url>', 'Override built-in tunnel public URL')
|
|
23
|
-
// Escape hatch: fall back to ephemeral trycloudflare
|
|
24
|
-
.option('--quick-tunnel', 'Use ephemeral trycloudflare.com tunnel instead of the named tunnel')
|
|
16
|
+
.option('--ide <type>', 'IDE type: claude_code or cursor', 'claude_code')
|
|
17
|
+
.option('--tunnel-token <token>', 'Cloudflare Tunnel token (managed tunnel)', CLOUDFLARE_TUNNEL_TOKEN)
|
|
18
|
+
.option('--public-url <url>', 'Static public URL for the tunnel', CLOUDFLARE_PUBLIC_URL)
|
|
25
19
|
.action(async (options) => {
|
|
26
|
-
const { token, server, path, port, ide,
|
|
20
|
+
const { token, server, path, port, ide, tunnelToken, publicUrl } = options;
|
|
27
21
|
if (!token) {
|
|
28
22
|
console.error('Error: --token is required');
|
|
29
23
|
process.exit(1);
|
|
30
24
|
}
|
|
31
|
-
//
|
|
32
|
-
const cfToken = options.cloudflareToken ?? BUILTIN_CF_TOKEN;
|
|
33
|
-
const tunnelUrl = options.tunnelUrl ?? BUILTIN_TUNNEL_URL;
|
|
34
|
-
const useNamedTunnel = !quickTunnel;
|
|
35
|
-
let tunnelResult = null;
|
|
36
|
-
let terminalServer = null;
|
|
37
|
-
// Cleanup
|
|
25
|
+
// Define cleanup function
|
|
38
26
|
const cleanup = async () => {
|
|
39
27
|
console.log('\n👋 Cleaning up session and shutting down...');
|
|
40
28
|
try {
|
|
29
|
+
// Optional: Notify server to end session
|
|
41
30
|
await fetch(`${server}/api/sessions/${token}/expire`, { method: 'POST' }).catch(() => { });
|
|
42
31
|
}
|
|
43
32
|
finally {
|
|
44
|
-
tunnelResult
|
|
45
|
-
terminalServer
|
|
33
|
+
tunnelResult.cleanup();
|
|
34
|
+
terminalServer.close();
|
|
46
35
|
process.exit(0);
|
|
47
36
|
}
|
|
48
37
|
};
|
|
49
38
|
// 1. Start local terminal server
|
|
50
39
|
console.log('📦 Starting terminal server...');
|
|
51
40
|
const localPort = parseInt(port, 10);
|
|
52
|
-
const { server:
|
|
53
|
-
terminalServer = ts;
|
|
41
|
+
const { server: terminalServer, actualPort } = await startTerminalServer(localPort, path, token, ide);
|
|
54
42
|
console.log(`✅ Terminal server started on port ${actualPort}`);
|
|
55
43
|
// 2. Start Cloudflare Tunnel
|
|
44
|
+
console.log('🌐 Establishing secure tunnel...');
|
|
56
45
|
const tunnel = new CloudflareTunnel();
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
console.log('🌐 Establishing ephemeral Cloudflare tunnel...');
|
|
63
|
-
tunnelResult = await tunnel.startQuick(actualPort);
|
|
64
|
-
}
|
|
65
|
-
if (!tunnelResult.url) {
|
|
66
|
-
console.error('❌ Failed to start Cloudflare Tunnel.');
|
|
46
|
+
const tunnelResult = await tunnel.start(actualPort, tunnelToken);
|
|
47
|
+
// If using a token, use the provided publicUrl or hardcoded one
|
|
48
|
+
const finalUrl = tunnelToken ? publicUrl : tunnelResult.url;
|
|
49
|
+
if (!finalUrl) {
|
|
50
|
+
console.error('❌ Failed to establish secure tunnel.');
|
|
67
51
|
process.exit(1);
|
|
68
52
|
}
|
|
69
|
-
console.log(`✅
|
|
70
|
-
// 3. Validate token
|
|
53
|
+
console.log(`✅ Secure tunnel established at ${finalUrl}`);
|
|
54
|
+
// 3. Validate token and report public URL to central server
|
|
71
55
|
console.log('🔑 Validating session token...');
|
|
72
56
|
try {
|
|
73
57
|
const res = await fetch(`${server}/api/sessions/${token}/validate`, {
|
|
74
58
|
method: 'POST',
|
|
75
|
-
headers: {
|
|
76
|
-
|
|
59
|
+
headers: {
|
|
60
|
+
'Content-Type': 'application/json'
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
public_url: finalUrl
|
|
64
|
+
})
|
|
77
65
|
});
|
|
78
|
-
if (!res.ok)
|
|
66
|
+
if (!res.ok) {
|
|
79
67
|
throw new Error('Invalid token or session expired');
|
|
68
|
+
}
|
|
80
69
|
console.log('✅ Session connected and authorized');
|
|
81
|
-
console.log(`📱 Mobile app can now connect via: ${tunnelResult.url}`);
|
|
82
70
|
}
|
|
83
71
|
catch (err) {
|
|
84
72
|
console.error(`❌ Validation failed: ${err.message}`);
|
|
85
73
|
console.error(' Ensure the central server is available');
|
|
86
|
-
|
|
74
|
+
cleanup();
|
|
87
75
|
}
|
|
76
|
+
// Handle graceful shutdown signals
|
|
88
77
|
process.on('SIGINT', cleanup);
|
|
89
78
|
process.on('SIGTERM', cleanup);
|
|
90
79
|
process.on('SIGHUP', cleanup);
|
package/dist/tunnel.d.ts
CHANGED
|
@@ -4,15 +4,6 @@ export interface TunnelResult {
|
|
|
4
4
|
}
|
|
5
5
|
export declare class CloudflareTunnel {
|
|
6
6
|
private process;
|
|
7
|
-
|
|
8
|
-
* Start a named Cloudflare tunnel using a pre-existing tunnel token.
|
|
9
|
-
* The public URL is known ahead of time (configured in Cloudflare dashboard).
|
|
10
|
-
*/
|
|
11
|
-
startNamed(tunnelToken: string, publicUrl: string): Promise<TunnelResult>;
|
|
12
|
-
/**
|
|
13
|
-
* Start an ephemeral quick tunnel (trycloudflare.com).
|
|
14
|
-
* The public URL is parsed from cloudflared stderr output.
|
|
15
|
-
*/
|
|
16
|
-
startQuick(port: number): Promise<TunnelResult>;
|
|
7
|
+
start(port: number, token?: string): Promise<TunnelResult>;
|
|
17
8
|
private cleanup;
|
|
18
9
|
}
|
package/dist/tunnel.js
CHANGED
|
@@ -1,65 +1,23 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
export class CloudflareTunnel {
|
|
3
3
|
process = null;
|
|
4
|
-
|
|
5
|
-
* Start a named Cloudflare tunnel using a pre-existing tunnel token.
|
|
6
|
-
* The public URL is known ahead of time (configured in Cloudflare dashboard).
|
|
7
|
-
*/
|
|
8
|
-
async startNamed(tunnelToken, publicUrl) {
|
|
4
|
+
async start(port, token) {
|
|
9
5
|
return new Promise((resolve) => {
|
|
10
6
|
try {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// Give cloudflared a moment to connect; fail if it exits immediately
|
|
16
|
-
const timeout = setTimeout(() => {
|
|
17
|
-
// Still resolving with the known public URL — named tunnels don't print the URL
|
|
18
|
-
resolve({ url: publicUrl, cleanup: () => this.cleanup() });
|
|
19
|
-
}, 5000);
|
|
20
|
-
proc.stdout?.on('data', (data) => {
|
|
21
|
-
console.log('[cloudflared]', data.toString().trim());
|
|
22
|
-
});
|
|
23
|
-
proc.stderr?.on('data', (data) => {
|
|
24
|
-
const line = data.toString().trim();
|
|
25
|
-
if (line)
|
|
26
|
-
console.log('[cloudflared]', line);
|
|
27
|
-
// If we see a connection established log, resolve immediately
|
|
28
|
-
if (line.includes('Connection') || line.includes('registered')) {
|
|
29
|
-
clearTimeout(timeout);
|
|
30
|
-
resolve({ url: publicUrl, cleanup: () => this.cleanup() });
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
proc.on('error', (err) => {
|
|
34
|
-
clearTimeout(timeout);
|
|
35
|
-
console.error('[cloudflared] Failed to start:', err.message);
|
|
36
|
-
resolve({ url: null, cleanup: () => this.cleanup() });
|
|
37
|
-
});
|
|
38
|
-
proc.on('exit', (code) => {
|
|
39
|
-
clearTimeout(timeout);
|
|
40
|
-
if (code !== 0) {
|
|
41
|
-
console.error(`[cloudflared] Exited with code ${code}`);
|
|
42
|
-
}
|
|
43
|
-
this.process = null;
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
catch (err) {
|
|
47
|
-
console.error('[cloudflared] Exception:', err.message);
|
|
48
|
-
resolve({ url: null, cleanup: () => this.cleanup() });
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Start an ephemeral quick tunnel (trycloudflare.com).
|
|
54
|
-
* The public URL is parsed from cloudflared stderr output.
|
|
55
|
-
*/
|
|
56
|
-
async startQuick(port) {
|
|
57
|
-
return new Promise((resolve) => {
|
|
58
|
-
try {
|
|
59
|
-
const proc = spawn('cloudflared', ['tunnel', '--url', `http://localhost:${port}`], {
|
|
7
|
+
const args = token
|
|
8
|
+
? ['tunnel', 'run', '--token', token]
|
|
9
|
+
: ['tunnel', '--url', `http://localhost:${port}`];
|
|
10
|
+
const proc = spawn('cloudflared', args, {
|
|
60
11
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
61
12
|
});
|
|
62
13
|
this.process = proc;
|
|
14
|
+
if (token) {
|
|
15
|
+
// For named tunnels, resolve immediately after a short delay
|
|
16
|
+
setTimeout(() => {
|
|
17
|
+
resolve({ url: null, cleanup: () => this.cleanup() });
|
|
18
|
+
}, 2000);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
63
21
|
let output = '';
|
|
64
22
|
const timeout = setTimeout(() => {
|
|
65
23
|
resolve({ url: null, cleanup: () => this.cleanup() });
|