This guide walks you through building an AI assistant that lives on Telegram, has a persistent personality and memory, and can fully manage your Google Calendar. Text it from your phone like you would a real EA.
If you're too lazy and want it built for you SKIP TO THE END where I've included the high-detail one-shot prompt that will get your Claude agent to build it for you perfectly!
Why this beats every other setup
When most people connect an AI to their calendar, they use an MCP server. That's useful for coding sessions, but it's not a personal assistant. This is different.
- Runs 24/7: Hosted in the background on your Mac.
- Persistent Memory: It remembers your preferences and past decisions.
- Phone-First: Accessible via Telegram from anywhere.
- Direct Control: Calls Google Calendar directly via CLI (no extra running processes).
The Stack
- Claude Code CLI (
claude) - Telegram Bot API (Free)
@googleworkspace/cli(gws) — npm package for Google APIs- macOS launchd — Keeps the bot running after restarts
- Bash scripts — The glue
Part 1: Create your Telegram bot
1.1 Get a bot token from BotFather
- Open Telegram and message @BotFather.
- Send
/newbotand follow the prompts. - Save the bot token (looks like
712...:AAH...).
1.2 Get your Telegram Chat ID
Start a conversation with your bot (send it "Hello"), then run:
curl -s "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates" | python3 -c "
import sys, json
data = json.load(sys.stdin)
for u in data['result']:
msg = u.get('message', {})
print('chat_id:', msg.get('chat', {}).get('id'))
print('text:', msg.get('text'))
"[!IMPORTANT] Note your chat ID. This locks the bot to only respond to you. If that fails, message @userinfobot to get your ID.
Part 2: Set up the bot scripts
2.1 Create the directory structure
mkdir -p ~/.claude/telegram/myassistant ~/.claude/logs ~/.claude/agents ~/.claude/memory2.2 Store credentials
# Store Token
echo "MYASSISTANT_BOT_TOKEN=\"YOUR_BOT_TOKEN_HERE\"" > ~/.claude/telegram/.env
# Store Chat ID
echo "YOUR_CHAT_ID_HERE" > ~/.claude/telegram/myassistant/chat_id.txt
# Initialize offset
echo "0" > ~/.claude/telegram/myassistant/offset.txt2.3 Create Scripts
receive.sh (Long Polling Logic)
cat > ~/.claude/telegram/myassistant/receive.sh << 'SCRIPT'
#!/bin/bash
source "$HOME/.claude/telegram/.env"
TOKEN="$MYASSISTANT_BOT_TOKEN"
OFFSET_FILE="$HOME/.claude/telegram/myassistant/offset.txt"
CHAT_ID_FILE="$HOME/.claude/telegram/myassistant/chat_id.txt"
ALLOWED_CHAT_ID=$(cat "$CHAT_ID_FILE")
OFFSET=$(cat "$OFFSET_FILE")
RESPONSE=$(curl -s --max-time 35 "https://api.telegram.org/bot${TOKEN}/getUpdates?offset=${OFFSET}&limit=10&timeout=30")
echo "$RESPONSE" | node -e "
const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8'));
const allowedId = ${ALLOWED_CHAT_ID};
if (data.ok && data.result.length > 0) {
let maxId = parseInt(require('fs').readFileSync('${OFFSET_FILE}', 'utf8').trim()) || 0;
data.result.forEach(u => {
if (u.update_id >= maxId) maxId = u.update_id + 1;
if (u.message && u.message.text && u.message.chat.id === allowedId) {
console.log(u.message.text);
}
});
require('fs').writeFileSync('${OFFSET_FILE}', String(maxId));
}
"
SCRIPT
chmod +x ~/.claude/telegram/myassistant/receive.shsend.sh (Telegram Send Logic)
cat > ~/.claude/telegram/myassistant/send.sh << 'SCRIPT'
#!/bin/bash
source "$HOME/.claude/telegram/.env"
TOKEN="$MYASSISTANT_BOT_TOKEN"
CHAT_ID=$(cat "$HOME/.claude/telegram/myassistant/chat_id.txt")
MESSAGE="$1"
curl -s "https://api.telegram.org/bot${TOKEN}/sendMessage" \
-d "chat_id=${CHAT_ID}" \
--data-urlencode "text=${MESSAGE}" > /dev/null
SCRIPT
chmod +x ~/.claude/telegram/myassistant/send.shloop.sh (The Brain)
cat > ~/.claude/telegram/myassistant/loop.sh << 'SCRIPT'
#!/bin/bash
BASE="$HOME/.claude/telegram/myassistant"
LOG="$HOME/.claude/logs/telegram-myassistant.log"
HISTORY="$BASE/history.txt"
source "$HOME/.claude/telegram/.env"
TOKEN="$MYASSISTANT_BOT_TOKEN"
CHAT_ID=$(cat "$BASE/chat_id.txt")
MAX_HISTORY=20
touch "$HISTORY"
collect_memory() {
if [ -f "$HOME/.claude/memory/myassistant.md" ]; then
echo "=== Assistant Memory ==="; cat "$HOME/.claude/memory/myassistant.md"; echo ""
fi
}
send_typing() {
curl -s "https://api.telegram.org/bot${TOKEN}/sendChatAction" -d "chat_id=${CHAT_ID}" -d "action=typing" > /dev/null
}
while true; do
MESSAGES=$(bash "$BASE/receive.sh" 2>/dev/null)
if [ -n "$MESSAGES" ]; then
send_typing
PERSONA=$(cat "$HOME/.claude/agents/myassistant.md")
ALL_MEMORY=$(collect_memory)
HISTORY_CONTENT=$(tail -n $((MAX_HISTORY * 2)) "$HISTORY")
TODAY=$(date '+%A, %d %B %Y %H:%M %Z')
PROMPT_FILE=$(mktemp /tmp/assistant_prompt.XXXXXX)
{
printf '%s\n\n' "$PERSONA"
printf 'Current date/time: %s\n\n' "$TODAY"
printf '## Memory\n%s\n\n' "$ALL_MEMORY"
printf '## Recent Conversation\n%s\n\n' "$HISTORY_CONTENT"
printf -- '---\nUser says via Telegram: %s\n' "$MESSAGES"
} > "$PROMPT_FILE"
RESPONSE=$(claude --dangerously-skip-permissions --no-session-persistence --print < "$PROMPT_FILE" 2>>"$LOG")
rm -f "$PROMPT_FILE"
if [ -n "$RESPONSE" ]; then
bash "$BASE/send.sh" "$RESPONSE"
echo "User: $MESSAGES" >> "$HISTORY"
echo "Assistant: $RESPONSE" >> "$HISTORY"
fi
fi
done
SCRIPT
chmod +x ~/.claude/telegram/myassistant/loop.shPart 3: Connect Google Calendar
3.1 Install GWS CLI
npm install -g @googleworkspace/cli
gws auth setup3.2 Google Cloud Project Setup
- Create Project: Go to console.cloud.google.com and name it
my-assistant. - Enable API: Search "Google Calendar API" and hit Enable.
- Credentials: Create "OAuth client ID" (Desktop app) and download the JSON.
- Publish: Go to "OAuth consent screen" and click Publish App.
[!WARNING] Crucial: Publishing to Production makes the token last indefinitely. In "Testing" mode, it expires every 7 days, killing your bot.
Common Snags & Fixes
| Symptom | Cause | Fix |
|---|---|---|
| Bot starts but never responds | claude not on PATH | Add /opt/homebrew/bin to the plist EnvironmentVariables. |
| Empty response from Claude | Unicode character crash | Use printf instead of bash variable assignments for prompts. |
| Bot responds once, then stops | Offset file not updating | Check receive.sh logic; ensure offset.txt is being written to. |
| gws 403 Permission Error | Calendar API not enabled | Enable API in GCP and re-run gws auth setup. |
The One-Shot Setup Prompt
Paste this into a Claude Code session. It includes all specific technical constraints to ensure a successful build.
Build me a personal AI assistant Telegram bot using Claude Code CLI on my Mac.
What it does: Polls Telegram for messages from me, passes them to
claude --printwith a custom agent persona and memory, sends the response back. Runs 24/7 via launchd.Assistant details: - Name: [YOUR ASSISTANT NAME] - Personality: [DESCRIBE THE TONE] - My context: [YOUR DETAILS]
Files to create: -
~/.claude/telegram/myassistant/receive.sh— uses long polling:curl -s --max-time 35 "...getUpdates?offset=${OFFSET}&limit=10&timeout=30". Filters by chat ID, updates offset.txt. -~/.claude/telegram/myassistant/send.sh— sends text via sendMessage API. -~/.claude/telegram/myassistant/loop.sh— main loop: no sleep (receive.sh blocks). On message: read persona/memory, build prompt into a temp file using printf (NOT a bash string assignment—this fails in launchd), pass toclaude --dangerously-skip-permissions --no-session-persistence --printvia stdin. -~/.claude/agents/myassistant.md— persona file. -~/Library/LaunchAgents/com.claude.telegram.myassistant.plist— launchd plist with KeepAlive true, PATH including /opt/homebrew/bin.Google Calendar: Set up
gwsCLI. Usegws calendar events list/insert/patch/delete/freebusy. Permission rules: read/create immediately, always confirm before modifying or deleting existing events.Critical implementation note: In launchd, bash runs with LANG=C. Do NOT assign the prompt as a multi-line double-quoted bash variable. Always use printf to write to a temp file, then pipe the file to claude via stdin.
My bot token is in
~/.claude/telegram/.envand chat ID is in~/.claude/telegram/myassistant/chat_id.txt.