@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 +76 -0
- package/appicon.png +0 -0
- package/dist/autostart.d.ts +3 -0
- package/dist/autostart.js +234 -0
- package/dist/claude.d.ts +30 -0
- package/dist/claude.js +102 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +48 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +91 -0
- package/dist/pairing.d.ts +14 -0
- package/dist/pairing.js +137 -0
- package/dist/supabase.d.ts +2 -0
- package/dist/supabase.js +5 -0
- package/package.json +41 -0
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
|
+

|
|
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,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
|
+
}
|
package/dist/claude.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/pairing.js
ADDED
|
@@ -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
|
+
}
|
package/dist/supabase.js
ADDED
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
|
+
}
|