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
12 changes: 11 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ jobs:
run: |
VERSION="${{ steps.get_version.outputs.version }}"
BUNDLE_NAME="tinyclaw-bundle.tar.gz"
CHECKSUM_NAME="tinyclaw-bundle.sha256"
TEMP_DIR=$(mktemp -d)
BUNDLE_DIR="$TEMP_DIR/tinyclaw"

Expand Down Expand Up @@ -89,11 +90,17 @@ jobs:
# Create tarball
cd "$TEMP_DIR"
tar -czf "$GITHUB_WORKSPACE/$BUNDLE_NAME" tinyclaw/
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$GITHUB_WORKSPACE/$BUNDLE_NAME" | awk '{print $1}' > "$GITHUB_WORKSPACE/$CHECKSUM_NAME"
else
sha256sum "$GITHUB_WORKSPACE/$BUNDLE_NAME" | awk '{print $1}' > "$GITHUB_WORKSPACE/$CHECKSUM_NAME"
fi

# Get bundle info
BUNDLE_SIZE=$(du -h "$GITHUB_WORKSPACE/$BUNDLE_NAME" | cut -f1)
echo "Bundle created: $BUNDLE_NAME ($BUNDLE_SIZE)"
echo "bundle_name=$BUNDLE_NAME" >> $GITHUB_OUTPUT
echo "checksum_name=$CHECKSUM_NAME" >> $GITHUB_OUTPUT
echo "bundle_size=$BUNDLE_SIZE" >> $GITHUB_OUTPUT

- name: Generate release notes
Expand Down Expand Up @@ -138,6 +145,7 @@ jobs:
body_path: release_notes.md
files: |
tinyclaw-bundle.tar.gz
tinyclaw-bundle.sha256
draft: false
prerelease: false
env:
Expand All @@ -148,5 +156,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: tinyclaw-bundle
path: tinyclaw-bundle.tar.gz
path: |
tinyclaw-bundle.tar.gz
tinyclaw-bundle.sha256
retention-days: 7
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Run multiple teams of AI agents that collaborate with each other simultaneously
- Node.js v14+
- tmux
- Bash 4.0+ (macOS: `brew install bash`)
- jq (`brew install jq` or `apt install jq`)
- [Claude Code CLI](https://claude.com/claude-code) (for Anthropic provider)
- [Codex CLI](https://docs.openai.com/codex) (for OpenAI provider)

Expand Down
7 changes: 6 additions & 1 deletion docs/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ curl -fsSL https://raw.githubusercontent.com/jlia0/tinyclaw/main/scripts/remote-
```

This one-line command:
- ✅ Checks all dependencies (node, npm, tmux, claude)
- ✅ Checks all dependencies (node, npm, tmux, claude, jq)
- ✅ Downloads pre-built bundle (no npm install needed!)
- ✅ Installs to `~/.tinyclaw`
- ✅ Creates global `tinyclaw` command
Expand All @@ -28,6 +28,7 @@ Before installing, ensure you have:
- **npm** (comes with Node.js)
- **tmux** - `sudo apt install tmux` or `brew install tmux`
- **Claude Code CLI** ([claude.com/claude-code](https://claude.com/claude-code))
- **jq** - `sudo apt install jq` or `brew install jq`

**Optional:**
- **git** (only needed for source install)
Expand Down Expand Up @@ -191,6 +192,10 @@ brew install tmux # macOS

# Claude Code
# Visit: https://claude.com/claude-code

# jq
sudo apt install jq # Ubuntu/Debian
brew install jq # macOS
```

### Bundle download fails
Expand Down
17 changes: 8 additions & 9 deletions lib/daemon.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,22 @@ start_daemon() {
PUPPETEER_SKIP_DOWNLOAD=true npm install
fi

# Build TypeScript if any src file is newer than its dist counterpart
# Build TypeScript if source changed since last build stamp.
local needs_build=false
if [ ! -d "$SCRIPT_DIR/dist" ]; then
local build_stamp="$SCRIPT_DIR/dist/.build-stamp"
if [ ! -d "$SCRIPT_DIR/dist" ] || [ ! -f "$build_stamp" ]; then
needs_build=true
else
for ts_file in "$SCRIPT_DIR"/src/*.ts; do
local js_file="$SCRIPT_DIR/dist/$(basename "${ts_file%.ts}.js")"
if [ ! -f "$js_file" ] || [ "$ts_file" -nt "$js_file" ]; then
needs_build=true
break
fi
done
if find "$SCRIPT_DIR/src" -type f \( -name "*.ts" -o -name "*.tsx" \) -newer "$build_stamp" | grep -q .; then
needs_build=true
fi
fi
if [ "$needs_build" = true ]; then
echo -e "${YELLOW}Building TypeScript...${NC}"
cd "$SCRIPT_DIR"
npm run build
mkdir -p "$SCRIPT_DIR/dist"
touch "$build_stamp"
fi

# Load settings or run setup wizard
Expand Down
7 changes: 6 additions & 1 deletion lib/messaging.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ send_message() {
log "[$source] Sending: ${message:0:50}..."

cd "$SCRIPT_DIR"
RESPONSE=$(claude --dangerously-skip-permissions -c -p "$message" 2>&1)
local claude_cmd=(claude -c -p "$message")
if [ "${TINYCLAW_ALLOW_DANGEROUS_FLAGS:-0}" = "1" ]; then
claude_cmd=(claude --dangerously-skip-permissions -c -p "$message")
log "[$source] WARNING: dangerous permissions flag enabled via TINYCLAW_ALLOW_DANGEROUS_FLAGS=1"
fi
RESPONSE=$("${claude_cmd[@]}" 2>&1)

echo "$RESPONSE"

Expand Down
188 changes: 137 additions & 51 deletions lib/setup-wizard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,31 @@ echo -e "${GREEN} TinyClaw - Setup Wizard${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""

if ! command -v jq &> /dev/null; then
echo -e "${RED}Error: jq is required for setup.${NC}"
echo "Install with: brew install jq (macOS) or apt-get install jq (Linux)"
exit 1
fi

csv_to_json_array() {
local csv="$1"
local values=()
IFS=',' read -ra raw_values <<< "$csv"
for raw in "${raw_values[@]}"; do
local value
value="$(echo "$raw" | tr -d '[:space:]')"
if [ -n "$value" ]; then
values+=("$value")
fi
done

if [ ${#values[@]} -eq 0 ]; then
jq -n '[]'
else
jq -n --args "${values[@]}" '$ARGS.positional'
fi
}

# --- Channel registry ---
# To add a new channel, add its ID here and fill in the config arrays below.
ALL_CHANNELS=(telegram discord whatsapp)
Expand Down Expand Up @@ -184,13 +209,19 @@ echo "Users route messages with '@agent_id message' in chat."
echo ""
read -rp "Set up additional agents? [y/N]: " SETUP_AGENTS

AGENTS_JSON=""
# Always create the default agent
DEFAULT_AGENT_DIR="$WORKSPACE_PATH/$DEFAULT_AGENT_NAME"
# Capitalize first letter of agent name (proper bash method)
DEFAULT_AGENT_DISPLAY="$(tr '[:lower:]' '[:upper:]' <<< "${DEFAULT_AGENT_NAME:0:1}")${DEFAULT_AGENT_NAME:1}"
AGENTS_JSON='"agents": {'
AGENTS_JSON="$AGENTS_JSON \"$DEFAULT_AGENT_NAME\": { \"name\": \"$DEFAULT_AGENT_DISPLAY\", \"provider\": \"$PROVIDER\", \"model\": \"$MODEL\", \"working_directory\": \"$DEFAULT_AGENT_DIR\" }"
declare -a ALL_AGENT_IDS=("$DEFAULT_AGENT_NAME")
declare -A AGENT_NAME_MAP=()
declare -A AGENT_PROVIDER_MAP=()
declare -A AGENT_MODEL_MAP=()
declare -A AGENT_DIR_MAP=()

AGENT_NAME_MAP["$DEFAULT_AGENT_NAME"]="$DEFAULT_AGENT_DISPLAY"
AGENT_PROVIDER_MAP["$DEFAULT_AGENT_NAME"]="$PROVIDER"
AGENT_MODEL_MAP["$DEFAULT_AGENT_NAME"]="$MODEL"
AGENT_DIR_MAP["$DEFAULT_AGENT_NAME"]="$DEFAULT_AGENT_DIR"

ADDITIONAL_AGENTS=() # Track additional agent IDs for directory creation

Expand Down Expand Up @@ -240,8 +271,11 @@ if [[ "$SETUP_AGENTS" =~ ^[yY] ]]; then
fi

NEW_AGENT_DIR="$WORKSPACE_PATH/$NEW_AGENT_ID"

AGENTS_JSON="$AGENTS_JSON, \"$NEW_AGENT_ID\": { \"name\": \"$NEW_AGENT_NAME\", \"provider\": \"$NEW_PROVIDER\", \"model\": \"$NEW_MODEL\", \"working_directory\": \"$NEW_AGENT_DIR\" }"
ALL_AGENT_IDS+=("$NEW_AGENT_ID")
AGENT_NAME_MAP["$NEW_AGENT_ID"]="$NEW_AGENT_NAME"
AGENT_PROVIDER_MAP["$NEW_AGENT_ID"]="$NEW_PROVIDER"
AGENT_MODEL_MAP["$NEW_AGENT_ID"]="$NEW_MODEL"
AGENT_DIR_MAP["$NEW_AGENT_ID"]="$NEW_AGENT_DIR"

# Track this agent for directory creation later
ADDITIONAL_AGENTS+=("$NEW_AGENT_ID")
Expand All @@ -250,59 +284,111 @@ if [[ "$SETUP_AGENTS" =~ ^[yY] ]]; then
done
fi

AGENTS_JSON="$AGENTS_JSON },"
# Security defaults
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN} Security Defaults${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo "To reduce abuse risk, TinyClaw enforces per-channel sender allowlists by default."
echo "Add trusted sender IDs now (comma-separated), or leave blank to block that channel until configured."
echo ""

# Build enabled channels array JSON
CHANNELS_JSON="["
for i in "${!ENABLED_CHANNELS[@]}"; do
if [ $i -gt 0 ]; then
CHANNELS_JSON="${CHANNELS_JSON}, "
fi
CHANNELS_JSON="${CHANNELS_JSON}\"${ENABLED_CHANNELS[$i]}\""
declare -A ALLOWED_SENDERS_JSON=()
for ch in "${ENABLED_CHANNELS[@]}"; do
read -rp "Trusted sender IDs for ${CHANNEL_DISPLAY[$ch]}: " sender_ids
ALLOWED_SENDERS_JSON["$ch"]="$(csv_to_json_array "$sender_ids")"
done
CHANNELS_JSON="${CHANNELS_JSON}]"

# Build channel configs with tokens
DISCORD_TOKEN="${TOKENS[discord]:-}"
TELEGRAM_TOKEN="${TOKENS[telegram]:-}"
echo ""
read -rp "Allow dangerous agent permission-bypass flags? [y/N]: " ALLOW_DANGEROUS_INPUT
if [[ "$ALLOW_DANGEROUS_INPUT" =~ ^[yY] ]]; then
ALLOW_DANGEROUS_FLAGS=true
else
ALLOW_DANGEROUS_FLAGS=false
fi

# Write settings.json with layered structure
# Use jq to build valid JSON to avoid escaping issues with agent prompts
if [ "$PROVIDER" = "anthropic" ]; then
MODELS_SECTION='"models": { "provider": "anthropic", "anthropic": { "model": "'"${MODEL}"'" } }'
read -rp "Persist full team chat transcripts to disk? [y/N]: " PERSIST_CHATS_INPUT
if [[ "$PERSIST_CHATS_INPUT" =~ ^[yY] ]]; then
PERSIST_TEAM_CHATS=true
else
MODELS_SECTION='"models": { "provider": "openai", "openai": { "model": "'"${MODEL}"'" } }'
PERSIST_TEAM_CHATS=false
fi
echo ""

cat > "$SETTINGS_FILE" <<EOF
{
"workspace": {
"path": "${WORKSPACE_PATH}",
"name": "${WORKSPACE_NAME}"
},
"channels": {
"enabled": ${CHANNELS_JSON},
"discord": {
"bot_token": "${DISCORD_TOKEN}"
},
"telegram": {
"bot_token": "${TELEGRAM_TOKEN}"
},
"whatsapp": {}
},
${AGENTS_JSON}
${MODELS_SECTION},
"monitoring": {
"heartbeat_interval": ${HEARTBEAT_INTERVAL}
}
}
EOF
# Build enabled channels and agents JSON safely via jq
CHANNELS_ENABLED_JSON="$(jq -n --args "${ENABLED_CHANNELS[@]}" '$ARGS.positional')"
AGENTS_OBJECT='{}'
for agent_id in "${ALL_AGENT_IDS[@]}"; do
AGENTS_OBJECT="$(jq -c \
--argjson current "$AGENTS_OBJECT" \
--arg id "$agent_id" \
--arg name "${AGENT_NAME_MAP[$agent_id]}" \
--arg provider "${AGENT_PROVIDER_MAP[$agent_id]}" \
--arg model "${AGENT_MODEL_MAP[$agent_id]}" \
--arg workdir "${AGENT_DIR_MAP[$agent_id]}" \
'$current + {($id): {name: $name, provider: $provider, model: $model, working_directory: $workdir}}' \
)"
done

# Normalize JSON with jq (fix any formatting issues)
if command -v jq &> /dev/null; then
tmp_file="$SETTINGS_FILE.tmp"
jq '.' "$SETTINGS_FILE" > "$tmp_file" 2>/dev/null && mv "$tmp_file" "$SETTINGS_FILE"
fi
# Build channel configs with tokens
DISCORD_TOKEN="${TOKENS[discord]:-}"
TELEGRAM_TOKEN="${TOKENS[telegram]:-}"

# Build allowed_senders object (all channels present for consistency).
ALLOWLIST_DISCORD="${ALLOWED_SENDERS_JSON[discord]:-[]}"
ALLOWLIST_TELEGRAM="${ALLOWED_SENDERS_JSON[telegram]:-[]}"
ALLOWLIST_WHATSAPP="${ALLOWED_SENDERS_JSON[whatsapp]:-[]}"

mkdir -p "$(dirname "$SETTINGS_FILE")"

jq -n \
--arg workspacePath "$WORKSPACE_PATH" \
--arg workspaceName "$WORKSPACE_NAME" \
--argjson channelsEnabled "$CHANNELS_ENABLED_JSON" \
--arg discordToken "$DISCORD_TOKEN" \
--arg telegramToken "$TELEGRAM_TOKEN" \
--argjson agents "$AGENTS_OBJECT" \
--arg provider "$PROVIDER" \
--arg model "$MODEL" \
--argjson heartbeatInterval "$HEARTBEAT_INTERVAL" \
--argjson allowlistDiscord "$ALLOWLIST_DISCORD" \
--argjson allowlistTelegram "$ALLOWLIST_TELEGRAM" \
--argjson allowlistWhatsapp "$ALLOWLIST_WHATSAPP" \
--argjson allowDangerous "$ALLOW_DANGEROUS_FLAGS" \
--argjson persistTeamChats "$PERSIST_TEAM_CHATS" \
'{
workspace: {
path: $workspacePath,
name: $workspaceName
},
channels: {
enabled: $channelsEnabled,
discord: { bot_token: $discordToken },
telegram: { bot_token: $telegramToken },
whatsapp: {}
},
agents: $agents,
models: (
if $provider == "anthropic"
then { provider: "anthropic", anthropic: { model: $model } }
else { provider: "openai", openai: { model: $model } }
end
),
monitoring: {
heartbeat_interval: $heartbeatInterval
},
security: {
require_sender_allowlist: true,
allowed_senders: {
discord: $allowlistDiscord,
telegram: $allowlistTelegram,
whatsapp: $allowlistWhatsapp
},
allow_dangerous_agent_flags: $allowDangerous,
allow_outbound_file_paths_outside_files_dir: false,
persist_team_chats: $persistTeamChats
}
}' > "$SETTINGS_FILE"

# Create workspace directory
mkdir -p "$WORKSPACE_PATH"
Expand Down
Loading