diff --git a/SKILL.md b/SKILL.md
index 1896515..2fd70d3 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -12,7 +12,7 @@
node
index.js
- Registers a user-level systemd service (clawbridge.service) that auto-starts on login and restarts on failure.
+ Optional. Run setup.sh --enable-service to register a user-level systemd service for auto-start. Not enabled by default.
@@ -49,13 +49,13 @@
-
+
- curl -sL https://raw.githubusercontent.com/dreamwing/clawbridge/master/install.sh | bash
+ git clone https://github.com/dreamwing/clawbridge.git skills/clawbridge && cd skills/clawbridge && npm install --production && node index.js
- ClawBridge installs itself as a persistent background service.
+ ClawBridge runs as a foreground Node.js process by default.
After installation, the dashboard is accessible at the local IP shown in the terminal output.
An ACCESS_KEY is generated and displayed â keep it safe, it is required to log in.
@@ -63,10 +63,16 @@
To enable remote access (optional), supply a Cloudflare Tunnel token when prompted,
or leave it blank to use a temporary Quick Tunnel URL.
- To update to the latest version:
+ To enable auto-start as a background service (optional):
+ ./setup.sh --enable-service
+
+ To install with one command (includes service registration):
curl -sL https://raw.githubusercontent.com/dreamwing/clawbridge/master/install.sh | bash
- To stop the service:
+ To update to the latest version via git:
+ git pull && npm install
+
+ To stop the service (if enabled):
systemctl --user stop clawbridge
Full documentation: https://github.com/dreamwing/clawbridge/blob/master/README.md
diff --git a/install.sh b/install.sh
index e1bc593..b94ab29 100644
--- a/install.sh
+++ b/install.sh
@@ -216,10 +216,10 @@ if [ "$HAS_TOKEN" = true ]; then
# Let's modify setup.sh to support --update mode or similar.
# OR: Extract token and pass it back?
TOKEN=$(grep "TUNNEL_TOKEN=" "$TARGET_DIR/.env" | cut -d'=' -f2)
- ./setup.sh --token="$TOKEN"
+ ./setup.sh --token="$TOKEN" --enable-service
else
# Force quick mode for zero-friction
- ./setup.sh --quick
+ ./setup.sh --quick --enable-service
fi
# Final Notification
diff --git a/setup.sh b/setup.sh
index 2d7e190..66e0d9c 100755
--- a/setup.sh
+++ b/setup.sh
@@ -23,6 +23,8 @@ NO_TUNNEL=false
QUICK_TUNNEL=false
FORCE_CF=false
+ENABLE_SERVICE=false
+
for arg in "$@"
do
case $arg in
@@ -42,6 +44,10 @@ do
FORCE_CF=true
shift
;;
+ --enable-service)
+ ENABLE_SERVICE=true
+ shift
+ ;;
esac
done
@@ -106,6 +112,7 @@ if [ ! -f "$ENV_FILE" ]; then
RAND_KEY=$(openssl rand -hex 16)
echo "ACCESS_KEY=$RAND_KEY" > "$ENV_FILE"
echo "PORT=$PORT" >> "$ENV_FILE"
+ chmod 600 "$ENV_FILE"
echo -e "${YELLOW}đ Generated Access Key: $RAND_KEY${NC}"
else
echo "â
Updating .env configuration..."
@@ -123,6 +130,7 @@ else
else
RAND_KEY=$ACCESS_KEY
fi
+ chmod 600 "$ENV_FILE"
fi
# 3b. Auto-detect OPENCLAW_PATH
@@ -147,13 +155,14 @@ fi
# 4. Setup Service
NODE_PATH=$(which node)
-if [ "$OS_TYPE" = "Darwin" ]; then
- # macOS launchd setup
- SERVICE_DIR="$HOME/Library/LaunchAgents"
- mkdir -p "$SERVICE_DIR"
- SERVICE_FILE="$SERVICE_DIR/com.dreamwing.${SERVICE_NAME}.plist"
-
- cat > "$SERVICE_FILE" < "$SERVICE_FILE" <
@@ -183,32 +192,32 @@ if [ "$OS_TYPE" = "Darwin" ]; then
EOF
-
- echo "đ Service file created at: $SERVICE_FILE"
- echo "đ Loading macOS Launch Agent (com.dreamwing.${SERVICE_NAME})..."
- launchctl load -w "$SERVICE_FILE" >/dev/null 2>&1 || true
- # If already loaded and we just want to restart:
- launchctl unload "$SERVICE_FILE" >/dev/null 2>&1 || true
- launchctl load -w "$SERVICE_FILE"
- echo -e "${GREEN}â
Service started!${NC}"
+
+ echo "đ Service file created at: $SERVICE_FILE"
+ echo "đ Loading macOS Launch Agent (com.dreamwing.${SERVICE_NAME})..."
+ launchctl load -w "$SERVICE_FILE" >/dev/null 2>&1 || true
+ # If already loaded and we just want to restart:
+ launchctl unload "$SERVICE_FILE" >/dev/null 2>&1 || true
+ launchctl load -w "$SERVICE_FILE"
+ echo -e "${GREEN}â
Service started!${NC}"
-else
- # Linux systemd setup
- SERVICE_FILE="$HOME/.config/systemd/user/${SERVICE_NAME}.service"
- USE_USER_SYSTEMD=true
+ else
+ # Linux systemd setup
+ SERVICE_FILE="$HOME/.config/systemd/user/${SERVICE_NAME}.service"
+ USE_USER_SYSTEMD=true
- if [ ! -d "$HOME/.config/systemd/user" ]; then
- mkdir -p "$HOME/.config/systemd/user"
- fi
+ if [ ! -d "$HOME/.config/systemd/user" ]; then
+ mkdir -p "$HOME/.config/systemd/user"
+ fi
- # Check if user dbus is active (common issue in bare VPS)
- if ! systemctl --user list-units >/dev/null 2>&1; then
- echo -e "${YELLOW}â ī¸ User-level systemd not available. Generating standard systemd file...${NC}"
- USE_USER_SYSTEMD=false
- SERVICE_FILE="/tmp/${SERVICE_NAME}.service"
- fi
+ # Check if user dbus is active (common issue in bare VPS)
+ if ! systemctl --user list-units >/dev/null 2>&1; then
+ echo -e "${YELLOW}â ī¸ User-level systemd not available. Generating standard systemd file...${NC}"
+ USE_USER_SYSTEMD=false
+ SERVICE_FILE="/tmp/${SERVICE_NAME}.service"
+ fi
- cat > "$SERVICE_FILE" < "$SERVICE_FILE" < /dev/null; then
- echo "âŦī¸ Downloading cloudflared..."
- # Detect arch
- ARCH=$(uname -m)
+ if ! command -v cloudflared &> /dev/null && [ ! -x "./cloudflared" ]; then
+ echo -e "${YELLOW}â ī¸ cloudflared not found.${NC}"
+ echo " Please install it manually to enable remote access:"
if [ "$OS_TYPE" = "Darwin" ]; then
- if [[ "$ARCH" == "x86_64" ]] || [[ "$ARCH" == "amd64" ]]; then
- wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz -O cloudflared.tgz
- tar -xzf cloudflared.tgz && rm cloudflared.tgz
- elif [[ "$ARCH" == "arm64" ]] || [[ "$ARCH" == "aarch64" ]]; then
- wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64.tgz -O cloudflared.tgz
- tar -xzf cloudflared.tgz && rm cloudflared.tgz
- else
- echo "â Architecture $ARCH not supported for macOS auto-download."
- exit 1
- fi
+ echo " brew install cloudflared"
else
- if [[ "$ARCH" == "x86_64" ]]; then
- wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -O cloudflared
- elif [[ "$ARCH" == "aarch64" ]]; then
- wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64 -O cloudflared
- else
- echo "â Architecture $ARCH not supported for Linux auto-download."
- exit 1
- fi
+ echo " sudo apt install cloudflared (Debian/Ubuntu)"
+ echo " Or: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/"
fi
- chmod +x cloudflared
+ echo " Then re-run this setup."
+ ENABLE_TUNNEL="n"
fi
# If no token and NOT quick mode, ask for it
@@ -465,30 +462,34 @@ fi
if [ "$QUICK_TUNNEL" = true ] || [ -z "$CF_TOKEN" ]; then
# ONLY if VPN is NOT used OR Force CF is enabled
if [ "$USE_VPN" = false ] || [ "$FORCE_CF" = true ]; then
- echo "âŗ Waiting for Quick Tunnel URL (max 20s)..."
-
- # Loop wait for 20s
- for i in {1..20}; do
- if [ -f "$APP_DIR/.quick_tunnel_url" ]; then
- QURL=$(cat "$APP_DIR/.quick_tunnel_url")
- echo -e "\n${GREEN}đ ClawBridge Dashboard Live:${NC}"
- echo -e "đ ${BLUE}${QURL}${NC}"
- echo -e "â ī¸ Note: This link expires if the dashboard restarts."
- print_qr "$QURL"
- break
- fi
- sleep 1
- echo -n "."
- done
-
- if [ ! -f "$APP_DIR/.quick_tunnel_url" ]; then
- if [ "$OS_TYPE" = "Darwin" ]; then
- echo -e "\n${YELLOW}â ī¸ URL not ready yet. Check logs later: tail -f /tmp/com.dreamwing.${SERVICE_NAME}.log${NC}"
- elif [ "$USE_USER_SYSTEMD" = true ]; then
- echo -e "\n${YELLOW}â ī¸ URL not ready yet. Check logs later: journalctl --user -u ${SERVICE_NAME} -f${NC}"
- else
- echo -e "\n${YELLOW}â ī¸ URL not ready yet. Check logs later: sudo journalctl -u ${SERVICE_NAME} -f${NC}"
+ if [ "$ENABLE_SERVICE" = true ]; then
+ echo "âŗ Waiting for Quick Tunnel URL (max 20s)..."
+
+ # Loop wait for 20s
+ for i in {1..20}; do
+ if [ -f "$APP_DIR/.quick_tunnel_url" ]; then
+ QURL=$(cat "$APP_DIR/.quick_tunnel_url")
+ echo -e "\n${GREEN}đ ClawBridge Dashboard Live:${NC}"
+ echo -e "đ ${BLUE}${QURL}${NC}"
+ echo -e "â ī¸ Note: This link expires if the dashboard restarts."
+ print_qr "$QURL"
+ break
+ fi
+ sleep 1
+ echo -n "."
+ done
+
+ if [ ! -f "$APP_DIR/.quick_tunnel_url" ]; then
+ if [ "$OS_TYPE" = "Darwin" ]; then
+ echo -e "\n${YELLOW}â ī¸ URL not ready yet. Check logs later: tail -f /tmp/com.dreamwing.${SERVICE_NAME}.log${NC}"
+ elif [ "$USE_USER_SYSTEMD" = true ]; then
+ echo -e "\n${YELLOW}â ī¸ URL not ready yet. Check logs later: journalctl --user -u ${SERVICE_NAME} -f${NC}"
+ else
+ echo -e "\n${YELLOW}â ī¸ URL not ready yet. Check logs later: sudo journalctl -u ${SERVICE_NAME} -f${NC}"
+ fi
fi
+ else
+ echo -e "\n${YELLOW}âšī¸ Quick Tunnel is configured. Run 'npm start' or 'node index.js' to see your public URL.${NC}"
fi
fi
fi
diff --git a/tunnel.js b/tunnel.js
index c5f22de..241b947 100644
--- a/tunnel.js
+++ b/tunnel.js
@@ -4,70 +4,25 @@ const os = require('os');
const { spawn } = require('child_process');
const BIN_NAME = 'cloudflared';
-const BIN_PATH = path.join(__dirname, BIN_NAME);
+let BIN_PATH = path.join(__dirname, BIN_NAME);
const PID_FILE = path.join(__dirname, '.cloudflared.pid');
-function getDownloadUrl() {
- const arch = os.arch(); // 'x64', 'arm64', etc.
- const platform = os.platform(); // 'linux', 'darwin', etc.
-
- const archMap = {
- 'x64': 'amd64',
- 'arm64': 'arm64',
- 'arm': 'arm',
- };
-
- const cfArch = archMap[arch];
- if (!cfArch) {
- throw new Error(`Unsupported architecture: ${arch}. Supported: x64, arm64, arm`);
- }
-
- if (platform === 'linux') {
- return `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cfArch}`;
- } else if (platform === 'darwin') {
- return `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${cfArch}.tgz`;
- }
-
- throw new Error(`Unsupported platform: ${platform}. Supported: linux, darwin`);
-}
-
async function downloadBinary() {
+ // Check local binary first (legacy or manual placement)
if (fs.existsSync(BIN_PATH) && fs.statSync(BIN_PATH).size > 1000000) return;
- const url = getDownloadUrl();
- console.log(`[Tunnel] Downloading cloudflared for ${os.platform()}/${os.arch()}...`);
-
- const https = require('https');
- const http = require('http');
+ // Check system PATH
+ const { execSync } = require('child_process');
+ try {
+ const systemPath = execSync('which cloudflared', { encoding: 'utf8' }).trim();
+ if (systemPath && fs.existsSync(systemPath)) {
+ BIN_PATH = systemPath;
+ return;
+ }
+ } catch (e) { /* expected if not found in PATH */ }
- return new Promise((resolve, reject) => {
- const download = (downloadUrl, redirects = 0) => {
- if (redirects > 5) return reject(new Error('Too many redirects'));
- const client = downloadUrl.startsWith('https') ? https : http;
- client.get(downloadUrl, (res) => {
- // Follow redirects (GitHub releases use 302)
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
- return download(res.headers.location, redirects + 1);
- }
- if (res.statusCode !== 200) {
- return reject(new Error(`Download failed: HTTP ${res.statusCode}`));
- }
- const file = fs.createWriteStream(BIN_PATH);
- res.pipe(file);
- file.on('finish', () => {
- file.close();
- fs.chmodSync(BIN_PATH, '755');
- console.log('[Tunnel] Download complete.');
- resolve();
- });
- file.on('error', (err) => {
- fs.unlinkSync(BIN_PATH);
- reject(err);
- });
- }).on('error', reject);
- };
- download(url);
- });
+ // If both fail, throw error so index.js catches it and doesn't start tunnel
+ throw new Error('cloudflared not found. Install via: brew install cloudflared (macOS) or apt install cloudflared (Linux)');
}
function stopExistingTunnel() {