@hmduc16031996/claude-mb-bridge 1.1.10 ā 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.
- package/dist/index.js +36 -91
- package/dist/server.d.ts +1 -0
- package/dist/server.js +62 -0
- package/dist/tunnel.d.ts +9 -0
- package/dist/tunnel.js +43 -0
- package/package.json +14 -8
- package/public/index.html +77 -0
- package/scripts/postinstall.cjs +39 -0
- package/appicon.png +0 -0
- package/dist/autostart.d.ts +0 -3
- package/dist/autostart.js +0 -234
- package/dist/claude.d.ts +0 -30
- package/dist/claude.js +0 -102
- package/dist/config.d.ts +0 -25
- package/dist/config.js +0 -48
- package/dist/pairing.d.ts +0 -14
- package/dist/pairing.js +0 -137
- package/dist/supabase.d.ts +0 -1
- package/dist/supabase.js +0 -4
package/dist/index.js
CHANGED
|
@@ -1,117 +1,62 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
const require = createRequire(import.meta.url);
|
|
7
|
-
const pty = require('node-pty');
|
|
3
|
+
import { CloudflareTunnel } from './tunnel.js';
|
|
4
|
+
import { startTerminalServer } from './server.js';
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
8
6
|
const program = new Command();
|
|
9
7
|
program
|
|
10
8
|
.name('claude-mobile-bridge')
|
|
11
|
-
.description('Bridge Claude Code CLI to mobile')
|
|
12
|
-
.version('
|
|
9
|
+
.description('Bridge Claude Code CLI to mobile via WebView')
|
|
10
|
+
.version('2.0.0')
|
|
13
11
|
.option('--token <token>', 'Pairing token from mobile app')
|
|
14
|
-
.option('--server <url>', 'Backend server URL', 'http://localhost:3000')
|
|
12
|
+
.option('--server <url>', 'Backend server URL', 'http://localhost:3000')
|
|
15
13
|
.option('--path <path>', 'Working directory', process.cwd())
|
|
14
|
+
.option('--port <port>', 'Local port for terminal server', '38473')
|
|
16
15
|
.action(async (options) => {
|
|
17
|
-
const { token, server, path } = options;
|
|
16
|
+
const { token, server, path, port } = options;
|
|
18
17
|
if (!token) {
|
|
19
18
|
console.error('Error: --token is required');
|
|
20
19
|
process.exit(1);
|
|
21
20
|
}
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
// 1. Start local terminal server
|
|
22
|
+
const terminalAuthToken = randomUUID().replace(/-/g, '');
|
|
23
|
+
const localPort = parseInt(port, 10);
|
|
24
|
+
const terminalServer = startTerminalServer(localPort, terminalAuthToken, path);
|
|
25
|
+
// 2. Start Cloudflare Tunnel
|
|
26
|
+
const tunnel = new CloudflareTunnel();
|
|
27
|
+
const tunnelResult = await tunnel.start(localPort);
|
|
28
|
+
if (!tunnelResult.url) {
|
|
29
|
+
console.error('ā Failed to start Cloudflare Tunnel.');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
// 3. Validate token and report public URL to central server
|
|
24
33
|
try {
|
|
25
34
|
const res = await fetch(`${server}/api/sessions/${token}/validate`, {
|
|
26
|
-
method: 'POST'
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: {
|
|
37
|
+
'Content-Type': 'application/json'
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
public_url: tunnelResult.url,
|
|
41
|
+
terminal_auth_token: terminalAuthToken
|
|
42
|
+
})
|
|
27
43
|
});
|
|
28
44
|
if (!res.ok) {
|
|
29
45
|
throw new Error('Invalid token or session expired');
|
|
30
46
|
}
|
|
31
|
-
|
|
47
|
+
// ONLY show "session connected" as requested
|
|
48
|
+
console.log('ā
Session connected');
|
|
32
49
|
}
|
|
33
50
|
catch (err) {
|
|
34
51
|
console.error(`ā Validation failed: ${err.message}`);
|
|
52
|
+
tunnelResult.cleanup();
|
|
35
53
|
process.exit(1);
|
|
36
54
|
}
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.
|
|
42
|
-
event: 'INSERT',
|
|
43
|
-
schema: 'public',
|
|
44
|
-
table: 'messages',
|
|
45
|
-
filter: `session_id=eq.${token}`
|
|
46
|
-
}, async (payload) => {
|
|
47
|
-
const msg = payload.new;
|
|
48
|
-
if (msg.role === 'user') {
|
|
49
|
-
console.log(`\nš© Received prompt: ${msg.content}`);
|
|
50
|
-
await handleUserPrompt(msg.content, token, path);
|
|
51
|
-
}
|
|
52
|
-
})
|
|
53
|
-
.subscribe((status) => {
|
|
54
|
-
if (status === 'SUBSCRIBED') {
|
|
55
|
-
console.log('ā
Subscribed to Realtime channel.');
|
|
56
|
-
}
|
|
55
|
+
// Handle graceful shutdown
|
|
56
|
+
process.on('SIGINT', () => {
|
|
57
|
+
tunnelResult.cleanup();
|
|
58
|
+
terminalServer.close();
|
|
59
|
+
process.exit(0);
|
|
57
60
|
});
|
|
58
|
-
// 3. Local pairing server (optional / fallback as per plan)
|
|
59
|
-
// For now, we'll stick to the token-based pairing.
|
|
60
61
|
});
|
|
61
|
-
function resolveClaudePath() {
|
|
62
|
-
try {
|
|
63
|
-
return execSync('which claude', { encoding: 'utf8' }).trim();
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return 'claude';
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
async function handleUserPrompt(content, sessionId, cwd) {
|
|
70
|
-
console.log('ā³ Executing Claude Code with PTY...');
|
|
71
|
-
const isWin = process.platform === 'win32';
|
|
72
|
-
// Resolve absolute path so posix_spawnp can find it without shell PATH lookup
|
|
73
|
-
const claudeExe = isWin ? 'cmd.exe' : resolveClaudePath();
|
|
74
|
-
const args = isWin
|
|
75
|
-
? ['/c', 'claude', '--print', content]
|
|
76
|
-
: ['--print', content];
|
|
77
|
-
const proc = pty.spawn(claudeExe, args, {
|
|
78
|
-
name: 'xterm-256color',
|
|
79
|
-
cols: 80,
|
|
80
|
-
rows: 24,
|
|
81
|
-
cwd,
|
|
82
|
-
env: process.env,
|
|
83
|
-
});
|
|
84
|
-
let output = '';
|
|
85
|
-
// Capture PTY output
|
|
86
|
-
proc.onData((data) => {
|
|
87
|
-
output += data;
|
|
88
|
-
process.stdout.write(data);
|
|
89
|
-
});
|
|
90
|
-
return new Promise((resolve) => {
|
|
91
|
-
proc.onExit(async ({ exitCode }) => {
|
|
92
|
-
console.log(`\nā
Claude Code finished with code ${exitCode}`);
|
|
93
|
-
// Clean up output from common CLI noise and ANSI codes if needed
|
|
94
|
-
// Note: We might want to keep ANSI codes for the mobile app if it supports them
|
|
95
|
-
const cleanedOutput = output
|
|
96
|
-
.replace(/Warning: no stdin data received in 3s, proceeding without it\..*?\n/g, '')
|
|
97
|
-
.replace(/.*?DeprecationWarning:.*?\n/g, '')
|
|
98
|
-
.trim();
|
|
99
|
-
// Insert assistant response back to Supabase
|
|
100
|
-
const { error } = await supabase
|
|
101
|
-
.from('messages')
|
|
102
|
-
.insert({
|
|
103
|
-
session_id: sessionId,
|
|
104
|
-
role: 'assistant',
|
|
105
|
-
content: cleanedOutput || '(No output)'
|
|
106
|
-
});
|
|
107
|
-
if (error) {
|
|
108
|
-
console.error(`ā Failed to send response: ${error.message}`);
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
console.log('š¤ Response sent to mobile.');
|
|
112
|
-
}
|
|
113
|
-
resolve();
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
62
|
program.parse();
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startTerminalServer(port: number, authToken: string, workingDir: string): import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { createServer } from 'http';
|
|
3
|
+
import { WebSocketServer } from 'ws';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { createRequire } from 'module';
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const pty = require('node-pty');
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
export function startTerminalServer(port, authToken, workingDir) {
|
|
11
|
+
const app = express();
|
|
12
|
+
const server = createServer(app);
|
|
13
|
+
const wss = new WebSocketServer({ server });
|
|
14
|
+
const publicPath = path.join(__dirname, '../public');
|
|
15
|
+
app.use(express.static(publicPath));
|
|
16
|
+
wss.on('connection', (ws, req) => {
|
|
17
|
+
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
18
|
+
const token = url.searchParams.get('token');
|
|
19
|
+
if (token !== authToken) {
|
|
20
|
+
ws.close(1008, 'Unauthorized');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const shell = process.platform === 'win32' ? 'powershell.exe' : 'bash';
|
|
24
|
+
const term = pty.spawn(shell, [], {
|
|
25
|
+
name: 'xterm-256color',
|
|
26
|
+
cols: 80,
|
|
27
|
+
rows: 24,
|
|
28
|
+
cwd: workingDir,
|
|
29
|
+
env: process.env
|
|
30
|
+
});
|
|
31
|
+
// Automatically start claude if available
|
|
32
|
+
term.write('claude\r');
|
|
33
|
+
term.onData((data) => {
|
|
34
|
+
ws.send(data);
|
|
35
|
+
});
|
|
36
|
+
ws.on('message', (msg) => {
|
|
37
|
+
const data = msg.toString();
|
|
38
|
+
try {
|
|
39
|
+
const json = JSON.parse(data);
|
|
40
|
+
if (json.type === 'resize' && json.cols && json.rows) {
|
|
41
|
+
term.resize(json.cols, json.rows);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
term.write(data);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
term.write(data);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
term.onExit(() => {
|
|
52
|
+
ws.close();
|
|
53
|
+
});
|
|
54
|
+
ws.on('close', () => {
|
|
55
|
+
term.kill();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
server.listen(port, () => {
|
|
59
|
+
// console.log(`Local terminal server running on port ${port}`);
|
|
60
|
+
});
|
|
61
|
+
return server;
|
|
62
|
+
}
|
package/dist/tunnel.d.ts
ADDED
package/dist/tunnel.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
export class CloudflareTunnel {
|
|
3
|
+
process = null;
|
|
4
|
+
async start(port) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
try {
|
|
7
|
+
const proc = spawn('cloudflared', ['tunnel', '--url', `http://localhost:${port}`], {
|
|
8
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
9
|
+
});
|
|
10
|
+
this.process = proc;
|
|
11
|
+
let output = '';
|
|
12
|
+
const timeout = setTimeout(() => {
|
|
13
|
+
resolve({ url: null, cleanup: () => this.cleanup() });
|
|
14
|
+
}, 15000);
|
|
15
|
+
proc.stderr.on('data', (data) => {
|
|
16
|
+
output += data.toString();
|
|
17
|
+
const urlMatch = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
18
|
+
if (urlMatch) {
|
|
19
|
+
clearTimeout(timeout);
|
|
20
|
+
resolve({ url: urlMatch[0], cleanup: () => this.cleanup() });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
proc.on('error', () => {
|
|
24
|
+
clearTimeout(timeout);
|
|
25
|
+
resolve({ url: null, cleanup: () => this.cleanup() });
|
|
26
|
+
});
|
|
27
|
+
proc.on('exit', () => {
|
|
28
|
+
clearTimeout(timeout);
|
|
29
|
+
this.process = null;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
resolve({ url: null, cleanup: () => this.cleanup() });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
cleanup() {
|
|
38
|
+
if (this.process) {
|
|
39
|
+
this.process.kill();
|
|
40
|
+
this.process = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hmduc16031996/claude-mb-bridge",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Bridge between Claude Code CLI and your mobile app via
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Bridge between Claude Code CLI and your mobile app via WebView",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -10,15 +10,19 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"start": "node dist/index.js",
|
|
13
|
-
"dev": "tsx src/index.ts"
|
|
13
|
+
"dev": "tsx src/index.ts",
|
|
14
|
+
"postinstall": "node scripts/postinstall.cjs"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
16
|
-
"@supabase/supabase-js": "^2.49.0",
|
|
17
17
|
"commander": "^13.1.0",
|
|
18
|
-
"
|
|
18
|
+
"express": "^4.19.0",
|
|
19
|
+
"node-pty": "^1.1.0",
|
|
20
|
+
"ws": "^8.16.0"
|
|
19
21
|
},
|
|
20
22
|
"devDependencies": {
|
|
23
|
+
"@types/express": "^4.17.21",
|
|
21
24
|
"@types/node": "^22.13.9",
|
|
25
|
+
"@types/ws": "^8.5.10",
|
|
22
26
|
"tsx": "^4.19.3",
|
|
23
27
|
"typescript": "^5.8.2"
|
|
24
28
|
},
|
|
@@ -27,15 +31,17 @@
|
|
|
27
31
|
},
|
|
28
32
|
"files": [
|
|
29
33
|
"dist",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
34
|
+
"scripts",
|
|
35
|
+
"public",
|
|
36
|
+
"README.md"
|
|
32
37
|
],
|
|
33
38
|
"keywords": [
|
|
34
39
|
"claude",
|
|
35
40
|
"mobile",
|
|
36
41
|
"bridge",
|
|
37
42
|
"cli",
|
|
38
|
-
"
|
|
43
|
+
"terminal",
|
|
44
|
+
"webview"
|
|
39
45
|
],
|
|
40
46
|
"license": "MIT"
|
|
41
47
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
6
|
+
<title>Claude Remote Terminal</title>
|
|
7
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
|
|
8
|
+
<style>
|
|
9
|
+
body, html {
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
height: 100%;
|
|
13
|
+
background: #0d1117;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
}
|
|
16
|
+
#terminal-container {
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: 100%;
|
|
19
|
+
}
|
|
20
|
+
.xterm-viewport {
|
|
21
|
+
overflow-y: auto !important;
|
|
22
|
+
}
|
|
23
|
+
</style>
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
<div id="terminal-container"></div>
|
|
27
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
28
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
29
|
+
<script>
|
|
30
|
+
const term = new Terminal({
|
|
31
|
+
cursorBlink: true,
|
|
32
|
+
theme: {
|
|
33
|
+
background: '#0d1117',
|
|
34
|
+
foreground: '#f0f6fc'
|
|
35
|
+
},
|
|
36
|
+
fontSize: 14,
|
|
37
|
+
fontFamily: 'Menlo, Monaco, "Courier New", monospace'
|
|
38
|
+
});
|
|
39
|
+
const fitAddon = new FitAddon.FitAddon();
|
|
40
|
+
term.loadAddon(fitAddon);
|
|
41
|
+
term.open(document.getElementById('terminal-container'));
|
|
42
|
+
fitAddon.fit();
|
|
43
|
+
|
|
44
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
45
|
+
const token = urlParams.get('token');
|
|
46
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
47
|
+
const ws = new WebSocket(`${protocol}//${window.location.host}?token=${token}`);
|
|
48
|
+
|
|
49
|
+
ws.onopen = () => {
|
|
50
|
+
// term.write('Connected to bridge.\r\n');
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
ws.onmessage = (event) => {
|
|
54
|
+
term.write(event.data);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
term.onData((data) => {
|
|
58
|
+
ws.send(data);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
ws.onclose = () => {
|
|
62
|
+
term.write('\r\nConnection closed.\r\n');
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
window.addEventListener('resize', () => {
|
|
66
|
+
fitAddon.fit();
|
|
67
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
68
|
+
ws.send(JSON.stringify({
|
|
69
|
+
type: 'resize',
|
|
70
|
+
cols: term.cols,
|
|
71
|
+
rows: term.rows
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
</script>
|
|
76
|
+
</body>
|
|
77
|
+
</html>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Postinstall script to fix node-pty spawn-helper permissions on macOS.
|
|
5
|
+
*
|
|
6
|
+
* node-pty's prebuilt binaries for macOS include a spawn-helper executable
|
|
7
|
+
* that sometimes loses its execute permission when installed via npm/npx.
|
|
8
|
+
* This script restores the permission.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
// Check both nested (local) and flat (npm/npx) node_modules layouts
|
|
15
|
+
const possiblePaths = [
|
|
16
|
+
path.join(__dirname, '..', 'node_modules', 'node-pty', 'prebuilds'),
|
|
17
|
+
path.join(__dirname, '..', '..', 'node-pty', 'prebuilds'),
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
for (const prebuildsPath of possiblePaths) {
|
|
21
|
+
try {
|
|
22
|
+
const entries = fs.readdirSync(prebuildsPath);
|
|
23
|
+
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (!entry.startsWith('darwin-')) continue;
|
|
26
|
+
|
|
27
|
+
const spawnHelper = path.join(prebuildsPath, entry, 'spawn-helper');
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
fs.chmodSync(spawnHelper, 0o755);
|
|
31
|
+
console.log(`ā
Fixed spawn-helper permissions: ${spawnHelper}`);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
// Ignore errors (file might not exist on some platforms)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch (e) {
|
|
37
|
+
// Directory doesn't exist, skip
|
|
38
|
+
}
|
|
39
|
+
}
|
package/appicon.png
DELETED
|
Binary file
|
package/dist/autostart.d.ts
DELETED
package/dist/autostart.js
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import { writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import { platform, homedir } from 'os';
|
|
5
|
-
const SERVICE_NAME = 'com.claude-mb-bridge';
|
|
6
|
-
const LABEL = 'Claude Mobile Bridge';
|
|
7
|
-
/**
|
|
8
|
-
* Get the path to the globally installed bridge binary
|
|
9
|
-
*/
|
|
10
|
-
function getBridgePath() {
|
|
11
|
-
try {
|
|
12
|
-
// Try to find the global install
|
|
13
|
-
const globalBin = execSync('which claude-mb-bridge', { encoding: 'utf-8' }).trim();
|
|
14
|
-
if (globalBin)
|
|
15
|
-
return globalBin;
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
// not found
|
|
19
|
-
}
|
|
20
|
-
// Fallback: use npx
|
|
21
|
-
try {
|
|
22
|
-
const npxPath = execSync('which npx', { encoding: 'utf-8' }).trim();
|
|
23
|
-
return `${npxPath} claude-mb-bridge`;
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
throw new Error('Neither claude-mb-bridge nor npx found in PATH');
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Get the full PATH from current shell (needed for LaunchAgent)
|
|
31
|
-
*/
|
|
32
|
-
function getShellPath() {
|
|
33
|
-
try {
|
|
34
|
-
return execSync('echo $PATH', { encoding: 'utf-8', shell: '/bin/zsh' }).trim();
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return '/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin';
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
// āāā macOS LaunchAgent āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
41
|
-
function getMacPlistPath() {
|
|
42
|
-
return join(homedir(), 'Library', 'LaunchAgents', `${SERVICE_NAME}.plist`);
|
|
43
|
-
}
|
|
44
|
-
function installMac(projectPath) {
|
|
45
|
-
const shellPath = getShellPath();
|
|
46
|
-
const logDir = join(homedir(), '.claude-mb-bridge');
|
|
47
|
-
try {
|
|
48
|
-
if (!existsSync(logDir)) {
|
|
49
|
-
mkdirSync(logDir, { recursive: true });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
// Directory might already exist or be created by another process
|
|
54
|
-
}
|
|
55
|
-
// Always use npx with @latest to ensure the latest version runs
|
|
56
|
-
let npxPath;
|
|
57
|
-
try {
|
|
58
|
-
npxPath = execSync('which npx', { encoding: 'utf-8', shell: '/bin/zsh' }).trim();
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
npxPath = '/usr/local/bin/npx';
|
|
62
|
-
}
|
|
63
|
-
let programArgs = ` <string>${npxPath}</string>\n <string>-y</string>\n <string>claude-mb-bridge@latest</string>`;
|
|
64
|
-
if (projectPath) {
|
|
65
|
-
programArgs += `\n <string>--path</string>\n <string>${projectPath}</string>`;
|
|
66
|
-
}
|
|
67
|
-
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
68
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
69
|
-
<plist version="1.0">
|
|
70
|
-
<dict>
|
|
71
|
-
<key>Label</key>
|
|
72
|
-
<string>${SERVICE_NAME}</string>
|
|
73
|
-
<key>ProgramArguments</key>
|
|
74
|
-
<array>
|
|
75
|
-
${programArgs}
|
|
76
|
-
</array>
|
|
77
|
-
<key>RunAtLoad</key>
|
|
78
|
-
<true/>
|
|
79
|
-
<key>KeepAlive</key>
|
|
80
|
-
<true/>
|
|
81
|
-
<key>EnvironmentVariables</key>
|
|
82
|
-
<dict>
|
|
83
|
-
<key>PATH</key>
|
|
84
|
-
<string>${shellPath}</string>
|
|
85
|
-
<key>HOME</key>
|
|
86
|
-
<string>${homedir()}</string>
|
|
87
|
-
</dict>
|
|
88
|
-
<key>StandardOutPath</key>
|
|
89
|
-
<string>${logDir}/bridge.log</string>
|
|
90
|
-
<key>StandardErrorPath</key>
|
|
91
|
-
<string>${logDir}/bridge-error.log</string>
|
|
92
|
-
<key>WorkingDirectory</key>
|
|
93
|
-
<string>${projectPath || homedir()}</string>
|
|
94
|
-
</dict>
|
|
95
|
-
</plist>`;
|
|
96
|
-
const plistPath = getMacPlistPath();
|
|
97
|
-
writeFileSync(plistPath, plist);
|
|
98
|
-
// Load the agent
|
|
99
|
-
try {
|
|
100
|
-
execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: 'ignore' });
|
|
101
|
-
}
|
|
102
|
-
catch { /* ignore */ }
|
|
103
|
-
execSync(`launchctl load "${plistPath}"`);
|
|
104
|
-
console.log('ā
Auto-launch installed (macOS LaunchAgent)');
|
|
105
|
-
console.log(` Plist: ${plistPath}`);
|
|
106
|
-
console.log(` Logs: ${logDir}/bridge.log`);
|
|
107
|
-
console.log(' Bridge will start automatically on login.');
|
|
108
|
-
}
|
|
109
|
-
function uninstallMac() {
|
|
110
|
-
const plistPath = getMacPlistPath();
|
|
111
|
-
if (!existsSync(plistPath)) {
|
|
112
|
-
console.log('ā¹ļø Auto-launch is not installed.');
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
try {
|
|
116
|
-
execSync(`launchctl unload "${plistPath}"`, { stdio: 'ignore' });
|
|
117
|
-
}
|
|
118
|
-
catch { /* ignore */ }
|
|
119
|
-
unlinkSync(plistPath);
|
|
120
|
-
console.log('ā
Auto-launch removed (macOS LaunchAgent)');
|
|
121
|
-
}
|
|
122
|
-
// āāā Linux systemd āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
123
|
-
function getLinuxServicePath() {
|
|
124
|
-
const dir = join(homedir(), '.config', 'systemd', 'user');
|
|
125
|
-
if (!existsSync(dir)) {
|
|
126
|
-
mkdirSync(dir, { recursive: true });
|
|
127
|
-
}
|
|
128
|
-
return join(dir, 'claude-mb-bridge.service');
|
|
129
|
-
}
|
|
130
|
-
function installLinux(projectPath) {
|
|
131
|
-
const bridgePath = getBridgePath();
|
|
132
|
-
const shellPath = getShellPath();
|
|
133
|
-
const logDir = join(homedir(), '.claude-mb-bridge');
|
|
134
|
-
try {
|
|
135
|
-
if (!existsSync(logDir)) {
|
|
136
|
-
mkdirSync(logDir, { recursive: true });
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
// ignore
|
|
141
|
-
}
|
|
142
|
-
let execStart = bridgePath;
|
|
143
|
-
if (projectPath) {
|
|
144
|
-
execStart += ` --path "${projectPath}"`;
|
|
145
|
-
}
|
|
146
|
-
const service = `[Unit]
|
|
147
|
-
Description=${LABEL}
|
|
148
|
-
After=network-online.target
|
|
149
|
-
Wants=network-online.target
|
|
150
|
-
|
|
151
|
-
[Service]
|
|
152
|
-
Type=simple
|
|
153
|
-
ExecStart=${execStart}
|
|
154
|
-
Restart=on-failure
|
|
155
|
-
RestartSec=10
|
|
156
|
-
Environment=PATH=${shellPath}
|
|
157
|
-
Environment=HOME=${homedir()}
|
|
158
|
-
WorkingDirectory=${projectPath || homedir()}
|
|
159
|
-
|
|
160
|
-
[Install]
|
|
161
|
-
WantedBy=default.target
|
|
162
|
-
`;
|
|
163
|
-
const servicePath = getLinuxServicePath();
|
|
164
|
-
writeFileSync(servicePath, service);
|
|
165
|
-
// Enable and start
|
|
166
|
-
try {
|
|
167
|
-
execSync('systemctl --user daemon-reload');
|
|
168
|
-
execSync('systemctl --user enable claude-mb-bridge');
|
|
169
|
-
execSync('systemctl --user start claude-mb-bridge');
|
|
170
|
-
}
|
|
171
|
-
catch (err) {
|
|
172
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
173
|
-
console.log(`ā ļø Could not start service: ${msg}`);
|
|
174
|
-
console.log(' Try manually: systemctl --user start claude-mb-bridge');
|
|
175
|
-
}
|
|
176
|
-
console.log('ā
Auto-launch installed (systemd user service)');
|
|
177
|
-
console.log(` Service: ${servicePath}`);
|
|
178
|
-
console.log(' Bridge will start automatically on login.');
|
|
179
|
-
console.log(' Status: systemctl --user status claude-mb-bridge');
|
|
180
|
-
}
|
|
181
|
-
function uninstallLinux() {
|
|
182
|
-
const servicePath = getLinuxServicePath();
|
|
183
|
-
if (!existsSync(servicePath)) {
|
|
184
|
-
console.log('ā¹ļø Auto-launch is not installed.');
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
try {
|
|
188
|
-
execSync('systemctl --user stop claude-mb-bridge', { stdio: 'ignore' });
|
|
189
|
-
execSync('systemctl --user disable claude-mb-bridge', { stdio: 'ignore' });
|
|
190
|
-
}
|
|
191
|
-
catch { /* ignore */ }
|
|
192
|
-
unlinkSync(servicePath);
|
|
193
|
-
try {
|
|
194
|
-
execSync('systemctl --user daemon-reload');
|
|
195
|
-
}
|
|
196
|
-
catch { /* ignore */ }
|
|
197
|
-
console.log('ā
Auto-launch removed (systemd user service)');
|
|
198
|
-
}
|
|
199
|
-
// āāā Public API āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
200
|
-
export function installAutostart(projectPath) {
|
|
201
|
-
const os = platform();
|
|
202
|
-
if (os === 'darwin') {
|
|
203
|
-
installMac(projectPath);
|
|
204
|
-
}
|
|
205
|
-
else if (os === 'linux') {
|
|
206
|
-
installLinux(projectPath);
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
console.log(`ā Auto-launch is not supported on ${os}`);
|
|
210
|
-
console.log(' Supported: macOS (LaunchAgent), Linux (systemd)');
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
export function uninstallAutostart() {
|
|
214
|
-
const os = platform();
|
|
215
|
-
if (os === 'darwin') {
|
|
216
|
-
uninstallMac();
|
|
217
|
-
}
|
|
218
|
-
else if (os === 'linux') {
|
|
219
|
-
uninstallLinux();
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
console.log(`ā Auto-launch is not supported on ${os}`);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
export function isAutostartInstalled() {
|
|
226
|
-
const os = platform();
|
|
227
|
-
if (os === 'darwin') {
|
|
228
|
-
return existsSync(getMacPlistPath());
|
|
229
|
-
}
|
|
230
|
-
else if (os === 'linux') {
|
|
231
|
-
return existsSync(getLinuxServicePath());
|
|
232
|
-
}
|
|
233
|
-
return false;
|
|
234
|
-
}
|
package/dist/claude.d.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { ChildProcess } from 'child_process';
|
|
2
|
-
/**
|
|
3
|
-
* Find the Claude CLI binary path
|
|
4
|
-
*/
|
|
5
|
-
export declare function findClaudeCLI(): string | null;
|
|
6
|
-
export interface ClaudeOptions {
|
|
7
|
-
model?: string;
|
|
8
|
-
cwd?: string;
|
|
9
|
-
resumeSession?: string;
|
|
10
|
-
continueSession?: boolean;
|
|
11
|
-
timeout?: number;
|
|
12
|
-
}
|
|
13
|
-
export interface ClaudeResult {
|
|
14
|
-
output: string;
|
|
15
|
-
exitCode: number;
|
|
16
|
-
killed: boolean;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Run Claude CLI with streaming output
|
|
20
|
-
*
|
|
21
|
-
* @param prompt The prompt to send
|
|
22
|
-
* @param cliPath Path to claude binary
|
|
23
|
-
* @param options Additional options
|
|
24
|
-
* @param onChunk Called with each chunk of stdout for streaming
|
|
25
|
-
* @returns Full result when completed
|
|
26
|
-
*/
|
|
27
|
-
export declare function runClaude(prompt: string, cliPath: string, options?: ClaudeOptions, onChunk?: (chunk: string, fullOutput: string) => void): {
|
|
28
|
-
process: ChildProcess;
|
|
29
|
-
result: Promise<ClaudeResult>;
|
|
30
|
-
};
|
package/dist/claude.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { spawn, execSync } from 'child_process';
|
|
2
|
-
/**
|
|
3
|
-
* Find the Claude CLI binary path
|
|
4
|
-
*/
|
|
5
|
-
export function findClaudeCLI() {
|
|
6
|
-
const commonPaths = [
|
|
7
|
-
'/usr/local/bin/claude',
|
|
8
|
-
'/opt/homebrew/bin/claude',
|
|
9
|
-
`${process.env.HOME}/.local/bin/claude`,
|
|
10
|
-
`${process.env.HOME}/.claude/bin/claude`,
|
|
11
|
-
];
|
|
12
|
-
// Try `which` first
|
|
13
|
-
try {
|
|
14
|
-
const result = execSync('which claude', { encoding: 'utf-8' }).trim();
|
|
15
|
-
if (result)
|
|
16
|
-
return result;
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
// not in PATH
|
|
20
|
-
}
|
|
21
|
-
// Check common paths
|
|
22
|
-
for (const p of commonPaths) {
|
|
23
|
-
try {
|
|
24
|
-
execSync(`test -x "${p}"`, { stdio: 'ignore' });
|
|
25
|
-
return p;
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
// not found
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Run Claude CLI with streaming output
|
|
35
|
-
*
|
|
36
|
-
* @param prompt The prompt to send
|
|
37
|
-
* @param cliPath Path to claude binary
|
|
38
|
-
* @param options Additional options
|
|
39
|
-
* @param onChunk Called with each chunk of stdout for streaming
|
|
40
|
-
* @returns Full result when completed
|
|
41
|
-
*/
|
|
42
|
-
export function runClaude(prompt, cliPath, options = {}, onChunk) {
|
|
43
|
-
const args = ['--print', '--dangerously-skip-permissions'];
|
|
44
|
-
if (options.model) {
|
|
45
|
-
args.push('--model', options.model);
|
|
46
|
-
}
|
|
47
|
-
if (options.resumeSession) {
|
|
48
|
-
args.push('--resume', options.resumeSession);
|
|
49
|
-
}
|
|
50
|
-
else if (options.continueSession) {
|
|
51
|
-
args.push('--continue');
|
|
52
|
-
}
|
|
53
|
-
// Prompt passed via stdin pipe for max CLI compatibility
|
|
54
|
-
const timeout = options.timeout || 10 * 60 * 1000; // 10 min default
|
|
55
|
-
const proc = spawn(cliPath, args, {
|
|
56
|
-
cwd: options.cwd || process.cwd(),
|
|
57
|
-
env: { ...process.env },
|
|
58
|
-
stdio: ['pipe', 'pipe', 'pipe'], // stdin is pipe ā we write prompt then close
|
|
59
|
-
});
|
|
60
|
-
// Write prompt to stdin and close
|
|
61
|
-
if (proc.stdin) {
|
|
62
|
-
proc.stdin.write(prompt);
|
|
63
|
-
proc.stdin.end();
|
|
64
|
-
}
|
|
65
|
-
const result = new Promise((resolve) => {
|
|
66
|
-
let output = '';
|
|
67
|
-
let stderr = '';
|
|
68
|
-
let killed = false;
|
|
69
|
-
const timer = setTimeout(() => {
|
|
70
|
-
killed = true;
|
|
71
|
-
proc.kill('SIGTERM');
|
|
72
|
-
}, timeout);
|
|
73
|
-
proc.stdout?.on('data', (data) => {
|
|
74
|
-
const chunk = data.toString('utf-8');
|
|
75
|
-
output += chunk;
|
|
76
|
-
onChunk?.(chunk, output);
|
|
77
|
-
});
|
|
78
|
-
proc.stderr?.on('data', (data) => {
|
|
79
|
-
stderr += data.toString('utf-8');
|
|
80
|
-
});
|
|
81
|
-
proc.on('close', (code) => {
|
|
82
|
-
clearTimeout(timer);
|
|
83
|
-
if (stderr && !output) {
|
|
84
|
-
output = `ā ļø Claude CLI Error:\n${stderr}`;
|
|
85
|
-
}
|
|
86
|
-
resolve({
|
|
87
|
-
output: output || '(empty response)',
|
|
88
|
-
exitCode: code ?? 1,
|
|
89
|
-
killed,
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
proc.on('error', (err) => {
|
|
93
|
-
clearTimeout(timer);
|
|
94
|
-
resolve({
|
|
95
|
-
output: `ā ļø Failed to start Claude CLI: ${err.message}`,
|
|
96
|
-
exitCode: 1,
|
|
97
|
-
killed: false,
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
return { process: proc, result };
|
|
102
|
-
}
|
package/dist/config.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export interface BridgeConfig {
|
|
2
|
-
supabaseUrl: string;
|
|
3
|
-
supabaseAnonKey: string;
|
|
4
|
-
pairId: string | null;
|
|
5
|
-
pairCode: string | null;
|
|
6
|
-
projectPath: string | null;
|
|
7
|
-
port: number;
|
|
8
|
-
}
|
|
9
|
-
interface SavedConfig {
|
|
10
|
-
pairId?: string;
|
|
11
|
-
pairCode?: string;
|
|
12
|
-
projectPath?: string;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Save config to ~/.claude-mobile/config.json
|
|
16
|
-
*/
|
|
17
|
-
export declare function saveConfig(updates: Partial<SavedConfig>): void;
|
|
18
|
-
/**
|
|
19
|
-
* Build the full BridgeConfig from env + saved config + CLI args
|
|
20
|
-
*/
|
|
21
|
-
export declare function getConfig(options: {
|
|
22
|
-
path?: string;
|
|
23
|
-
port?: number;
|
|
24
|
-
}): BridgeConfig;
|
|
25
|
-
export {};
|
package/dist/config.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
-
import { homedir } from 'os';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
// Config file location: ~/.claude-mobile/config.json
|
|
5
|
-
const CONFIG_DIR = join(homedir(), '.claude-mobile');
|
|
6
|
-
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
7
|
-
/**
|
|
8
|
-
* Load saved config from ~/.claude-mobile/config.json
|
|
9
|
-
*/
|
|
10
|
-
function loadSavedConfig() {
|
|
11
|
-
try {
|
|
12
|
-
if (existsSync(CONFIG_FILE)) {
|
|
13
|
-
const data = readFileSync(CONFIG_FILE, 'utf-8');
|
|
14
|
-
return JSON.parse(data);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
// ignore
|
|
19
|
-
}
|
|
20
|
-
return {};
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Save config to ~/.claude-mobile/config.json
|
|
24
|
-
*/
|
|
25
|
-
export function saveConfig(updates) {
|
|
26
|
-
const current = loadSavedConfig();
|
|
27
|
-
const merged = { ...current, ...updates };
|
|
28
|
-
if (!existsSync(CONFIG_DIR)) {
|
|
29
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
30
|
-
}
|
|
31
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), 'utf-8');
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Build the full BridgeConfig from env + saved config + CLI args
|
|
35
|
-
*/
|
|
36
|
-
export function getConfig(options) {
|
|
37
|
-
const saved = loadSavedConfig();
|
|
38
|
-
const supabaseUrl = 'https://cyhklykwiqmwpcwnfszv.supabase.co';
|
|
39
|
-
const supabaseAnonKey = 'sb_publishable_703eeIzzKe8olUyXFB84fw_0HN6NEWH';
|
|
40
|
-
return {
|
|
41
|
-
supabaseUrl,
|
|
42
|
-
supabaseAnonKey,
|
|
43
|
-
pairId: saved.pairId || null,
|
|
44
|
-
pairCode: saved.pairCode || null,
|
|
45
|
-
projectPath: options.path || process.cwd(),
|
|
46
|
-
port: options.port || 38473,
|
|
47
|
-
};
|
|
48
|
-
}
|
package/dist/pairing.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { Server } from 'http';
|
|
2
|
-
/**
|
|
3
|
-
* Start a local HTTP server to receive pairing callbacks from the web page.
|
|
4
|
-
* The `connect` edge function redirects to http://127.0.0.1:38473/callback?token=PAIR_CODE
|
|
5
|
-
*
|
|
6
|
-
* @param port Port to listen on (default 38473)
|
|
7
|
-
* @param onPairCode Called when a valid pair code is received
|
|
8
|
-
* @param persistent If true, server stays running after pairing (for re-connections)
|
|
9
|
-
* @returns Server instance and a promise that resolves when pairing succeeds
|
|
10
|
-
*/
|
|
11
|
-
export declare function startPairingServer(port: number, onPairCode: (code: string) => Promise<void>, persistent?: boolean): {
|
|
12
|
-
server: Server;
|
|
13
|
-
paired: Promise<void>;
|
|
14
|
-
};
|
package/dist/pairing.js
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { createServer } from 'http';
|
|
2
|
-
import { readFileSync } from 'fs';
|
|
3
|
-
import { join, dirname } from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import { execSync } from 'child_process';
|
|
6
|
-
/**
|
|
7
|
-
* Start a local HTTP server to receive pairing callbacks from the web page.
|
|
8
|
-
* The `connect` edge function redirects to http://127.0.0.1:38473/callback?token=PAIR_CODE
|
|
9
|
-
*
|
|
10
|
-
* @param port Port to listen on (default 38473)
|
|
11
|
-
* @param onPairCode Called when a valid pair code is received
|
|
12
|
-
* @param persistent If true, server stays running after pairing (for re-connections)
|
|
13
|
-
* @returns Server instance and a promise that resolves when pairing succeeds
|
|
14
|
-
*/
|
|
15
|
-
export function startPairingServer(port, onPairCode, persistent = false) {
|
|
16
|
-
let resolvePaired;
|
|
17
|
-
const paired = new Promise((resolve) => {
|
|
18
|
-
resolvePaired = resolve;
|
|
19
|
-
});
|
|
20
|
-
const server = createServer(async (req, res) => {
|
|
21
|
-
// CORS headers for browser requests
|
|
22
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
23
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
24
|
-
if (req.method === 'OPTIONS') {
|
|
25
|
-
res.writeHead(204);
|
|
26
|
-
res.end();
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
const url = new URL(req.url || '/', `http://localhost:${port}`);
|
|
30
|
-
if (url.pathname === '/callback') {
|
|
31
|
-
const token = url.searchParams.get('token');
|
|
32
|
-
if (!token) {
|
|
33
|
-
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
34
|
-
res.end('<h1>Missing token</h1>');
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
try {
|
|
38
|
-
await onPairCode(token);
|
|
39
|
-
// Send success response
|
|
40
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
41
|
-
res.end(`
|
|
42
|
-
<!DOCTYPE html>
|
|
43
|
-
<html>
|
|
44
|
-
<head>
|
|
45
|
-
<title>Connected!</title>
|
|
46
|
-
<meta charset="UTF-8">
|
|
47
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
48
|
-
<style>
|
|
49
|
-
body { font-family: -apple-system, system-ui, sans-serif; background: #0a0a0b; color: white; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
|
|
50
|
-
.container { text-align: center; }
|
|
51
|
-
.icon { width: 80px; height: 80px; margin-bottom: 20px; opacity: 0.85; }
|
|
52
|
-
h1 { font-size: 24px; margin-bottom: 8px; font-weight: 700; }
|
|
53
|
-
p { color: #71717a; font-size: 15px; line-height: 1.5; }
|
|
54
|
-
</style>
|
|
55
|
-
</head>
|
|
56
|
-
<body>
|
|
57
|
-
<div class="container">
|
|
58
|
-
<img class="icon" src="/icon" alt="App Icon" />
|
|
59
|
-
<h1>Connected!</h1>
|
|
60
|
-
<p>You can close this tab and return to your mobile app.</p>
|
|
61
|
-
</div>
|
|
62
|
-
</body>
|
|
63
|
-
</html>`);
|
|
64
|
-
if (!persistent) {
|
|
65
|
-
// Close server after successful pairing (one-time mode)
|
|
66
|
-
setTimeout(() => {
|
|
67
|
-
server.close();
|
|
68
|
-
resolvePaired();
|
|
69
|
-
}, 500);
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
// In persistent mode, just resolve the promise but keep server running
|
|
73
|
-
resolvePaired();
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
catch (err) {
|
|
77
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
78
|
-
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
79
|
-
res.end(`<h1>Pairing failed</h1><p>${message}</p>`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
else if (url.pathname === '/icon') {
|
|
83
|
-
// Serve the app icon
|
|
84
|
-
try {
|
|
85
|
-
const iconPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'appicon.png');
|
|
86
|
-
const icon = readFileSync(iconPath);
|
|
87
|
-
res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=3600' });
|
|
88
|
-
res.end(icon);
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
res.writeHead(404);
|
|
92
|
-
res.end();
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
else if (url.pathname === '/health') {
|
|
96
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
97
|
-
res.end(JSON.stringify({ status: 'waiting_for_pair' }));
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
res.writeHead(404);
|
|
101
|
-
res.end('Not found');
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
server.on('error', (err) => {
|
|
105
|
-
if (err.code === 'EADDRINUSE') {
|
|
106
|
-
if (persistent) {
|
|
107
|
-
// Already paired, just skip the pairing server
|
|
108
|
-
console.log(`\nā ļø Port ${port} is already in use (another bridge may be running).`);
|
|
109
|
-
console.log(' Pairing server skipped ā bridge continues working.\n');
|
|
110
|
-
resolvePaired();
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
// Need to pair! Kill the existing process on port and retry
|
|
114
|
-
console.log(`\nā ļø Port ${port} is busy. Freeing port...`);
|
|
115
|
-
try {
|
|
116
|
-
execSync(`lsof -ti:${port} | xargs kill -9 2>/dev/null`, { stdio: 'ignore' });
|
|
117
|
-
}
|
|
118
|
-
catch { /* ignore */ }
|
|
119
|
-
// Retry after a short delay
|
|
120
|
-
setTimeout(() => {
|
|
121
|
-
server.listen(port, '127.0.0.1', () => {
|
|
122
|
-
console.log(`\nš Pairing server ready at http://127.0.0.1:${port}`);
|
|
123
|
-
console.log(' š Now go to the mobile app and tap "Connect" on the setup page.\n');
|
|
124
|
-
});
|
|
125
|
-
}, 1000);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
console.error('ā Pairing server error:', err.message);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
server.listen(port, '127.0.0.1', () => {
|
|
133
|
-
console.log(`\nš Pairing server ready at http://127.0.0.1:${port}`);
|
|
134
|
-
console.log(' š Now go to the mobile app and tap "Connect" on the setup page.\n');
|
|
135
|
-
});
|
|
136
|
-
return { server, paired };
|
|
137
|
-
}
|
package/dist/supabase.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const supabase: import("@supabase/supabase-js").SupabaseClient<any, "public", "public", any, any>;
|
package/dist/supabase.js
DELETED