All guides

Build a Personal AI Assistant You Can Text

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

  1. Open Telegram and message @BotFather.
  2. Send /newbot and follow the prompts.
  3. 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:

bash
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

bash
mkdir -p ~/.claude/telegram/myassistant ~/.claude/logs ~/.claude/agents ~/.claude/memory

2.2 Store credentials

bash
# 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.txt

2.3 Create Scripts

receive.sh (Long Polling Logic)

bash
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.sh

send.sh (Telegram Send Logic)

bash
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.sh

loop.sh (The Brain)

bash
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.sh

Part 3: Connect Google Calendar

3.1 Install GWS CLI

bash
npm install -g @googleworkspace/cli
gws auth setup

3.2 Google Cloud Project Setup

  1. Create Project: Go to console.cloud.google.com and name it my-assistant.
  2. Enable API: Search "Google Calendar API" and hit Enable.
  3. Credentials: Create "OAuth client ID" (Desktop app) and download the JSON.
  4. 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

SymptomCauseFix
Bot starts but never respondsclaude not on PATHAdd /opt/homebrew/bin to the plist EnvironmentVariables.
Empty response from ClaudeUnicode character crashUse printf instead of bash variable assignments for prompts.
Bot responds once, then stopsOffset file not updatingCheck receive.sh logic; ensure offset.txt is being written to.
gws 403 Permission ErrorCalendar API not enabledEnable 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 --print with 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 to claude --dangerously-skip-permissions --no-session-persistence --print via 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 gws CLI. Use gws 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/.env and chat ID is in ~/.claude/telegram/myassistant/chat_id.txt.

Learn this inside the community

The full course, templates, and the people building this, free in the Skool community.

Join the community