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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
- ✅ **Multi-team collaboration** - Agents hand off work to teammates via chain execution and fan-out
- ✅ **Multi-channel** - Discord, WhatsApp, and Telegram
- ✅ **Team Observation** - You can observe agent teams conversations via `tinyclaw team visualize`
- ✅ **Multiple AI providers** - Anthropic Claude and OpenAI Codex using existing subscriptions without breaking ToS
- ✅ **Multiple AI providers** - Use multiple provider coding agents using existing subscriptions without breaking ToS
- ✅ **Parallel processing** - Agents process messages concurrently
- ✅ **Live TUI dashboard** - Real-time team visualizer for monitoring agent chains
- ✅ **Persistent sessions** - Conversation context maintained across restarts
Expand Down Expand Up @@ -80,10 +80,12 @@ The setup wizard will guide you through:
2. **Bot tokens** - Enter tokens for enabled channels
3. **Workspace setup** - Name your workspace directory
4. **Default agent** - Configure your main AI assistant
5. **AI provider** - Select Anthropic (Claude) or OpenAI
5. **AI provider** - Select Anthropic (Claude), OpenAI, or Qoder
6. **Model selection** - Choose model (e.g., Sonnet, Opus, GPT-5.3)
7. **Heartbeat interval** - Set proactive check-in frequency

Provider definitions are loaded from `config/providers.json` at startup. Add a new provider by editing that file (executable, args, output parsing, model mappings) and restart TinyClaw.

<details>
<summary><b>📱 Channel Setup Guides</b></summary>

Expand Down
12 changes: 12 additions & 0 deletions channels/discord.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"id": "discord",
"display_name": "Discord",
"alias": "dc",
"script": "dist/channels/discord-client.js",
"token": {
"settings_key": "bot_token",
"env_var": "DISCORD_BOT_TOKEN",
"prompt": "Enter your Discord bot token:",
"help": "(Get one at: https://discord.com/developers/applications)"
}
}
12 changes: 12 additions & 0 deletions channels/telegram.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"id": "telegram",
"display_name": "Telegram",
"alias": "tg",
"script": "dist/channels/telegram-client.js",
"token": {
"settings_key": "bot_token",
"env_var": "TELEGRAM_BOT_TOKEN",
"prompt": "Enter your Telegram bot token:",
"help": "(Create a bot via @BotFather on Telegram to get a token)"
}
}
6 changes: 6 additions & 0 deletions channels/tui.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"id": "tui",
"display_name": "TUI (stdin/stdout)",
"alias": "tui",
"script": "dist/channels/tui-client.js"
}
6 changes: 6 additions & 0 deletions channels/whatsapp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"id": "whatsapp",
"display_name": "WhatsApp",
"alias": "wa",
"script": "dist/channels/whatsapp-client.js"
}
53 changes: 53 additions & 0 deletions config/providers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"version": 1,
"providers": {
"anthropic": {
"display_name": "Claude",
"executable": "claude",
"args": ["--dangerously-skip-permissions", "{{?model}}--model", "{{model}}", "{{?resume}}-c", "-p", "{{message}}"],
"output": { "type": "plain" },
"models": {
"sonnet": "claude-sonnet-4-5",
"opus": "claude-opus-4-6",
"claude-sonnet-4-5": "claude-sonnet-4-5",
"claude-opus-4-6": "claude-opus-4-6"
}
},
"openai": {
"display_name": "Codex",
"executable": "codex",
"args": ["exec", "{{?resume}}resume", "{{?resume}}--last", "{{?model}}--model", "{{model}}", "--skip-git-repo-check", "--dangerously-bypass-approvals-and-sandbox", "--json", "{{message}}"],
"output": {
"type": "jsonl",
"select": {
"match": {
"type": "item.completed",
"item.type": "agent_message"
},
"field": "item.text"
}
},
"models": {
"gpt-5.2": "gpt-5.2",
"gpt-5.3-codex": "gpt-5.3-codex"
}
},
"qoder": {
"display_name": "Qoder",
"executable": "qodercli",
"args": ["--workspace", "{{cwd}}", "{{?model}}--model", "{{model}}", "{{?resume}}--continue", "--print", "{{message}}"],
"output": { "type": "plain" },
"models": {
"auto": "auto",
"efficient": "efficient",
"gmodel": "gmodel",
"kmodel": "kmodel",
"lite": "lite",
"mmodel": "mmodel",
"performance": "performance",
"qmodel": "qmodel",
"ultimate": "ultimate"
}
}
}
}
81 changes: 58 additions & 23 deletions lib/agents.sh
Original file line number Diff line number Diff line change
Expand Up @@ -129,36 +129,71 @@ agent_add() {
fi

# Provider
PROVIDERS_FILE="$SCRIPT_DIR/config/providers.json"
PROVIDER_IDS=()
PROVIDER_NAMES=()
DEFAULT_PROVIDER="anthropic"

if command -v jq &> /dev/null && [ -f "$PROVIDERS_FILE" ]; then
mapfile -t PROVIDER_IDS < <(jq -r '.providers | keys[]' "$PROVIDERS_FILE")
for pid in "${PROVIDER_IDS[@]}"; do
pname=$(jq -r --arg id "$pid" '.providers[$id].display_name // $id' "$PROVIDERS_FILE")
PROVIDER_NAMES+=("$pname")
done
for pid in "${PROVIDER_IDS[@]}"; do
if [ "$pid" = "anthropic" ]; then
DEFAULT_PROVIDER="anthropic"
break
fi
DEFAULT_PROVIDER="${PROVIDER_IDS[0]}"
done
else
PROVIDER_IDS=("anthropic" "openai" "qoder")
PROVIDER_NAMES=("Anthropic (Claude)" "OpenAI (Codex)" "Qoder")
fi

echo ""
echo "Provider:"
echo " 1) Anthropic (Claude)"
echo " 2) OpenAI (Codex)"
read -rp "Choose [1-2, default: 1]: " AGENT_PROVIDER_CHOICE
case "$AGENT_PROVIDER_CHOICE" in
2) AGENT_PROVIDER="openai" ;;
*) AGENT_PROVIDER="anthropic" ;;
esac
for i in "${!PROVIDER_IDS[@]}"; do
idx=$((i + 1))
label="${PROVIDER_NAMES[$i]} (${PROVIDER_IDS[$i]})"
if [ "${PROVIDER_IDS[$i]}" = "$DEFAULT_PROVIDER" ]; then
label="${label} (recommended)"
fi
echo " ${idx}) ${label}"
done
read -rp "Choose [1-${#PROVIDER_IDS[@]}, default: 1]: " AGENT_PROVIDER_CHOICE
if [[ "$AGENT_PROVIDER_CHOICE" =~ ^[0-9]+$ ]] && [ "$AGENT_PROVIDER_CHOICE" -ge 1 ] && [ "$AGENT_PROVIDER_CHOICE" -le "${#PROVIDER_IDS[@]}" ]; then
AGENT_PROVIDER="${PROVIDER_IDS[$((AGENT_PROVIDER_CHOICE - 1))]}"
else
AGENT_PROVIDER="${PROVIDER_IDS[0]}"
fi

# Model
echo ""
if [ "$AGENT_PROVIDER" = "anthropic" ]; then
echo "Model:"
echo " 1) Sonnet (fast)"
echo " 2) Opus (smartest)"
read -rp "Choose [1-2, default: 1]: " AGENT_MODEL_CHOICE
case "$AGENT_MODEL_CHOICE" in
2) AGENT_MODEL="opus" ;;
*) AGENT_MODEL="sonnet" ;;
esac
if command -v jq &> /dev/null && [ -f "$PROVIDERS_FILE" ]; then
mapfile -t MODEL_IDS < <(jq -r --arg id "$AGENT_PROVIDER" '.providers[$id].models // {} | keys[]' "$PROVIDERS_FILE")
else
MODEL_IDS=()
fi

if [ "${#MODEL_IDS[@]}" -eq 0 ]; then
AGENT_MODEL=""
elif [ "${#MODEL_IDS[@]}" -eq 1 ]; then
AGENT_MODEL="${MODEL_IDS[0]}"
echo "Model: ${AGENT_MODEL}"
else
echo "Model:"
echo " 1) GPT-5.3 Codex"
echo " 2) GPT-5.2"
read -rp "Choose [1-2, default: 1]: " AGENT_MODEL_CHOICE
case "$AGENT_MODEL_CHOICE" in
2) AGENT_MODEL="gpt-5.2" ;;
*) AGENT_MODEL="gpt-5.3-codex" ;;
esac
for i in "${!MODEL_IDS[@]}"; do
idx=$((i + 1))
echo " ${idx}) ${MODEL_IDS[$i]}"
done
read -rp "Choose [1-${#MODEL_IDS[@]}, default: 1]: " AGENT_MODEL_CHOICE
if [[ "$AGENT_MODEL_CHOICE" =~ ^[0-9]+$ ]] && [ "$AGENT_MODEL_CHOICE" -ge 1 ] && [ "$AGENT_MODEL_CHOICE" -le "${#MODEL_IDS[@]}" ]; then
AGENT_MODEL="${MODEL_IDS[$((AGENT_MODEL_CHOICE - 1))]}"
else
AGENT_MODEL="${MODEL_IDS[0]}"
fi
fi

# Working directory - automatically set to agent directory
Expand Down
111 changes: 83 additions & 28 deletions lib/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,33 +29,88 @@ BLUE='\033[0;34m'
NC='\033[0m'

# --- Channel registry ---
# Single source of truth. Add new channels here and everything else adapts.

ALL_CHANNELS=(discord whatsapp telegram)

declare -A CHANNEL_DISPLAY=(
[discord]="Discord"
[whatsapp]="WhatsApp"
[telegram]="Telegram"
)
declare -A CHANNEL_SCRIPT=(
[discord]="dist/channels/discord-client.js"
[whatsapp]="dist/channels/whatsapp-client.js"
[telegram]="dist/channels/telegram-client.js"
)
declare -A CHANNEL_ALIAS=(
[discord]="dc"
[whatsapp]="wa"
[telegram]="tg"
)
declare -A CHANNEL_TOKEN_KEY=(
[discord]="discord_bot_token"
[telegram]="telegram_bot_token"
)
declare -A CHANNEL_TOKEN_ENV=(
[discord]="DISCORD_BOT_TOKEN"
[telegram]="TELEGRAM_BOT_TOKEN"
)
# Source of truth is channels/*.json manifests.

CHANNELS_DIR="$SCRIPT_DIR/channels"

ALL_CHANNELS=()
declare -A CHANNEL_DISPLAY=()
declare -A CHANNEL_SCRIPT=()
declare -A CHANNEL_ALIAS=()
declare -A CHANNEL_TOKEN_KEY=()
declare -A CHANNEL_TOKEN_ENV=()
declare -A CHANNEL_TOKEN_PROMPT=()
declare -A CHANNEL_TOKEN_HELP=()

load_channel_registry() {
ALL_CHANNELS=()
CHANNEL_DISPLAY=()
CHANNEL_SCRIPT=()
CHANNEL_ALIAS=()
CHANNEL_TOKEN_KEY=()
CHANNEL_TOKEN_ENV=()
CHANNEL_TOKEN_PROMPT=()
CHANNEL_TOKEN_HELP=()

if [ ! -d "$CHANNELS_DIR" ]; then
echo -e "${RED}Channel registry not found: ${CHANNELS_DIR}${NC}"
return 1
fi

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

local files=()
while IFS= read -r f; do
files+=("$f")
done < <(find "$CHANNELS_DIR" -maxdepth 1 -type f -name '*.json' | sort)

if [ ${#files[@]} -eq 0 ]; then
echo -e "${RED}No channel manifests found in ${CHANNELS_DIR}${NC}"
return 1
fi

local file id display script alias token_key token_env token_prompt token_help
for file in "${files[@]}"; do
id=$(jq -r '.id // empty' "$file")
if [ -z "$id" ] || [ "$id" = "null" ]; then
echo -e "${YELLOW}Skipping invalid channel manifest (missing id): ${file}${NC}"
continue
fi
ALL_CHANNELS+=("$id")

display=$(jq -r '.display_name // .id // empty' "$file")
[ -n "$display" ] && CHANNEL_DISPLAY["$id"]="$display"

script=$(jq -r '.script // empty' "$file")
[ -n "$script" ] && CHANNEL_SCRIPT["$id"]="$script"

alias=$(jq -r '.alias // empty' "$file")
[ -n "$alias" ] && CHANNEL_ALIAS["$id"]="$alias"

token_key=$(jq -r '.token.settings_key // empty' "$file")
[ -n "$token_key" ] && CHANNEL_TOKEN_KEY["$id"]="$token_key"

token_env=$(jq -r '.token.env_var // empty' "$file")
[ -n "$token_env" ] && CHANNEL_TOKEN_ENV["$id"]="$token_env"

token_prompt=$(jq -r '.token.prompt // empty' "$file")
[ -n "$token_prompt" ] && CHANNEL_TOKEN_PROMPT["$id"]="$token_prompt"

token_help=$(jq -r '.token.help // empty' "$file")
[ -n "$token_help" ] && CHANNEL_TOKEN_HELP["$id"]="$token_help"
done

if [ ${#ALL_CHANNELS[@]} -eq 0 ]; then
echo -e "${RED}Channel registry loaded zero channels from ${CHANNELS_DIR}${NC}"
return 1
fi

return 0
}

# Runtime state: filled by load_settings
ACTIVE_CHANNELS=()
Expand Down Expand Up @@ -111,7 +166,7 @@ load_settings() {
for ch in "${ALL_CHANNELS[@]}"; do
local token_key="${CHANNEL_TOKEN_KEY[$ch]:-}"
if [ -n "$token_key" ]; then
CHANNEL_TOKENS[$ch]=$(jq -r ".channels.${ch}.bot_token // empty" "$SETTINGS_FILE" 2>/dev/null)
CHANNEL_TOKENS[$ch]=$(jq -r ".channels.${ch}.${token_key} // empty" "$SETTINGS_FILE" 2>/dev/null)
fi
done

Expand Down
Loading