Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<runtime>
<type>node</type>
<entrypoint>index.js</entrypoint>
<persistence>Registers a user-level systemd service (clawbridge.service) that auto-starts on login and restarts on failure.</persistence>
<persistence>Optional. Run setup.sh --enable-service to register a user-level systemd service for auto-start. Not enabled by default.</persistence>
</runtime>

<!-- System requirements -->
Expand Down Expand Up @@ -49,24 +49,30 @@
<path type="write" location="skills/clawbridge/cloudflared" description="cloudflared binary, only if downloaded during tunnel setup." />
</filesystem>

<!-- Installation — uses the script bundled in this repository -->
<!-- Installation -->
<install>
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Run setup before launching app in skill install command

The new <install> command starts node index.js directly, but fresh installs do not have .env yet and src/config.js exits when ACCESS_KEY is unset. That means the documented install path fails to boot and does not generate credentials for login. Call ./setup.sh (or otherwise create .env with ACCESS_KEY) before starting the Node process.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<install> skips setup.sh, so no ACCESS_KEY is generated

The new <install> command runs node index.js directly:

git clone https://github.com/dreamwing/clawbridge.git skills/clawbridge && cd skills/clawbridge && npm install --production && node index.js

setup.sh is what generates the .env file with a random ACCESS_KEY (and PORT). Without running it first, the app starts with no credentials — users have no access key to log in with, and no port override if 3000 is busy. The <install> path should run setup.sh before starting the process:

Suggested change
git clone https://github.com/dreamwing/clawbridge.git skills/clawbridge && cd skills/clawbridge && npm install --production && node index.js
git clone https://github.com/dreamwing/clawbridge.git skills/clawbridge && cd skills/clawbridge && npm install --production && ./setup.sh && node index.js

</install>

<instructions>
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.

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
Expand Down
4 changes: 2 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
179 changes: 90 additions & 89 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ NO_TUNNEL=false
QUICK_TUNNEL=false
FORCE_CF=false

ENABLE_SERVICE=false

for arg in "$@"
do
case $arg in
Expand All @@ -42,6 +44,10 @@ do
FORCE_CF=true
shift
;;
--enable-service)
ENABLE_SERVICE=true
shift
;;
esac
done

Expand Down Expand Up @@ -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..."
Expand All @@ -123,6 +130,7 @@ else
else
RAND_KEY=$ACCESS_KEY
fi
chmod 600 "$ENV_FILE"
fi

# 3b. Auto-detect OPENCLAW_PATH
Expand All @@ -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" <<EOF
if [ "$ENABLE_SERVICE" = true ]; then
if [ "$OS_TYPE" = "Darwin" ]; then
Comment on lines +158 to +159
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Skip service restart when service mode is disabled

This new opt-in gate allows setup.sh to run without registering launchd/systemd, but the tunnel section still unconditionally restarts a service later (launchctl load / systemctl restart). In the default --enable-service-absent flow, users who enable tunnel setup hit those restart commands with no unit/plist present, and because the script runs with set -e, setup aborts instead of completing. Guard the restart block behind ENABLE_SERVICE=true (or start the app directly in non-service mode).

Useful? React with 👍 / 👎.

# macOS launchd setup
SERVICE_DIR="$HOME/Library/LaunchAgents"
mkdir -p "$SERVICE_DIR"
SERVICE_FILE="$SERVICE_DIR/com.dreamwing.${SERVICE_NAME}.plist"

cat > "$SERVICE_FILE" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
Expand Down Expand Up @@ -183,32 +192,32 @@ if [ "$OS_TYPE" = "Darwin" ]; then
</dict>
</plist>
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" <<EOF
cat > "$SERVICE_FILE" <<EOF
[Unit]
Description=ClawBridge Dashboard (${SERVICE_NAME})
After=network.target
Expand All @@ -226,21 +235,24 @@ EnvironmentFile=$APP_DIR/.env
WantedBy=default.target
EOF

echo "📝 Service file created at: $SERVICE_FILE"

if [ "$USE_USER_SYSTEMD" = true ]; then
echo "🚀 Enabling User Service ($SERVICE_NAME)..."
systemctl --user daemon-reload
systemctl --user enable "$SERVICE_NAME"
systemctl --user restart "$SERVICE_NAME"
echo -e "${GREEN}✅ Service started!${NC}"
else
echo -e "${YELLOW}👉 Please run the following command with sudo to install the service:${NC}"
echo "sudo mv $SERVICE_FILE /etc/systemd/system/${SERVICE_NAME}.service"
echo "sudo systemctl daemon-reload"
echo "sudo systemctl enable ${SERVICE_NAME}"
echo "sudo systemctl start ${SERVICE_NAME}"
echo "📝 Service file created at: $SERVICE_FILE"

if [ "$USE_USER_SYSTEMD" = true ]; then
echo "🚀 Enabling User Service ($SERVICE_NAME)..."
systemctl --user daemon-reload
systemctl --user enable "$SERVICE_NAME"
systemctl --user restart "$SERVICE_NAME"
echo -e "${GREEN}✅ Service started!${NC}"
else
echo -e "${YELLOW}👉 Please run the following command with sudo to install the service:${NC}"
echo "sudo mv $SERVICE_FILE /etc/systemd/system/${SERVICE_NAME}.service"
echo "sudo systemctl daemon-reload"
echo "sudo systemctl enable ${SERVICE_NAME}"
echo "sudo systemctl start ${SERVICE_NAME}"
fi
fi
else
echo -e "${YELLOW}ℹ️ Skipping service registration. To enable auto-start, run with: ./setup.sh --enable-service${NC}"
fi

# 5. Remote Access (Cloudflare Tunnel)
Expand Down Expand Up @@ -319,32 +331,17 @@ if [[ "$ENABLE_TUNNEL" =~ ^[Yy]$ ]] || [ "$USE_VPN" = true ]; then
ENABLE_TUNNEL="y"
fi

if ! command -v cloudflared &> /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
Comment on lines +344 to 345
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Disable tunnel env writes when cloudflared is missing

When cloudflared is not installed, this branch sets ENABLE_TUNNEL="n", but a supplied --token is still preserved and later written back with ENABLE_EMBEDDED_TUNNEL=true. In that case every app start still attempts embedded tunnel startup and fails in tunnel.downloadBinary(), so remote access remains broken until users manually clean .env. Clear CF_TOKEN or skip tunnel env writes after the missing-binary check.

Useful? React with 👍 / 👎.


# If no token and NOT quick mode, ask for it
Expand Down Expand Up @@ -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
Expand Down
71 changes: 13 additions & 58 deletions tunnel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)');
}
Comment on lines 10 to 26
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

downloadBinary is a misleading name — it no longer downloads anything

The function was renamed in spirit but not in name: it now only resolves an existing binary (checks local path, then falls back to which cloudflared). Calling it downloadBinary will confuse future maintainers who expect it to fetch a remote binary, and may cause callers in index.js to mis-handle the thrown error (expecting a network failure rather than a "binary not found" signal).

Consider renaming it to resolveBinary or findCloudflared to match its new responsibility.


function stopExistingTunnel() {
Expand Down
Loading