AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# ClawdTalk Approval Requests
|
||||
#
|
||||
# Request user approval for sensitive actions during voice calls.
|
||||
# Sends push notification to user's phone and waits for response.
|
||||
#
|
||||
# Usage:
|
||||
# ./approval.sh request "Book flight LAX→JFK for $450"
|
||||
# ./approval.sh request "Send email to john@example.com" --details "Subject: Meeting tomorrow"
|
||||
# ./approval.sh request "Delete 50 files" --biometric --timeout 120
|
||||
# ./approval.sh status <request_id>
|
||||
#
|
||||
# Env vars: none
|
||||
# Endpoints: https://clawdtalk.com
|
||||
# Reads: skill-config.json
|
||||
# Writes: none
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
CONFIG_FILE="$SKILL_DIR/skill-config.json"
|
||||
|
||||
# Default timeout for waiting on approval (seconds)
|
||||
DEFAULT_TIMEOUT=300
|
||||
POLL_INTERVAL=2
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
check_config() {
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo -e "${RED}Error: Configuration not found. Run ./setup.sh first.${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if user has any registered devices (from ws-client cache)
|
||||
check_devices() {
|
||||
local status_file="$SKILL_DIR/.device-status"
|
||||
if [ -f "$status_file" ]; then
|
||||
local has_devices
|
||||
has_devices=$(jq -r '.has_devices // false' "$status_file" 2>/dev/null)
|
||||
if [ "$has_devices" = "false" ]; then
|
||||
return 1 # No devices
|
||||
fi
|
||||
fi
|
||||
return 0 # Has devices (or unknown - assume yes)
|
||||
}
|
||||
|
||||
get_config() {
|
||||
local key="$1"
|
||||
local value
|
||||
value=$(jq -r ".$key // empty" "$CONFIG_FILE" 2>/dev/null)
|
||||
|
||||
# Resolve ${ENV_VAR} references
|
||||
if [[ "$value" =~ ^\$\{([A-Z_][A-Z0-9_]*)\}$ ]]; then
|
||||
local env_var="${BASH_REMATCH[1]}"
|
||||
value="${!env_var:-$value}"
|
||||
fi
|
||||
|
||||
echo "$value"
|
||||
}
|
||||
|
||||
request_approval() {
|
||||
local action=""
|
||||
local details=""
|
||||
local require_biometric=false
|
||||
local timeout=$DEFAULT_TIMEOUT
|
||||
local wait=true
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--details)
|
||||
details="$2"
|
||||
shift 2
|
||||
;;
|
||||
--biometric)
|
||||
require_biometric=true
|
||||
shift
|
||||
;;
|
||||
--timeout)
|
||||
timeout="$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-wait)
|
||||
wait=false
|
||||
shift
|
||||
;;
|
||||
-*)
|
||||
echo -e "${RED}Unknown option: $1${NC}" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
if [ -z "$action" ]; then
|
||||
action="$1"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$action" ]; then
|
||||
echo -e "${RED}Error: Action description required${NC}" >&2
|
||||
echo "Usage: $0 request \"Description of action\" [--details \"More info\"] [--biometric] [--timeout 300]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if user has devices - skip API call entirely if not
|
||||
if ! check_devices; then
|
||||
echo "no_devices"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local api_key
|
||||
api_key=$(get_config "api_key")
|
||||
local server
|
||||
server=$(get_config "server")
|
||||
server="${server:-https://clawdtalk.com}"
|
||||
|
||||
if [ -z "$api_key" ]; then
|
||||
echo -e "${RED}Error: No API key configured${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build request body
|
||||
local body
|
||||
body=$(jq -n \
|
||||
--arg action "$action" \
|
||||
--arg details "$details" \
|
||||
--argjson biometric "$require_biometric" \
|
||||
--argjson expires_in "$timeout" \
|
||||
'{
|
||||
action: $action,
|
||||
require_biometric: $biometric,
|
||||
expires_in: $expires_in
|
||||
} + (if $details != "" then {details: $details} else {} end)'
|
||||
)
|
||||
|
||||
# Create approval request
|
||||
local response
|
||||
response=$(curl -s -X POST "$server/v1/approvals" \
|
||||
-H "Authorization: Bearer $api_key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$body")
|
||||
|
||||
local request_id
|
||||
request_id=$(echo "$response" | jq -r '.request_id // empty')
|
||||
|
||||
if [ -z "$request_id" ]; then
|
||||
local error
|
||||
error=$(echo "$response" | jq -r '.message // .error // "Unknown error"')
|
||||
echo -e "${RED}Error creating approval request: $error${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local devices_notified
|
||||
devices_notified=$(echo "$response" | jq -r '.devices_notified // 0')
|
||||
|
||||
if [ "$devices_notified" -eq 0 ]; then
|
||||
# No devices registered — return immediately
|
||||
echo "no_devices"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$wait" = false ]; then
|
||||
# Just return the request ID, don't wait
|
||||
echo "$request_id"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Wait for response
|
||||
echo -e "Waiting for approval (timeout: ${timeout}s)..." >&2
|
||||
|
||||
local start_time
|
||||
start_time=$(date +%s)
|
||||
local end_time=$((start_time + timeout))
|
||||
|
||||
while true; do
|
||||
local current_time
|
||||
current_time=$(date +%s)
|
||||
|
||||
if [ "$current_time" -ge "$end_time" ]; then
|
||||
echo "timeout"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check status
|
||||
local status_response
|
||||
status_response=$(curl -s "$server/v1/approvals/$request_id" \
|
||||
-H "Authorization: Bearer $api_key")
|
||||
|
||||
local status
|
||||
status=$(echo "$status_response" | jq -r '.status // "pending"')
|
||||
|
||||
case "$status" in
|
||||
approved)
|
||||
echo "approved"
|
||||
exit 0
|
||||
;;
|
||||
denied)
|
||||
echo "denied"
|
||||
exit 0
|
||||
;;
|
||||
expired)
|
||||
echo "expired"
|
||||
exit 0
|
||||
;;
|
||||
pending)
|
||||
# Still waiting
|
||||
sleep "$POLL_INTERVAL"
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unexpected status: $status${NC}" >&2
|
||||
echo "error"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
check_status() {
|
||||
local request_id="$1"
|
||||
|
||||
if [ -z "$request_id" ]; then
|
||||
echo -e "${RED}Error: Request ID required${NC}" >&2
|
||||
echo "Usage: $0 status <request_id>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local api_key
|
||||
api_key=$(get_config "api_key")
|
||||
local server
|
||||
server=$(get_config "server")
|
||||
server="${server:-https://clawdtalk.com}"
|
||||
|
||||
local response
|
||||
response=$(curl -s "$server/v1/approvals/$request_id" \
|
||||
-H "Authorization: Bearer $api_key")
|
||||
|
||||
local status
|
||||
status=$(echo "$response" | jq -r '.status // "unknown"')
|
||||
|
||||
echo "$status"
|
||||
}
|
||||
|
||||
list_approvals() {
|
||||
local status="${1:-pending}"
|
||||
|
||||
local api_key
|
||||
api_key=$(get_config "api_key")
|
||||
local server
|
||||
server=$(get_config "server")
|
||||
server="${server:-https://clawdtalk.com}"
|
||||
|
||||
curl -s "$server/v1/approvals?status=$status" \
|
||||
-H "Authorization: Bearer $api_key" | jq '.'
|
||||
}
|
||||
|
||||
show_help() {
|
||||
cat << 'EOF'
|
||||
ClawdTalk Approval Requests
|
||||
|
||||
Request user approval for sensitive actions. Sends a push notification
|
||||
to the user's phone and waits for their response.
|
||||
|
||||
COMMANDS:
|
||||
request <action> Create approval request and wait for response
|
||||
status <id> Check status of an existing request
|
||||
list [status] List approval requests (default: pending)
|
||||
|
||||
OPTIONS (for request):
|
||||
--details "text" Additional details to show user
|
||||
--biometric Require biometric auth (fingerprint/face) to approve
|
||||
--timeout <secs> How long to wait for response (default: 300)
|
||||
--no-wait Return request ID immediately, don't wait
|
||||
|
||||
EXAMPLES:
|
||||
# Simple approval
|
||||
./approval.sh request "Send email to boss@company.com"
|
||||
|
||||
# With details
|
||||
./approval.sh request "Book flight" --details "Delta 123, LAX→JFK, $450, Feb 15"
|
||||
|
||||
# Require biometric for sensitive action
|
||||
./approval.sh request "Transfer $5000 to external account" --biometric
|
||||
|
||||
# Quick check without waiting
|
||||
id=$(./approval.sh request "Delete files" --no-wait)
|
||||
# ... do other things ...
|
||||
status=$(./approval.sh status "$id")
|
||||
|
||||
OUTPUT:
|
||||
approved - User approved the action
|
||||
denied - User denied the action
|
||||
timeout - No response within timeout period
|
||||
expired - Request expired before user responded
|
||||
no_devices - User has no mobile app installed (no registered devices)
|
||||
pending - Still waiting (for status command)
|
||||
EOF
|
||||
}
|
||||
|
||||
# Main
|
||||
check_config
|
||||
|
||||
case "${1:-}" in
|
||||
request)
|
||||
shift
|
||||
request_approval "$@"
|
||||
;;
|
||||
status)
|
||||
shift
|
||||
check_status "$@"
|
||||
;;
|
||||
list)
|
||||
shift
|
||||
list_approvals "$@"
|
||||
;;
|
||||
help|--help|-h)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# ClawdTalk Outbound Call Script
|
||||
# Initiates an outbound call to user's phone or an external number
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/call.sh # Call your phone
|
||||
# ./scripts/call.sh "Hey, what's up?" # Call with greeting
|
||||
# ./scripts/call.sh --to +15551234567 # Call external (paid only)
|
||||
# ./scripts/call.sh --to +1555... --purpose "Schedule meeting" # External with purpose
|
||||
# ./scripts/call.sh status <call_id> # Check call status
|
||||
# ./scripts/call.sh end <call_id> # End an active call
|
||||
#
|
||||
# Env vars: none
|
||||
# Endpoints: https://clawdtalk.com
|
||||
# Reads: skill-config.json
|
||||
# Writes: none
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
CONFIG_FILE="$SKILL_DIR/skill-config.json"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
error() { echo -e "${RED}Error:${NC} $1" >&2; exit 1; }
|
||||
info() { echo -e "${GREEN}$1${NC}"; }
|
||||
warn() { echo -e "${YELLOW}$1${NC}"; }
|
||||
|
||||
# Load config
|
||||
[[ -f "$CONFIG_FILE" ]] || error "Config not found. Run ./setup.sh first."
|
||||
|
||||
# Resolve env vars in config
|
||||
resolve_config() {
|
||||
local config
|
||||
config=$(cat "$CONFIG_FILE")
|
||||
|
||||
# Find .env files
|
||||
local env_files=(
|
||||
"$HOME/.openclaw/.env"
|
||||
"$HOME/.clawdbot/.env"
|
||||
"$SKILL_DIR/.env"
|
||||
)
|
||||
|
||||
for env_file in "${env_files[@]}"; do
|
||||
if [[ -f "$env_file" ]]; then
|
||||
while IFS='=' read -r key value; do
|
||||
[[ -z "$key" || "$key" =~ ^# ]] && continue
|
||||
value="${value%\"}"
|
||||
value="${value#\"}"
|
||||
config="${config//\$\{$key\}/$value}"
|
||||
done < "$env_file"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "$config"
|
||||
}
|
||||
|
||||
CONFIG=$(resolve_config)
|
||||
API_KEY=$(echo "$CONFIG" | jq -r '.api_key // empty')
|
||||
SERVER=$(echo "$CONFIG" | jq -r '.server // "https://clawdtalk.com"')
|
||||
|
||||
[[ -n "$API_KEY" ]] || error "API key not configured. Run ./setup.sh"
|
||||
|
||||
# API helper
|
||||
api() {
|
||||
local method="$1"
|
||||
local endpoint="$2"
|
||||
local data="${3:-}"
|
||||
|
||||
local args=(-s -X "$method" -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json")
|
||||
[[ -n "$data" ]] && args+=(-d "$data")
|
||||
|
||||
curl "${args[@]}" "${SERVER}${endpoint}"
|
||||
}
|
||||
|
||||
# Commands
|
||||
cmd_call() {
|
||||
local greeting=""
|
||||
local to_number=""
|
||||
local purpose=""
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--to)
|
||||
to_number="$2"
|
||||
shift 2
|
||||
;;
|
||||
--purpose|--context)
|
||||
purpose="$2"
|
||||
shift 2
|
||||
;;
|
||||
-*)
|
||||
error "Unknown option: $1"
|
||||
;;
|
||||
*)
|
||||
greeting="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Build payload
|
||||
local payload='{}'
|
||||
|
||||
# Smart detection: if greeting looks like a phone number and no --to provided, treat it as --to
|
||||
if [[ -z "$to_number" && -n "$greeting" && "$greeting" =~ ^\+?[0-9]{10,15}$ ]]; then
|
||||
warn "Detected phone number in greeting, treating as --to target"
|
||||
to_number="$greeting"
|
||||
greeting=""
|
||||
fi
|
||||
|
||||
# Start with base object
|
||||
if [[ -n "$to_number" ]]; then
|
||||
payload=$(jq -n --arg t "$to_number" '{to: $t}')
|
||||
fi
|
||||
|
||||
# Add greeting if provided
|
||||
if [[ -n "$greeting" ]]; then
|
||||
payload=$(echo "$payload" | jq --arg g "$greeting" '. + {greeting: $g}')
|
||||
fi
|
||||
|
||||
# Add context with purpose for external calls
|
||||
if [[ -n "$purpose" ]]; then
|
||||
payload=$(echo "$payload" | jq --arg p "$purpose" '. + {context: {purpose: $p}}')
|
||||
fi
|
||||
|
||||
if [[ -n "$to_number" ]]; then
|
||||
info "Initiating outbound call to $to_number..."
|
||||
else
|
||||
info "Initiating outbound call to your phone..."
|
||||
fi
|
||||
|
||||
local result
|
||||
result=$(api POST "/v1/calls" "$payload")
|
||||
|
||||
local status
|
||||
status=$(echo "$result" | jq -r '.status // .error.code // "unknown"')
|
||||
|
||||
if [[ "$status" == "initiating" || "$status" == "ringing" ]]; then
|
||||
local call_id
|
||||
call_id=$(echo "$result" | jq -r '.call_id')
|
||||
info "Call initiated: $call_id"
|
||||
echo "$result" | jq .
|
||||
else
|
||||
error "Failed to initiate call: $(echo "$result" | jq -r '.error.message // .message // "Unknown error"')"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
local call_id="$1"
|
||||
[[ -n "$call_id" ]] || error "Usage: $0 status <call_id>"
|
||||
|
||||
api GET "/v1/calls/$call_id" | jq .
|
||||
}
|
||||
|
||||
cmd_end() {
|
||||
local call_id="$1"
|
||||
local reason="${2:-user_ended}"
|
||||
[[ -n "$call_id" ]] || error "Usage: $0 end <call_id> [reason]"
|
||||
|
||||
local payload
|
||||
payload=$(jq -n --arg r "$reason" '{reason: $r}')
|
||||
|
||||
info "Ending call $call_id..."
|
||||
api POST "/v1/calls/$call_id/end" "$payload" | jq .
|
||||
}
|
||||
|
||||
cmd_help() {
|
||||
cat <<EOF
|
||||
ClawdTalk Outbound Call
|
||||
|
||||
Usage:
|
||||
$0 Call your own phone (default)
|
||||
$0 "Hello!" Call with custom greeting
|
||||
$0 --to +15551234567 Call an external number (paid only)
|
||||
$0 --to +1555... --purpose "Schedule mtg" Call external with purpose
|
||||
$0 --to +1555... "Hi!" --purpose "..." External + greeting + purpose
|
||||
$0 status <call_id> Check call status
|
||||
$0 end <call_id> End an active call
|
||||
|
||||
Options:
|
||||
--to <number> Call external number instead of your own
|
||||
--purpose <text> Tell the AI why you're calling (critical for external calls)
|
||||
|
||||
Without --to: calls your verified phone number.
|
||||
With --to: calls the specified number (requires paid account with dedicated number).
|
||||
The --purpose flag tells the AI what the call is about so it knows what to do.
|
||||
EOF
|
||||
}
|
||||
|
||||
# Main
|
||||
case "${1:-}" in
|
||||
status)
|
||||
cmd_status "${2:-}"
|
||||
;;
|
||||
end)
|
||||
cmd_end "${2:-}" "${3:-}"
|
||||
;;
|
||||
help|--help|-h)
|
||||
cmd_help
|
||||
;;
|
||||
*)
|
||||
cmd_call "$@"
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,330 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# ClawdTalk - WebSocket Connection Manager
|
||||
#
|
||||
# Manages the WebSocket connection to ClawdTalk server for receiving
|
||||
# voice transcriptions and sending responses.
|
||||
# Works with both Clawdbot and OpenClaw.
|
||||
#
|
||||
# Usage: ./connect.sh {start|stop|status|restart} [--server <url>]
|
||||
#
|
||||
# Env vars: via .env
|
||||
# Endpoints: none (launches ws-client.js)
|
||||
# Reads: skill-config.json, .env
|
||||
# Writes: .connect.pid, .connect.log
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
CONFIG_FILE="$SKILL_DIR/skill-config.json"
|
||||
PID_FILE="$SKILL_DIR/.connect.pid"
|
||||
LOG_FILE="$SKILL_DIR/.connect.log"
|
||||
|
||||
# Parse server override from args
|
||||
SERVER_FLAG=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--server)
|
||||
SERVER_FLAG="--server $2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
CMD="${CMD:-$1}"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
print_status() {
|
||||
echo -e "${BLUE}📞 Clawd Talk Connection Manager${NC}"
|
||||
echo "================================="
|
||||
echo ""
|
||||
}
|
||||
|
||||
check_config() {
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo -e "${RED}❌ Configuration not found. Run './setup.sh' first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if we have API key
|
||||
local api_key=$(jq -r '.api_key // empty' "$CONFIG_FILE" 2>/dev/null || echo "")
|
||||
if [ -z "$api_key" ] || [ "$api_key" = "null" ] || [ "$api_key" = "YOUR_API_KEY_HERE" ]; then
|
||||
echo -e "${RED}❌ No API key configured.${NC}"
|
||||
echo ""
|
||||
echo "Get your API key from https://clawdtalk.com → Dashboard"
|
||||
echo "Then add it to skill-config.json"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_gateway_tools() {
|
||||
# Check if sessions_send is allowed on the gateway /tools/invoke endpoint
|
||||
# Without this, the voice assistant can't proxy questions to the Clawdbot
|
||||
local config_paths=(
|
||||
"$HOME/.openclaw/openclaw.json"
|
||||
"$HOME/.clawdbot/clawdbot.json"
|
||||
)
|
||||
|
||||
for cfg in "${config_paths[@]}"; do
|
||||
if [ -f "$cfg" ]; then
|
||||
local tools_allow=$(jq -r '.gateway.tools.allow // [] | join(",")' "$cfg" 2>/dev/null)
|
||||
if [ -z "$tools_allow" ] || ! echo "$tools_allow" | grep -q "sessions_send"; then
|
||||
echo -e "${YELLOW}⚠️ Gateway missing 'sessions_send' in tools allowlist${NC}"
|
||||
echo ""
|
||||
echo " The voice assistant needs sessions_send to proxy questions to your Clawdbot."
|
||||
echo " Add it to your config ($cfg):"
|
||||
echo ""
|
||||
echo ' "gateway": { "tools": { "allow": ["sessions_send"] } }'
|
||||
echo ""
|
||||
echo " Or ask your Clawdbot to run:"
|
||||
echo " openclaw config set gateway.tools.allow '[\"sessions_send\"]'"
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
echo -e "${YELLOW}⚠️ No OpenClaw/Clawdbot config found. Gateway tools check skipped.${NC}"
|
||||
return 0
|
||||
}
|
||||
|
||||
check_dependencies() {
|
||||
for tool in node jq; do
|
||||
if ! command -v "$tool" &> /dev/null; then
|
||||
echo -e "${RED}❌ Required tool '$tool' is not installed.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Check node_modules exist
|
||||
if [ ! -d "$SKILL_DIR/node_modules/ws" ]; then
|
||||
echo -e "${YELLOW}📦 Installing dependencies...${NC}"
|
||||
(cd "$SKILL_DIR" && npm install --production 2>/dev/null)
|
||||
if [ ! -d "$SKILL_DIR/node_modules/ws" ]; then
|
||||
echo -e "${RED}❌ Failed to install dependencies. Run 'npm install' in $SKILL_DIR${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e " ${GREEN}✓ Dependencies installed${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
is_running() {
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
local pid=$(cat "$PID_FILE")
|
||||
if ps -p "$pid" &> /dev/null; then
|
||||
return 0
|
||||
else
|
||||
# Stale PID file
|
||||
rm -f "$PID_FILE"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
start_connection() {
|
||||
if is_running; then
|
||||
echo -e "${YELLOW}⚠️ Connection already running (PID: $(cat "$PID_FILE"))${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "🚀 Starting WebSocket connection..."
|
||||
|
||||
# Source skill's own .env if it exists (for skill-specific env vars only)
|
||||
[ -f "$SKILL_DIR/.env" ] && . "$SKILL_DIR/.env"
|
||||
|
||||
# Rotate log if it's too big (> 1MB)
|
||||
if [ -f "$LOG_FILE" ] && [ $(stat -f%z "$LOG_FILE" 2>/dev/null || stat -c%s "$LOG_FILE" 2>/dev/null || echo 0) -gt 1048576 ]; then
|
||||
echo "🔄 Rotating large log file..."
|
||||
mv "$LOG_FILE" "${LOG_FILE}.1" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Start the WebSocket client in background (append to log)
|
||||
nohup node "$SCRIPT_DIR/ws-client.js" $SERVER_FLAG >> "$LOG_FILE" 2>&1 &
|
||||
local pid=$!
|
||||
echo $pid > "$PID_FILE"
|
||||
|
||||
# Give it a moment to start
|
||||
sleep 2
|
||||
|
||||
# Check if it's still running
|
||||
if ps -p "$pid" &> /dev/null; then
|
||||
echo -e " ✓ ${GREEN}WebSocket client started (PID: $pid)${NC}"
|
||||
echo ""
|
||||
echo "Use './scripts/connect.sh status' to check connection health"
|
||||
echo "Logs: $LOG_FILE"
|
||||
else
|
||||
rm -f "$PID_FILE"
|
||||
echo -e " ❌ ${RED}Failed to start WebSocket client${NC}"
|
||||
echo ""
|
||||
echo "Check logs: $LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
stop_connection() {
|
||||
if ! is_running; then
|
||||
echo -e "${YELLOW}⚠️ Connection not running${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local pid=$(cat "$PID_FILE")
|
||||
echo "🛑 Stopping WebSocket connection (PID: $pid)..."
|
||||
|
||||
# Try graceful shutdown first
|
||||
if kill "$pid" 2>/dev/null; then
|
||||
# Wait up to 5 seconds for graceful shutdown
|
||||
for i in {1..5}; do
|
||||
if ! ps -p "$pid" &> /dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Force kill if still running
|
||||
if ps -p "$pid" &> /dev/null; then
|
||||
kill -9 "$pid" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$PID_FILE"
|
||||
echo -e " ✓ ${GREEN}WebSocket client stopped${NC}"
|
||||
}
|
||||
|
||||
show_status() {
|
||||
print_status
|
||||
|
||||
if is_running; then
|
||||
local pid=$(cat "$PID_FILE")
|
||||
echo -e "Status: ${GREEN}CONNECTED${NC} (PID: $pid)"
|
||||
|
||||
# Show recent log lines
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
echo ""
|
||||
echo "Recent activity:"
|
||||
echo "================"
|
||||
tail -n 5 "$LOG_FILE" 2>/dev/null | while IFS= read -r line; do
|
||||
echo " $line"
|
||||
done
|
||||
fi
|
||||
else
|
||||
echo -e "Status: ${RED}DISCONNECTED${NC}"
|
||||
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
echo ""
|
||||
echo "Last error (if any):"
|
||||
echo "==================="
|
||||
tail -n 3 "$LOG_FILE" 2>/dev/null | while IFS= read -r line; do
|
||||
if [[ "$line" =~ (ERROR|Error|error|FAILED|Failed|failed) ]]; then
|
||||
echo -e " ${RED}$line${NC}"
|
||||
else
|
||||
echo " $line"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check gateway tools
|
||||
echo ""
|
||||
check_gateway_tools 2>/dev/null && echo -e "Gateway tools: ${GREEN}sessions_send allowed${NC}" || true
|
||||
|
||||
echo ""
|
||||
echo "Configuration:"
|
||||
echo "============="
|
||||
local server_url=$(jq -r '.server // "https://clawdtalk.com"' "$CONFIG_FILE" 2>/dev/null)
|
||||
|
||||
echo " Server: $server_url"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo "========="
|
||||
echo " start - Start WebSocket connection"
|
||||
echo " stop - Stop WebSocket connection"
|
||||
echo " restart - Restart WebSocket connection"
|
||||
echo " status - Show this status"
|
||||
echo " watchdog - Check if running and restart if needed"
|
||||
echo ""
|
||||
echo "Flags:"
|
||||
echo " --server <url> - Override server URL"
|
||||
echo ""
|
||||
}
|
||||
|
||||
restart_connection() {
|
||||
echo "🔄 Restarting WebSocket connection..."
|
||||
stop_connection
|
||||
sleep 1
|
||||
start_connection
|
||||
}
|
||||
|
||||
watchdog_check() {
|
||||
# Silent watchdog - only log when taking action
|
||||
if ! is_running; then
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] WATCHDOG: Process not running, restarting..." >> "$SKILL_DIR/.watchdog.log"
|
||||
check_config 2>/dev/null || {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] WATCHDOG: Config check failed, skipping restart" >> "$SKILL_DIR/.watchdog.log"
|
||||
return 1
|
||||
}
|
||||
check_dependencies 2>/dev/null || {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] WATCHDOG: Dependencies check failed, skipping restart" >> "$SKILL_DIR/.watchdog.log"
|
||||
return 1
|
||||
}
|
||||
start_connection >> "$SKILL_DIR/.watchdog.log" 2>&1
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] WATCHDOG: Restart completed" >> "$SKILL_DIR/.watchdog.log"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main command handling
|
||||
case "${CMD:-}" in
|
||||
start)
|
||||
print_status
|
||||
check_config
|
||||
check_dependencies
|
||||
check_gateway_tools || true
|
||||
start_connection
|
||||
;;
|
||||
stop)
|
||||
print_status
|
||||
stop_connection
|
||||
;;
|
||||
restart)
|
||||
print_status
|
||||
check_config
|
||||
check_dependencies
|
||||
check_gateway_tools || true
|
||||
restart_connection
|
||||
;;
|
||||
status)
|
||||
check_config
|
||||
show_status
|
||||
;;
|
||||
watchdog)
|
||||
# Silent watchdog mode - used by cron
|
||||
watchdog_check
|
||||
;;
|
||||
*)
|
||||
print_status
|
||||
echo -e "${RED}❌ Invalid command${NC}"
|
||||
echo ""
|
||||
echo "Usage: $0 {start|stop|status|restart|watchdog} [--server <url>]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " start - Start WebSocket connection to ClawdTalk server"
|
||||
echo " stop - Stop WebSocket connection"
|
||||
echo " restart - Restart WebSocket connection"
|
||||
echo " status - Show connection status and configuration"
|
||||
echo " watchdog - Check if running and restart if needed (for cron)"
|
||||
echo ""
|
||||
echo "Flags:"
|
||||
echo " --server <url> - Override server URL"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,245 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# ClawdTalk SMS — Send and list SMS messages
|
||||
#
|
||||
# Usage:
|
||||
# sms.sh send +1234567890 "Hello, world!"
|
||||
# sms.sh send +1234567890 "Message" --media https://example.com/image.jpg
|
||||
# sms.sh list [--limit 20] [--contact +1234567890]
|
||||
# sms.sh conversations
|
||||
#
|
||||
# Env vars: none
|
||||
# Endpoints: https://clawdtalk.com
|
||||
# Reads: skill-config.json
|
||||
# Writes: none
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
CONFIG_FILE="$SKILL_DIR/skill-config.json"
|
||||
|
||||
# ─── Load config ────────────────────────────────────────────────────────────
|
||||
|
||||
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||
echo "Error: skill-config.json not found. Run setup.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
API_KEY=$(jq -r '.api_key // empty' "$CONFIG_FILE")
|
||||
SERVER=$(jq -r '.server // "https://clawdtalk.com"' "$CONFIG_FILE")
|
||||
|
||||
if [[ -z "$API_KEY" || "$API_KEY" == "null" ]]; then
|
||||
echo "Error: No API key configured. Add api_key to skill-config.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ─── Helper functions ───────────────────────────────────────────────────────
|
||||
|
||||
api_call() {
|
||||
local method="$1"
|
||||
local endpoint="$2"
|
||||
local data="${3:-}"
|
||||
|
||||
local url="${SERVER}${endpoint}"
|
||||
local args=(
|
||||
-s -S
|
||||
-X "$method"
|
||||
-H "Authorization: Bearer $API_KEY"
|
||||
-H "Content-Type: application/json"
|
||||
)
|
||||
|
||||
if [[ -n "$data" ]]; then
|
||||
args+=(-d "$data")
|
||||
fi
|
||||
|
||||
curl "${args[@]}" "$url"
|
||||
}
|
||||
|
||||
show_help() {
|
||||
cat << 'EOF'
|
||||
ClawdTalk SMS — Send and receive text messages
|
||||
|
||||
USAGE:
|
||||
sms.sh send <to> <message> [--media <url>]
|
||||
sms.sh list [--limit N] [--contact +1xxx]
|
||||
sms.sh conversations
|
||||
|
||||
COMMANDS:
|
||||
send Send an SMS/MMS message
|
||||
list List message history
|
||||
conversations List conversation threads
|
||||
|
||||
EXAMPLES:
|
||||
# Send a text
|
||||
sms.sh send +13125551234 "Hey, what's up?"
|
||||
|
||||
# Send with image (MMS)
|
||||
sms.sh send +13125551234 "Check this out" --media https://example.com/photo.jpg
|
||||
|
||||
# List recent messages
|
||||
sms.sh list --limit 10
|
||||
|
||||
# List messages with a specific contact
|
||||
sms.sh list --contact +13125551234
|
||||
|
||||
# Get conversation threads
|
||||
sms.sh conversations
|
||||
EOF
|
||||
}
|
||||
|
||||
# ─── Commands ───────────────────────────────────────────────────────────────
|
||||
|
||||
cmd_send() {
|
||||
local to=""
|
||||
local message=""
|
||||
local media_urls=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--media)
|
||||
media_urls+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$to" ]]; then
|
||||
to="$1"
|
||||
elif [[ -z "$message" ]]; then
|
||||
message="$1"
|
||||
else
|
||||
message="$message $1"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$to" || -z "$message" ]]; then
|
||||
echo "Usage: sms.sh send <to> <message> [--media <url>]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build JSON payload
|
||||
local payload
|
||||
if [[ ${#media_urls[@]} -gt 0 ]]; then
|
||||
local media_json
|
||||
media_json=$(printf '%s\n' "${media_urls[@]}" | jq -R . | jq -s .)
|
||||
payload=$(jq -n --arg to "$to" --arg msg "$message" --argjson media "$media_json" \
|
||||
'{to: $to, message: $msg, media_urls: $media}')
|
||||
else
|
||||
payload=$(jq -n --arg to "$to" --arg msg "$message" '{to: $to, message: $msg}')
|
||||
fi
|
||||
|
||||
local response
|
||||
response=$(api_call POST "/v1/messages/send" "$payload")
|
||||
|
||||
# Check for error
|
||||
if echo "$response" | jq -e '.error' >/dev/null 2>&1; then
|
||||
local err_msg
|
||||
err_msg=$(echo "$response" | jq -r '.error.message // .error')
|
||||
echo "Error: $err_msg" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Success output
|
||||
local msg_id from_num
|
||||
msg_id=$(echo "$response" | jq -r '.id // "unknown"')
|
||||
from_num=$(echo "$response" | jq -r '.from // "unknown"')
|
||||
|
||||
echo "✓ Message sent"
|
||||
echo " ID: $msg_id"
|
||||
echo " From: $from_num"
|
||||
echo " To: $to"
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
local limit=20
|
||||
local contact=""
|
||||
local direction=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--limit)
|
||||
limit="$2"
|
||||
shift 2
|
||||
;;
|
||||
--contact)
|
||||
contact="$2"
|
||||
shift 2
|
||||
;;
|
||||
--direction)
|
||||
direction="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
local query="?limit=$limit"
|
||||
[[ -n "$contact" ]] && query="$query&contact=$contact"
|
||||
[[ -n "$direction" ]] && query="$query&direction=$direction"
|
||||
|
||||
local response
|
||||
response=$(api_call GET "/v1/messages$query")
|
||||
|
||||
if echo "$response" | jq -e '.error' >/dev/null 2>&1; then
|
||||
echo "Error: $(echo "$response" | jq -r '.error.message // .error')" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Format output
|
||||
echo "$response" | jq -r '.messages[] | "\(.direction | if . == "inbound" then "←" else "→" end) \(.created_at | split("T")[0]) \(if .direction == "inbound" then .from else .to end): \(.body[:60])\(if (.body | length) > 60 then "..." else "" end)"'
|
||||
|
||||
local total
|
||||
total=$(echo "$response" | jq -r '.pagination.total')
|
||||
echo ""
|
||||
echo "Total: $total messages"
|
||||
}
|
||||
|
||||
cmd_conversations() {
|
||||
local response
|
||||
response=$(api_call GET "/v1/messages/conversations")
|
||||
|
||||
if echo "$response" | jq -e '.error' >/dev/null 2>&1; then
|
||||
echo "Error: $(echo "$response" | jq -r '.error.message // .error')" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$response" | jq -r '.conversations[] | "\(.contact): \(.last_message[:50])\(if (.last_message | length) > 50 then "..." else "" end)"'
|
||||
}
|
||||
|
||||
# ─── Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
main() {
|
||||
if [[ $# -eq 0 ]]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local cmd="$1"
|
||||
shift
|
||||
|
||||
case "$cmd" in
|
||||
send)
|
||||
cmd_send "$@"
|
||||
;;
|
||||
list)
|
||||
cmd_list "$@"
|
||||
;;
|
||||
conversations)
|
||||
cmd_conversations "$@"
|
||||
;;
|
||||
-h|--help|help)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: $cmd" >&2
|
||||
echo "Run 'sms.sh --help' for usage." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user