clawtime
Install, configure, start, and troubleshoot ClawTime — a private self-hosted webchat UI for OpenClaw with passkey (Face ID) auth, Piper TTS voice, and 3D avatar. Requires Cloudflare tunnel for HTTPS (passkeys need a real domain). Use when the user wants to install ClawTime on a new machine, configure the tunnel, register a passkey, set up TTS, start/stop the server, or diagnose errors. Triggered by requests like "install clawtime", "set up clawtime", "start clawtime", "clawtime isn't working", "register passkey", "device auth issue".
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install openclaw-skills-clawtime-setup
Repository
Skill path: skills/bewareofddog/clawtime-setup
Install, configure, start, and troubleshoot ClawTime — a private self-hosted webchat UI for OpenClaw with passkey (Face ID) auth, Piper TTS voice, and 3D avatar. Requires Cloudflare tunnel for HTTPS (passkeys need a real domain). Use when the user wants to install ClawTime on a new machine, configure the tunnel, register a passkey, set up TTS, start/stop the server, or diagnose errors. Triggered by requests like "install clawtime", "set up clawtime", "start clawtime", "clawtime isn't working", "register passkey", "device auth issue".
Open repositoryBest for
Primary workflow: Run DevOps.
Technical facets: Full Stack, Frontend, Backend, Security.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: openclaw.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install clawtime into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding clawtime to shared team environments
- Use clawtime for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: clawtime
description: >
Install, configure, start, and troubleshoot ClawTime — a private self-hosted webchat UI for
OpenClaw with passkey (Face ID) auth, Piper TTS voice, and 3D avatar. Requires Cloudflare tunnel
for HTTPS (passkeys need a real domain). Use when the user wants to install ClawTime on a new
machine, configure the tunnel, register a passkey, set up TTS, start/stop the server, or
diagnose errors. Triggered by requests like "install clawtime", "set up clawtime",
"start clawtime", "clawtime isn't working", "register passkey", "device auth issue".
metadata:
openclaw:
requires:
bins:
- node
- git
- cloudflared
- npm
optionalBins:
- python3
- ffmpeg
- piper
env:
- PUBLIC_URL
- GATEWAY_TOKEN
- SETUP_TOKEN
files:
- scripts/install.sh
- references/device-auth.md
- references/troubleshooting.md
- references/launchd.md
permissions:
- network (Cloudflare tunnel, git clone, npm install)
- keychain (store/retrieve GATEWAY_TOKEN and SETUP_TOKEN)
- filesystem (~/Projects/clawtime, ~/.clawtime, ~/.cloudflared, ~/Library/LaunchAgents)
---
# ClawTime — Local Installation with Cloudflare Tunnel
ClawTime is a private webchat UI connecting to the OpenClaw gateway via WebSocket.
Features: passkey (Face ID/Touch ID) auth, Piper TTS voice, 3D avatar.
**Why Cloudflare is required:** WebAuthn (passkeys) need HTTPS on a real domain.
`http://localhost` only works on the same machine — not from a phone on your network.
## Architecture
```
iPhone/Browser → https://portal.yourdomain.com → Cloudflare Tunnel → localhost:3000 (ClawTime) → ws://127.0.0.1:18789 (OpenClaw Gateway)
```
## Prerequisites
- Node.js v22+
- `cloudflared` CLI: `brew install cloudflared`
- A domain with DNS on Cloudflare (free tier works)
- OpenClaw running: `openclaw status`
- (Optional) Piper TTS + ffmpeg for voice
## Installation Steps
### 1. Clone & install
```bash
cd ~/Projects
git clone https://github.com/youngkent/clawtime.git
cd clawtime
npm install --legacy-peer-deps
```
### 2. Set up Cloudflare Tunnel
```bash
# Login to Cloudflare
cloudflared tunnel login
# Create named tunnel
cloudflared tunnel create clawtime
# Configure routing
# Edit ~/.cloudflared/config.yml:
```
**~/.cloudflared/config.yml:**
```yaml
tunnel: clawtime
credentials-file: /Users/YOUR_USER/.cloudflared/<tunnel-id>.json
ingress:
- hostname: portal.yourdomain.com
service: http://localhost:3000
- service: http_status:404
```
Then in Cloudflare DNS dashboard: add a CNAME record:
- Name: `portal` → Target: `<tunnel-id>.cfargotunnel.com` (Proxied ✅)
### 3. Configure OpenClaw gateway
The gateway must whitelist ClawTime's origin:
```bash
openclaw config patch '{"gateway":{"controlUi":{"allowedOrigins":["https://portal.yourdomain.com"]}}}'
openclaw gateway restart
```
⚠️ `PUBLIC_URL` must match this origin exactly — it's used as the WebSocket origin header for device auth.
### 4. Start ClawTime server
Minimum (no TTS):
```bash
cd ~/Projects/clawtime
PUBLIC_URL=https://portal.yourdomain.com \
SETUP_TOKEN=<your-setup-token> \
GATEWAY_TOKEN=<gateway-token> \
node server.js
```
With Piper TTS:
```bash
cd ~/Projects/clawtime
PUBLIC_URL=https://portal.yourdomain.com \
SETUP_TOKEN=<your-setup-token> \
GATEWAY_TOKEN=<gateway-token> \
BOT_NAME="Beware" \
BOT_EMOJI="🌀" \
TTS_COMMAND='python3 -m piper --data-dir ~/Documents/resources/piper-voices -m en_US-kusal-medium -f /tmp/clawtime-tts-tmp.wav -- {{TEXT}} && ffmpeg -y -loglevel error -i /tmp/clawtime-tts-tmp.wav {{OUTPUT}}' \
node server.js
```
⚠️ **TTS Security Note:** The `{{TEXT}}` placeholder is substituted into a shell command.
ClawTime's server **must** sanitize text before substitution to prevent command injection.
The server should strip or escape shell metacharacters (`; | & $ \` ( ) { } < >`) from user
input before passing it to the TTS command. If you're modifying the TTS pipeline, use
`child_process.execFile()` with argument arrays instead of `child_process.exec()` with string
interpolation.
### 5. Start Cloudflare tunnel
```bash
cloudflared tunnel run clawtime
```
### 6. Register passkey (first time only)
1. Open `https://portal.yourdomain.com/?setup=<your-setup-token>` in **Safari**
2. Follow the passkey (Face ID / Touch ID) prompt
3. ❌ Do NOT use private/incognito mode — Safari blocks passkeys there
4. ❌ Do NOT use Chrome on iOS — use Safari
After registration, access ClawTime at `https://portal.yourdomain.com`.
---
## Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `PUBLIC_URL` | ✅ | Public HTTPS URL (must match `allowedOrigins` in gateway config) |
| `GATEWAY_TOKEN` | ✅ | OpenClaw gateway auth token |
| `SETUP_TOKEN` | For registration | Passphrase for `?setup=<token>` passkey registration URL |
| `TTS_COMMAND` | For voice | Piper command with `{{TEXT}}` and `{{OUTPUT}}` placeholders |
| `BOT_NAME` | No | Display name (default: "Beware") |
| `BOT_EMOJI` | No | Avatar emoji (default: "🌀") |
| `PORT` | No | Server port (default: 3000) |
### Storing Tokens Securely (recommended)
Instead of passing tokens as plaintext env vars or in plist files, store them in macOS Keychain:
```bash
# Store tokens in Keychain
security add-generic-password -s "clawtime-gateway-token" -a "$(whoami)" -w "YOUR_GATEWAY_TOKEN"
security add-generic-password -s "clawtime-setup-token" -a "$(whoami)" -w "YOUR_SETUP_TOKEN"
```
Then retrieve them at launch time:
```bash
GATEWAY_TOKEN=$(security find-generic-password -s "clawtime-gateway-token" -a "$(whoami)" -w) \
SETUP_TOKEN=$(security find-generic-password -s "clawtime-setup-token" -a "$(whoami)" -w) \
PUBLIC_URL=https://portal.yourdomain.com \
node server.js
```
This avoids storing secrets in plaintext on disk.
---
## Device Authentication (Critical)
ClawTime authenticates with the OpenClaw gateway using Ed25519 keypair auth.
This is where most installs break — see details in `references/device-auth.md`.
**Quick summary:**
- Keypair auto-generated in `~/.clawtime/device-key.json` on first run
- Device ID = SHA-256 of raw 32-byte Ed25519 pubkey (NOT the full SPKI-encoded key)
- Signature payload format: `v2|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce`
- If device auth fails → delete `~/.clawtime/device-key.json` and restart
---
## Auto-Start on Boot (macOS launchd)
See `references/launchd.md` for plist templates for both the server and tunnel.
---
## Managing Services
```bash
# Stop server
pkill -f "node server.js"
# Stop tunnel
pkill -f "cloudflared"
# View logs (if backgrounded)
tail -f /tmp/clawtime.log
tail -f /tmp/cloudflared.log
# Restart after code/config changes
pkill -9 -f "node server.js"; sleep 2; # then re-run start command
```
---
## Getting the Gateway Token
```bash
# From macOS Keychain
security find-generic-password -s "openclaw-gateway-token" -a "$(whoami)" -w
# From config file
cat ~/.openclaw/openclaw.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('gateway',{}).get('token',''))"
```
---
## Passkey Operations
```bash
# Reset passkeys (re-register from scratch)
echo '[]' > ~/.clawtime/credentials.json
# Restart server, then visit /?setup=<token>
# Reset device key (new keypair on next restart)
rm ~/.clawtime/device-key.json
```
---
## Troubleshooting
See `references/troubleshooting.md` for all common errors and fixes.
See `references/device-auth.md` for deep-dive on gateway auth issues.
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/device-auth.md
```markdown
# ClawTime — Device Authentication Deep Dive
The gateway handshake is where most ClawTime installs fail. This doc explains exactly how it works.
## How It Works
ClawTime authenticates with the OpenClaw gateway using an Ed25519 keypair:
1. On first run, `~/.clawtime/device-key.json` is auto-generated (contains public + private key)
2. On every gateway connection, ClawTime signs a payload and sends it with the WebSocket handshake
3. The gateway verifies the signature and grants access
## Device ID
Device ID = **SHA-256 hash of the raw 32-byte Ed25519 public key** (hex encoded).
⚠️ Common mistake: Node.js `crypto.generateKeyPairSync('ed25519')` returns SPKI-encoded keys (DER format). You must strip the SPKI prefix before hashing.
SPKI prefix to strip: `302a300506032b6570032100` (12 bytes)
```js
// Correct device ID derivation
const { publicKey } = crypto.generateKeyPairSync('ed25519', {
publicKeyEncoding: { type: 'spki', format: 'der' }
});
const rawPubKey = publicKey.slice(12); // strip SPKI prefix → 32 raw bytes
const deviceId = crypto.createHash('sha256').update(rawPubKey).digest('hex');
```
## Signature Payload Format
The signed payload must follow this exact format (v2):
```
v2|{deviceId}|{clientId}|{clientMode}|{role}|{scopes}|{signedAtMs}|{token}|{nonce}
```
Example:
```
v2|a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2|webchat-ui|webchat|operator|operator.write,operator.read|1740000000000|your-gateway-token|3888df6c-uuid-here
```
Fields:
- `deviceId` — SHA-256 of raw pubkey (hex)
- `clientId` — typically `"webchat-ui"`
- `clientMode` — typically `"webchat"`
- `role` — typically `"operator"`
- `scopes` — `"operator.write,operator.read"`
- `signedAtMs` — Unix timestamp in milliseconds (`Date.now()`)
- `token` — your `GATEWAY_TOKEN` env var value
- `nonce` — a UUID v4 generated fresh each connection
## Encoding
- Public key sent as **base64url** encoded raw 32-byte key (NOT base64, NOT SPKI DER)
- Signature is **base64url** encoded
- The gateway verifies with: `crypto.verify(null, payload, publicKey, signature)`
```js
const sig = crypto.sign(null, Buffer.from(payload), privateKeyObj);
const sigBase64url = sig.toString('base64url');
const pubBase64url = rawPubKey.toString('base64url');
```
## WebSocket Handshake
The `device` object sent during connection:
```json
{
"deviceId": "<sha256-hex>",
"publicKey": "<base64url-raw-32-bytes>",
"signature": "<base64url>",
"payload": "v2|deviceId|clientId|...|nonce"
}
```
## Debugging Auth Issues
### "device signature invalid"
→ Payload format doesn't match. Verify the exact format above.
→ Check that `signedAtMs` isn't too old (gateway may have a time window).
### "device identity mismatch"
→ `deviceId` doesn't match the hash of the provided `publicKey`.
→ You're hashing the SPKI-encoded key instead of the raw 32 bytes.
→ Fix: strip the 12-byte SPKI prefix before hashing.
### New device, needs approval
→ First time a keypair is seen, the gateway may require manual approval.
→ Check OpenClaw gateway device management panel.
### Nuclear reset
```bash
rm ~/.clawtime/device-key.json
pkill -f "node server.js"; sleep 2
# Restart — fresh keypair generated
```
```
### references/launchd.md
```markdown
# ClawTime — Auto-Start with macOS launchd
Set up both ClawTime and the Cloudflare tunnel as launchd user agents.
They auto-start on login and restart automatically if they crash.
## Important: Token Security
**Do NOT put tokens directly in plist files** — plists are plaintext XML on disk.
Instead, store tokens in macOS Keychain and use a wrapper script to load them at launch.
### Store tokens in Keychain (one-time setup)
```bash
security add-generic-password -U -s "clawtime-gateway-token" -a "$(whoami)" -w "YOUR_GATEWAY_TOKEN"
security add-generic-password -U -s "clawtime-setup-token" -a "$(whoami)" -w "YOUR_SETUP_TOKEN"
```
### Create wrapper script
File: `~/Projects/clawtime/launchd-start.sh`
```bash
#!/usr/bin/env bash
# Wrapper script for launchd — loads tokens from Keychain securely
export GATEWAY_TOKEN=$(security find-generic-password -s "clawtime-gateway-token" -a "$(whoami)" -w)
export SETUP_TOKEN=$(security find-generic-password -s "clawtime-setup-token" -a "$(whoami)" -w)
if [ -z "$GATEWAY_TOKEN" ]; then
echo "ERROR: Gateway token not found in Keychain" >&2
exit 1
fi
export PUBLIC_URL="https://portal.yourdomain.com"
export BOT_NAME="Beware"
export BOT_EMOJI="🌀"
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
# Optional: uncomment and edit for TTS
# export TTS_COMMAND='python3 -m piper --data-dir /Users/YOUR_USER/Documents/resources/piper-voices -m en_US-kusal-medium -f /tmp/clawtime-tts-tmp.wav -- {{TEXT}} && ffmpeg -y -loglevel error -i /tmp/clawtime-tts-tmp.wav {{OUTPUT}}'
cd /Users/YOUR_USER/Projects/clawtime
exec /opt/homebrew/bin/node server.js
```
```bash
chmod +x ~/Projects/clawtime/launchd-start.sh
```
---
## ClawTime Server Plist
File: `~/Library/LaunchAgents/com.clawtime.server.plist`
```xml
<?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">
<dict>
<key>Label</key>
<string>com.clawtime.server</string>
<key>ProgramArguments</key>
<array>
<string>/Users/YOUR_USER/Projects/clawtime/launchd-start.sh</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/YOUR_USER/Projects/clawtime</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/clawtime.log</string>
<key>StandardErrorPath</key>
<string>/tmp/clawtime-error.log</string>
</dict>
</plist>
```
**Note:** No tokens in the plist — they're loaded from Keychain by the wrapper script.
---
## Cloudflare Tunnel Plist
File: `~/Library/LaunchAgents/com.clawtime.tunnel.plist`
```xml
<?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">
<dict>
<key>Label</key>
<string>com.clawtime.tunnel</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/cloudflared</string>
<string>tunnel</string>
<string>run</string>
<string>clawtime</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/cloudflared.log</string>
<key>StandardErrorPath</key>
<string>/tmp/cloudflared-error.log</string>
</dict>
</plist>
```
---
## Commands
```bash
# Load (start now + persist)
launchctl load ~/Library/LaunchAgents/com.clawtime.server.plist
launchctl load ~/Library/LaunchAgents/com.clawtime.tunnel.plist
# Unload (stop + remove from startup)
launchctl unload ~/Library/LaunchAgents/com.clawtime.server.plist
launchctl unload ~/Library/LaunchAgents/com.clawtime.tunnel.plist
# Check status
launchctl list | grep clawtime
# Reload after editing plist (unload then load)
launchctl unload ~/Library/LaunchAgents/com.clawtime.server.plist
launchctl load ~/Library/LaunchAgents/com.clawtime.server.plist
# View logs
tail -f /tmp/clawtime.log
tail -f /tmp/clawtime-error.log
tail -f /tmp/cloudflared.log
```
---
## Notes
- Services run as your user (not root) — start on login, not on cold boot
- `KeepAlive: true` → launchd auto-restarts if process dies
- Find node path: `which node` (use full path in wrapper script)
- If Mac restarts and auto-login is enabled, services start automatically
- To update tokens: update in Keychain (`security add-generic-password -U ...`), then unload/load the plist
```
### references/troubleshooting.md
```markdown
# ClawTime Troubleshooting
## Gateway Errors
### "origin not allowed"
ClawTime's origin isn't whitelisted in the gateway.
```bash
openclaw config patch '{"gateway":{"controlUi":{"allowedOrigins":["http://localhost:3000"]}}}'
openclaw gateway restart
```
### "device signature invalid"
Signature payload format mismatch. Delete the keypair and restart (new one auto-generates):
```bash
rm ~/.clawtime/device-key.json
pkill -f "node server.js"; sleep 2
# Restart with start command
```
### "device identity mismatch"
Device ID must be SHA-256 of the raw 32-byte Ed25519 public key (not SPKI DER).
Same fix: delete `~/.clawtime/device-key.json` and restart.
### "missing scope: operator.write"
Device auth isn't working. Verify `device-key.json` exists and has correct permissions:
```bash
ls -la ~/.clawtime/device-key.json # should be -rw-------
chmod 600 ~/.clawtime/device-key.json
```
Do NOT use `allowInsecureAuth` as a workaround.
### WebSocket connect loop / never connects
- Confirm OpenClaw gateway is running: `openclaw gateway status`
- Confirm `GATEWAY_TOKEN` is correct
- Confirm `PUBLIC_URL` matches the origin you're accessing from
---
## Passkey / Auth Errors
### "Passkey access denied" on iOS
- Not in private/incognito mode? → Switch to regular Safari
- Using Chrome? → Switch to Safari (Chrome doesn't support local passkeys the same way)
- Already registered on a different domain? → Old passkeys won't work if PUBLIC_URL changed
### Registration URL not working
- Use `http://localhost:3000/?setup=<SETUP_TOKEN>` exactly
- Confirm SETUP_TOKEN matches what was set in the server start command
### Reset all passkeys (start fresh)
```bash
echo '[]' > ~/.clawtime/credentials.json
pkill -f "node server.js"; sleep 2
# Restart, then re-register
```
---
## Server Errors
### Port 3000 already in use
```bash
lsof -i :3000 # find what's using it
pkill -9 -f "node server.js"
sleep 2
# Restart
```
### Server starts but browser shows blank / error
- Hard-refresh: Cmd+Shift+R in browser
- Check logs: `tail -50 /tmp/clawtime.log`
- Try: `pkill -f "node server.js"` → wait 2s → restart
### npm install fails
```bash
cd ~/Projects/clawtime
rm -rf node_modules package-lock.json
npm install --legacy-peer-deps
```
---
## TTS Errors
### No voice / TTS silent
- Confirm `TTS_COMMAND` env var is set when starting server
- Test Piper manually: `python3 -m piper --help`
- Test ffmpeg: `ffmpeg -version`
- Check voice model exists: `ls ~/Documents/resources/piper-voices/`
### TTS command fails
- The `{{TEXT}}` placeholder must be in the command
- The `{{OUTPUT}}` placeholder must be in the command
- Ensure piper voices directory path is correct
- Try the TTS command manually replacing placeholders with test values
---
## Device Key Technical Details
ClawTime authenticates with the gateway using Ed25519:
- Keypair stored: `~/.clawtime/device-key.json` (perms: 0600)
- Device ID = SHA-256 of raw 32-byte pubkey (NOT SPKI DER)
- SPKI prefix `302a300506032b6570032100` must be stripped before hashing
- Signature payload format: `v2|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce`
- Public key + signature both base64url encoded
If a new device key is generated, the device may need approval in the OpenClaw gateway panel.
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### _meta.json
```json
{
"owner": "bewareofddog",
"slug": "clawtime-setup",
"displayName": "ClawTime Setup",
"latest": {
"version": "1.2.0",
"publishedAt": 1771782904127,
"commit": "https://github.com/openclaw/skills/commit/b5ab5d6f1db5ed981f8c5d4ec2cbc6845f6b7ed1"
},
"history": []
}
```
### scripts/install.sh
```bash
#!/usr/bin/env bash
# ClawTime Install Script
# Sets up ClawTime with Cloudflare tunnel for passkey (Face ID) access
# Usage: bash install.sh
set -e
REPO_URL="https://github.com/youngkent/clawtime.git"
INSTALL_DIR="$HOME/Projects/clawtime"
DATA_DIR="$HOME/.clawtime"
echo "=== ClawTime Installer ==="
echo ""
# ── 1. Prerequisites ──────────────────────────────────────────────────────────
echo "→ Checking prerequisites..."
MISSING=0
if ! command -v node &>/dev/null; then
echo " ✗ Node.js not found. Install: brew install node"
MISSING=1
fi
if ! command -v git &>/dev/null; then
echo " ✗ git not found. Install: brew install git"
MISSING=1
fi
if ! command -v cloudflared &>/dev/null; then
echo " ✗ cloudflared not found. Install: brew install cloudflared"
MISSING=1
fi
if ! command -v openclaw &>/dev/null; then
echo " ✗ openclaw CLI not found. Is OpenClaw installed?"
MISSING=1
fi
if [ $MISSING -ne 0 ]; then
echo ""
echo "Install missing prerequisites and re-run."
exit 1
fi
echo "✓ Prerequisites OK"
echo ""
# ── 2. Cloudflare domain setup ────────────────────────────────────────────────
echo "→ Cloudflare Tunnel setup"
echo " You need: a domain with DNS managed by Cloudflare."
echo " If you already have a tunnel configured, you can skip this step."
echo ""
read -r -p " Your public URL (e.g. https://portal.yourdomain.com): " PUBLIC_URL
if [ -z "$PUBLIC_URL" ]; then
echo "✗ PUBLIC_URL is required."
exit 1
fi
# Check if tunnel exists
TUNNEL_EXISTS=$(cloudflared tunnel list 2>/dev/null | grep -c "clawtime" || true)
if [ "$TUNNEL_EXISTS" -eq 0 ]; then
echo ""
echo " No 'clawtime' tunnel found. Setting up..."
cloudflared tunnel login
cloudflared tunnel create clawtime
TUNNEL_ID=$(cloudflared tunnel list 2>/dev/null | grep "clawtime" | awk '{print $1}')
CLOUDFLARED_CONFIG="$HOME/.cloudflared/config.yml"
echo " Writing ~/.cloudflared/config.yml..."
cat > "$CLOUDFLARED_CONFIG" <<CFCONFIG
tunnel: clawtime
credentials-file: $HOME/.cloudflared/$TUNNEL_ID.json
ingress:
- hostname: $(echo "$PUBLIC_URL" | sed 's|https://||')
service: http://localhost:3000
- service: http_status:404
CFCONFIG
echo ""
echo " ⚠️ ACTION REQUIRED: Add this CNAME in your Cloudflare DNS dashboard:"
echo " Name: portal (or whatever subdomain you chose)"
echo " Target: $TUNNEL_ID.cfargotunnel.com"
echo " Proxy: ✅ Proxied (orange cloud)"
echo ""
read -r -p " Press Enter once you've added the DNS record..."
else
echo " ✓ 'clawtime' tunnel already exists"
fi
echo ""
# ── 3. Clone / update repo ────────────────────────────────────────────────────
if [ -d "$INSTALL_DIR/.git" ]; then
echo "→ Repo found at $INSTALL_DIR — pulling latest..."
cd "$INSTALL_DIR"
git pull --ff-only 2>/dev/null || echo " (up to date or local changes present)"
else
echo "→ Cloning ClawTime to $INSTALL_DIR..."
mkdir -p "$HOME/Projects"
git clone "$REPO_URL" "$INSTALL_DIR"
fi
# ── 4. Install npm dependencies ───────────────────────────────────────────────
echo "→ Installing npm dependencies..."
cd "$INSTALL_DIR"
npm install --legacy-peer-deps --silent
echo "✓ Dependencies installed"
echo ""
# ── 5. Create data directory ──────────────────────────────────────────────────
mkdir -p "$DATA_DIR"
# ── 6. Get gateway token ──────────────────────────────────────────────────────
echo "→ Getting OpenClaw gateway token..."
GATEWAY_TOKEN=$(cat ~/.openclaw/openclaw.json 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('gateway',{}).get('token',''))" 2>/dev/null)
if [ -z "$GATEWAY_TOKEN" ]; then
# Try keychain
GATEWAY_TOKEN=$(security find-generic-password -s "openclaw-gateway-token" -a "$(whoami)" -w 2>/dev/null || true)
fi
if [ -z "$GATEWAY_TOKEN" ]; then
echo " Could not auto-detect token."
read -r -p " Enter your OpenClaw gateway token: " GATEWAY_TOKEN
fi
[ -z "$GATEWAY_TOKEN" ] && { echo "✗ Gateway token required."; exit 1; }
echo "✓ Gateway token found"
echo ""
# ── 7. Setup token ────────────────────────────────────────────────────────────
read -r -p "→ Enter a SETUP_TOKEN (passphrase to protect passkey registration): " SETUP_TOKEN
if [ -z "$SETUP_TOKEN" ]; then
SETUP_TOKEN=$(python3 -c "import secrets; print(secrets.token_hex(16))")
echo " (generated: $SETUP_TOKEN — save this!)"
fi
echo ""
# ── 8. Customize bot ─────────────────────────────────────────────────────────
read -r -p "→ Bot name (press Enter for 'Beware'): " BOT_NAME
BOT_NAME="${BOT_NAME:-Beware}"
read -r -p "→ Bot emoji (press Enter for '🌀'): " BOT_EMOJI
BOT_EMOJI="${BOT_EMOJI:-🌀}"
echo ""
# ── 9. Configure OpenClaw gateway ─────────────────────────────────────────────
echo "→ Configuring gateway to allow ClawTime origin ($PUBLIC_URL)..."
openclaw config patch "{\"gateway\":{\"controlUi\":{\"allowedOrigins\":[\"$PUBLIC_URL\"]}}}" 2>/dev/null && \
echo "✓ Gateway config updated" || \
echo " ⚠️ Config patch failed — add manually to ~/.openclaw/openclaw.json"
echo "→ Restarting gateway..."
openclaw gateway restart 2>/dev/null && echo "✓ Gateway restarted" || echo " ⚠️ Restart failed — run: openclaw gateway restart"
sleep 2
echo ""
# ── 10. Detect TTS ────────────────────────────────────────────────────────────
TTS_LINE=""
if command -v ffmpeg &>/dev/null && python3 -c "import piper" &>/dev/null 2>&1; then
VOICES_DIR="$HOME/Documents/resources/piper-voices"
if [ -d "$VOICES_DIR" ]; then
TTS_LINE="TTS_COMMAND='python3 -m piper --data-dir $VOICES_DIR -m en_US-kusal-medium -f /tmp/clawtime-tts-tmp.wav -- {{TEXT}} && ffmpeg -y -loglevel error -i /tmp/clawtime-tts-tmp.wav {{OUTPUT}}' \\"
echo "✓ Piper TTS detected — voice will be enabled"
fi
fi
# ── 11. Store tokens in Keychain ──────────────────────────────────────────────
echo "→ Storing tokens securely in macOS Keychain..."
security add-generic-password -U -s "clawtime-gateway-token" -a "$(whoami)" -w "$GATEWAY_TOKEN" 2>/dev/null && \
echo " ✓ Gateway token stored in Keychain" || \
echo " ⚠️ Could not store gateway token in Keychain"
security add-generic-password -U -s "clawtime-setup-token" -a "$(whoami)" -w "$SETUP_TOKEN" 2>/dev/null && \
echo " ✓ Setup token stored in Keychain" || \
echo " ⚠️ Could not store setup token in Keychain"
echo ""
# ── 12. Write start scripts ──────────────────────────────────────────────────
START_SERVER="$INSTALL_DIR/start-server.sh"
START_TUNNEL="$INSTALL_DIR/start-tunnel.sh"
START_ALL="$INSTALL_DIR/start.sh"
cat > "$START_SERVER" <<'STARTSCRIPT'
#!/usr/bin/env bash
# ClawTime server start script — tokens loaded from Keychain
cd "$(dirname "$0")"
GATEWAY_TOKEN=$(security find-generic-password -s "clawtime-gateway-token" -a "$(whoami)" -w 2>/dev/null)
SETUP_TOKEN=$(security find-generic-password -s "clawtime-setup-token" -a "$(whoami)" -w 2>/dev/null)
if [ -z "$GATEWAY_TOKEN" ]; then
echo "✗ Gateway token not found in Keychain. Run: security add-generic-password -s clawtime-gateway-token -a \$(whoami) -w YOUR_TOKEN"
exit 1
fi
STARTSCRIPT
# Append the non-secret config (these are safe to store in the script)
cat >> "$START_SERVER" <<STARTSCRIPT
PUBLIC_URL=$PUBLIC_URL \\
GATEWAY_TOKEN="\$GATEWAY_TOKEN" \\
SETUP_TOKEN="\$SETUP_TOKEN" \\
BOT_NAME="$BOT_NAME" \\
BOT_EMOJI="$BOT_EMOJI" \\
${TTS_LINE}
node server.js
STARTSCRIPT
cat > "$START_TUNNEL" <<TUNNELSCRIPT
#!/usr/bin/env bash
# Cloudflare tunnel start script
cloudflared tunnel run clawtime
TUNNELSCRIPT
cat > "$START_ALL" <<'ALLSCRIPT_HEAD'
#!/usr/bin/env bash
# Start both ClawTime and the Cloudflare tunnel in background
# Tokens loaded securely from macOS Keychain
GATEWAY_TOKEN=$(security find-generic-password -s "clawtime-gateway-token" -a "$(whoami)" -w 2>/dev/null)
SETUP_TOKEN=$(security find-generic-password -s "clawtime-setup-token" -a "$(whoami)" -w 2>/dev/null)
if [ -z "$GATEWAY_TOKEN" ]; then
echo "✗ Gateway token not found in Keychain."
exit 1
fi
echo "Starting ClawTime server..."
ALLSCRIPT_HEAD
cat >> "$START_ALL" <<ALLSCRIPT_BODY
PUBLIC_URL=$PUBLIC_URL \\
GATEWAY_TOKEN="\$GATEWAY_TOKEN" \\
SETUP_TOKEN="\$SETUP_TOKEN" \\
BOT_NAME="$BOT_NAME" \\
BOT_EMOJI="$BOT_EMOJI" \\
${TTS_LINE}
node "$INSTALL_DIR/server.js" &>/tmp/clawtime.log &
echo " PID: \$!"
echo "Starting Cloudflare tunnel..."
cloudflared tunnel run clawtime &>/tmp/cloudflared.log &
echo " PID: \$!"
echo ""
echo "✅ Both running in background"
echo " Server logs: tail -f /tmp/clawtime.log"
echo " Tunnel logs: tail -f /tmp/cloudflared.log"
ALLSCRIPT_BODY
chmod +x "$START_SERVER" "$START_TUNNEL" "$START_ALL"
# ── 12. Done ──────────────────────────────────────────────────────────────────
echo ""
echo "════════════════════════════════════════"
echo "✅ ClawTime installed!"
echo ""
echo " Start both: bash $START_ALL"
echo " Start server: bash $START_SERVER"
echo " Start tunnel: bash $START_TUNNEL"
echo ""
echo " Register passkey: $PUBLIC_URL/?setup=$SETUP_TOKEN"
echo " Chat UI: $PUBLIC_URL"
echo ""
echo " ⚠️ Use Safari (not Chrome) for passkey registration."
echo " ⚠️ Do NOT use private/incognito mode."
echo "════════════════════════════════════════"
```