@hmduc16031996/claude-mb-bridge 1.1.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/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # Claude MB Bridge šŸ¤–šŸ“±
2
+
3
+ A lightweight bridge between the **Claude Code CLI** and your **mobile app** (e.g., iOS/Android) via Supabase. It allows you to use Claude Code to interact with your codebase directly from your mobile device.
4
+
5
+ ![App Icon](appicon.png)
6
+
7
+ ## ✨ Features
8
+
9
+ - **Real-time Communication**: USes Supabase Realtime to bridge messages between your Mac and mobile device.
10
+ - **Persistent Sessions**: Resumes previous Claude conversations automatically, even after restarts.
11
+ - **Streaming Output**: (In-progress) Supports viewing Claude's output as it's generated.
12
+ - **Pairing System**: Securely pair your mobile app using a simple 6-digit code.
13
+ - **Background Service**: Easily install as a login item on macOS to keep the bridge running.
14
+ - **Model Selection**: Supports switching between Claude 3.5 Sonnet, 3 Opus, and 3.5 Haiku.
15
+ - **Command Cancellation**: Stop running Claude processes directly from your mobile app.
16
+
17
+ ## šŸ“‹ Prerequisites
18
+
19
+ 1. **Node.js**: Version 18.0.0 or higher.
20
+ 2. **Claude Code CLI**: Must be installed on your Mac.
21
+ - Install via brew: `brew install --cask claude-code`
22
+ - Or visit: [code.claude.com](https://code.claude.com)
23
+ 3. **Supabase Account**: A Supabase project with the required schema (tables: `device_pairs`, `bridge_sessions`, `messages`, `ide_presence`).
24
+
25
+ ## šŸš€ Installation
26
+
27
+ Install the package globally or run it directly using `npx`:
28
+
29
+ ```bash
30
+ # Run via npx
31
+ npx claude-mb-bridge
32
+
33
+ # Or install globally
34
+ npm install -g claude-mb-bridge
35
+ ```
36
+
37
+ ## šŸ”§ Setup & Pairing
38
+
39
+ 1. **Start the Bridge**: Run `claude-mb-bridge` in your project's root directory.
40
+ 2. **Get Pair Code**: The bridge will start a local pairing server (default port: `38473`).
41
+ 3. **Connect Mobile App**: Open your mobile app, enter the 6-digit code displayed in your terminal.
42
+ 4. **Authorized**: Once paired, the bridge will remember your device and automatically reconnect in the future.
43
+
44
+ ## šŸ’» CLI Options
45
+
46
+ | Option | Description |
47
+ | :--- | :--- |
48
+ | `-p, --path <dir>` | Project directory for Claude to work in (defaults to current directory) |
49
+ | `--port <number>` | Port for the pairing server (default: `38473`) |
50
+ | `--install` | Install as a background service (starts on login) |
51
+ | `--uninstall` | Remove the background service |
52
+ | `--version` | Show version number |
53
+
54
+ ## āš™ļø Configuration
55
+
56
+ The bridge stores its configuration in `~/.claude-mobile/config.json`.
57
+
58
+ You can override the default Supabase credentials using environment variables:
59
+
60
+ ```bash
61
+ SUPABASE_URL="your-project-url"
62
+ SUPABASE_ANON_KEY="your-anon-key"
63
+ ```
64
+
65
+ ## šŸ—ļø Architecture
66
+
67
+ The bridge acts as a middleman:
68
+ 1. **Mobile App** inserts a `user` message into the Supabase `messages` table.
69
+ 2. **Bridge** (this package) detects the new message via Realtime/Polling.
70
+ 3. **Bridge** executes the command using the local `claude-code` CLI.
71
+ 4. **Bridge** captures the output and inserts an `agent` reply back into the `messages` table.
72
+ 5. **Mobile App** displays the reply to the user.
73
+
74
+ ## šŸ“„ License
75
+
76
+ MIT Ā© [Your Name/Company]
package/appicon.png ADDED
Binary file
@@ -0,0 +1,3 @@
1
+ export declare function installAutostart(projectPath?: string): void;
2
+ export declare function uninstallAutostart(): void;
3
+ export declare function isAutostartInstalled(): boolean;
@@ -0,0 +1,234 @@
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
+ }
@@ -0,0 +1,30 @@
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 ADDED
@@ -0,0 +1,102 @@
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
+ }
@@ -0,0 +1,25 @@
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 ADDED
@@ -0,0 +1,48 @@
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 = process.env.SUPABASE_URL || 'https://dtaegtkfdwgdbyolcxht.supabase.co';
39
+ const supabaseAnonKey = process.env.SUPABASE_ANON_KEY || 'sb_publishable__giFEuM62kLTFecXfufGIw_a-Dv9y6O';
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
+ }
@@ -0,0 +1 @@
1
+ import 'dotenv/config';
package/dist/index.js ADDED
@@ -0,0 +1,91 @@
1
+ import { Command } from 'commander';
2
+ import { supabase } from './supabase.js';
3
+ import { spawn } from 'child_process';
4
+ import 'dotenv/config';
5
+ const program = new Command();
6
+ program
7
+ .name('claude-mobile-bridge')
8
+ .description('Bridge Claude Code CLI to mobile')
9
+ .version('1.1.0')
10
+ .option('--token <token>', 'Pairing token from mobile app')
11
+ .option('--server <url>', 'Backend server URL', 'http://localhost:3000') // Default to local for dev
12
+ .option('--path <path>', 'Working directory', process.cwd())
13
+ .action(async (options) => {
14
+ const { token, server, path } = options;
15
+ if (!token) {
16
+ console.error('Error: --token is required');
17
+ process.exit(1);
18
+ }
19
+ console.log(`šŸš€ Starting bridge for token: ${token}`);
20
+ // 1. Validate token and pair
21
+ try {
22
+ const res = await fetch(`${server}/api/sessions/${token}/validate`, {
23
+ method: 'POST'
24
+ });
25
+ if (!res.ok) {
26
+ throw new Error('Invalid token or session expired');
27
+ }
28
+ console.log('āœ… Session paired successfully.');
29
+ }
30
+ catch (err) {
31
+ console.error(`āŒ Validation failed: ${err.message}`);
32
+ process.exit(1);
33
+ }
34
+ // 2. Listen for messages via Supabase Realtime
35
+ console.log('šŸ“” Listening for messages...');
36
+ const channel = supabase.channel(`messages:${token}`);
37
+ channel
38
+ .on('postgres_changes', {
39
+ event: 'INSERT',
40
+ schema: 'public',
41
+ table: 'messages',
42
+ filter: `session_id=eq.${token}`
43
+ }, async (payload) => {
44
+ const msg = payload.new;
45
+ if (msg.role === 'user') {
46
+ console.log(`\nšŸ“© Received prompt: ${msg.content}`);
47
+ await handleUserPrompt(msg.content, token, path);
48
+ }
49
+ })
50
+ .subscribe((status) => {
51
+ if (status === 'SUBSCRIBED') {
52
+ console.log('āœ… Subscribed to Realtime channel.');
53
+ }
54
+ });
55
+ // 3. Local pairing server (optional / fallback as per plan)
56
+ // For now, we'll stick to the token-based pairing.
57
+ });
58
+ async function handleUserPrompt(content, sessionId, cwd) {
59
+ console.log('ā³ Executing Claude Code...');
60
+ // Using 'claude' CLI as per plan.
61
+ // We'll use '-p' to pass the prompt if supported, or pipe it.
62
+ // The plan says "runClaudeCode(payload.new.content, path)"
63
+ const child = spawn('claude', [content], { cwd, shell: true });
64
+ let output = '';
65
+ child.stdout.on('data', (data) => {
66
+ output += data.toString();
67
+ process.stdout.write(data);
68
+ });
69
+ child.stderr.on('data', (data) => {
70
+ output += data.toString();
71
+ process.stderr.write(data);
72
+ });
73
+ child.on('close', async (code) => {
74
+ console.log(`\nāœ… Claude Code finished with code ${code}`);
75
+ // Insert assistant response back to Supabase
76
+ const { error } = await supabase
77
+ .from('messages')
78
+ .insert({
79
+ session_id: sessionId,
80
+ role: 'assistant',
81
+ content: output || '(No output)'
82
+ });
83
+ if (error) {
84
+ console.error(`āŒ Failed to send response: ${error.message}`);
85
+ }
86
+ else {
87
+ console.log('šŸ“¤ Response sent to mobile.');
88
+ }
89
+ });
90
+ }
91
+ program.parse();
@@ -0,0 +1,14 @@
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
+ };
@@ -0,0 +1,137 @@
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
+ }
@@ -0,0 +1,2 @@
1
+ import 'dotenv/config';
2
+ export declare const supabase: import("@supabase/supabase-js").SupabaseClient<any, "public", "public", any, any>;
@@ -0,0 +1,5 @@
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import 'dotenv/config';
3
+ const supabaseUrl = process.env.SUPABASE_URL;
4
+ const supabaseAnonKey = process.env.SUPABASE_ANON_KEY;
5
+ export const supabase = createClient(supabaseUrl, supabaseAnonKey);
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@hmduc16031996/claude-mb-bridge",
3
+ "version": "1.1.0",
4
+ "description": "Bridge between Claude Code CLI and your mobile app via Supabase",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "claude-mb-bridge": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "dev": "tsx src/index.ts"
14
+ },
15
+ "dependencies": {
16
+ "@supabase/supabase-js": "^2.49.0",
17
+ "commander": "^13.1.0",
18
+ "dotenv": "^16.4.7"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.13.9",
22
+ "tsx": "^4.19.3",
23
+ "typescript": "^5.8.2"
24
+ },
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "appicon.png"
32
+ ],
33
+ "keywords": [
34
+ "claude",
35
+ "mobile",
36
+ "bridge",
37
+ "cli",
38
+ "supabase"
39
+ ],
40
+ "license": "MIT"
41
+ }