Skip to content

evilvic/theo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Theo

Personal Claude assistant on WhatsApp. Single Node.js process that routes messages to Claude Agent SDK running in Apple Container (Linux VMs). Each chat gets an isolated filesystem, persistent memory, and scheduled tasks.

Built from scratch as a learning exercise. ~600 lines of code. See COMPARISON.md for a detailed comparison with NanoClaw, nanobot, and OpenClaw.

Architecture

WhatsApp ──→ SQLite ──→ Polling Loop ──→ Apple Container ──→ Claude Agent SDK
   ↑                                           │
   └───────────── IPC (filesystem) ────────────┘

Host (Node.js): Connects to WhatsApp, stores messages in SQLite, spawns containers per request.

Container (Linux VM): Runs Claude with full tool access (Bash, Read, Write, Edit, WebSearch, etc.) plus custom MCP tools for messaging and scheduling. Destroyed after each invocation.

IPC: Bidirectional communication via atomic file writes in mounted directories. Host writes follow-up messages to inbox; agent writes commands (send_message, schedule_task) to outbox.

Structure

theo/
├── src/
│   ├── index.ts              # Orchestrator: message loop, follow-up handling
│   ├── whatsapp.ts           # WhatsApp connection (baileys)
│   ├── container-runner.ts   # Spawns agent containers with mounts
│   ├── db.ts                 # SQLite: messages, groups, sessions, tasks
│   ├── ipc.ts                # Host-side IPC watcher (agent → host)
│   └── task-scheduler.ts     # Cron/interval/one-time task execution
├── container/
│   ├── Dockerfile
│   ├── entrypoint.sh         # DNS fix + drop to non-root user
│   └── runner/src/
│       ├── index.ts          # Claude Agent SDK runner
│       └── ipc-mcp-stdio.ts  # MCP server: send_message, schedule_task, check_new_messages
├── groups/{name}/            # Per-chat isolated workspace (created at runtime)
│   └── CLAUDE.md             # Persistent memory per chat
├── .data/                    # Runtime data (created at runtime)
│   ├── ipc/{group}/          # IPC directories (messages, tasks, inbox)
│   └── sessions/{group}/     # Claude session transcripts
├── auth/                     # WhatsApp auth state (created on first connect)
└── .env                      # ANTHROPIC_API_KEY

Prerequisites

  • macOS 26+ with Apple Silicon
  • Apple Container installed and running (container system start)
  • Node.js 22+
  • Anthropic API key

Setup

# Install dependencies
npm install
cd container/runner && npm install && cd ../..

# Add your API key
echo 'ANTHROPIC_API_KEY=sk-ant-...' > .env

# Build the runner (compiled on host, copied into container)
cd container/runner && npx tsc && cd ../..

# Build the container image
container build -t theo:latest container/

# Start
npm run dev

Scan the QR code with WhatsApp on first run. Send @theo hello from any chat to auto-register it and start talking.

How it works

  1. Trigger: Messages starting with @theo activate the agent
  2. Container: Each invocation spawns a fresh Linux VM with the chat's workspace mounted at /workspace
  3. Agent: Claude runs with full tool access, reads/writes files in the workspace, can search the web
  4. Memory: CLAUDE.md in each group folder persists across conversations. Session transcripts enable Claude to remember prior context via resume
  5. Follow-ups: Messages sent while the agent is working get queued. After the agent finishes, queued messages trigger a follow-up invocation with session resume
  6. Tasks: The agent can schedule recurring or one-time tasks via MCP tools. A scheduler on the host executes them automatically

MCP Tools (available to the agent)

Tool Description
send_message Send a WhatsApp message to the current chat
schedule_task Schedule a task: cron ("09:00", "mon 09:00"), interval (ms), or once (ISO timestamp)
list_tasks List scheduled tasks for this chat
check_new_messages Check if the user sent follow-up messages while working

Container isolation

Each invocation gets:

  • /workspace — the chat's own directory (read-write)
  • /home/node/.claude — session transcripts (persistent)
  • /workspace/ipc — IPC communication directory
  • Non-root user (node)
  • Ephemeral (--rm) — destroyed after completion
  • DNS override to 8.8.8.8 (Apple Container 0.9.0 workaround)

Known quirks

  • Builder has no network: TypeScript is compiled on the host and copied into the container. npm install also runs on the host
  • Container DNS broken: Apple Container 0.9.0's gateway DNS forwarder doesn't work. entrypoint.sh overrides /etc/resolv.conf at runtime
  • SDK exits with code 1: Claude Agent SDK sometimes throws after yielding a result. The runner captures the result before the error and uses it anyway
  • Builder cache: Apple Container caches aggressively. To force a clean rebuild: container builder stop && container builder rm && container builder start

About

Personal Claude assistant on WhatsApp. Container-isolated agents, persistent memory, scheduled tasks. ~600 lines of TypeScript.

Topics

Resources

License

Stars

Watchers

Forks

Contributors