@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 CHANGED
@@ -1,117 +1,62 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import { supabase } from './supabase.js';
4
- import { createRequire } from 'module';
5
- import { execSync } from 'child_process';
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('1.1.0')
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') // Default to local for dev
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
- console.log(`šŸš€ Starting bridge for token: ${token}`);
23
- // 1. Validate token and pair
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
- console.log('āœ… Session paired successfully.');
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
- // 2. Listen for messages via Supabase Realtime
38
- console.log('šŸ“” Listening for messages...');
39
- const channel = supabase.channel(`messages:${token}`);
40
- channel
41
- .on('postgres_changes', {
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();
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ export interface TunnelResult {
2
+ url: string | null;
3
+ cleanup: () => void;
4
+ }
5
+ export declare class CloudflareTunnel {
6
+ private process;
7
+ start(port: number): Promise<TunnelResult>;
8
+ private cleanup;
9
+ }
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": "1.1.10",
4
- "description": "Bridge between Claude Code CLI and your mobile app via Supabase",
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
- "node-pty": "^1.1.0"
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
- "README.md",
31
- "appicon.png"
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
- "supabase"
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
@@ -1,3 +0,0 @@
1
- export declare function installAutostart(projectPath?: string): void;
2
- export declare function uninstallAutostart(): void;
3
- export declare function isAutostartInstalled(): boolean;
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
- }
@@ -1 +0,0 @@
1
- export declare const supabase: import("@supabase/supabase-js").SupabaseClient<any, "public", "public", any, any>;
package/dist/supabase.js DELETED
@@ -1,4 +0,0 @@
1
- import { createClient } from '@supabase/supabase-js';
2
- const supabaseUrl = 'https://cyhklykwiqmwpcwnfszv.supabase.co';
3
- const supabaseAnonKey = 'sb_publishable_703eeIzzKe8olUyXFB84fw_0HN6NEWH';
4
- export const supabase = createClient(supabaseUrl, supabaseAnonKey);