Skip to main content
Glama

Apollo

A single-user, local-first personal agent platform.

Declare goals. Apollo decomposes them into practices, projects, habits and metrics, a scheduler drives check-ins and reviews, and agents use skills and MCP tools to do real work — while keeping you accountable.

CI Python License: MIT Pydantic AI MCP Local-first

Features & use cases · Architecture · Quick start · Configuration · CLI


Why Apollo

Most productivity tools give you a place to put tasks. Apollo gives you a loop: evidence feeds reviews, reviews rewrite practice, and agents do the upkeep.

  • Ontology, not tags. A Goal is what you want; a Practice is what you run to get it. Projects, tasks, habits and metrics hang off those two — so the system knows why something matters, not just that it exists.

  • Agents with a job. A cheap supervisor routes to specialists (triage, planner, coach, researcher) that capture, decompose, review and research. Every run is typed, logged and replayable.

  • A scheduler that acts. Time, event and condition rules drive briefings, reviews, drift detection and overdue sweeps.

  • Durable memory. Your journal, check-ins and reviews are embedded and recalled semantically, behind a swappable Memory facade.

  • Local-first. Your database, vault and vector store stay on your machine. The only outbound traffic is your model/embeddings endpoint, Telegram, and MCP servers you choose to add.

  • Bidirectional MCP. Apollo consumes external MCP servers as tools, and exposes itself to Claude Desktop, Kilo and Cursor as an MCP server.

Related MCP server: Frontispice

Features

Area

What you get

Domain

Areas, Goals, Practices, Projects, Tasks, Habits, Metrics, Check-ins, Reviews, Journal — with a classification test that keeps the schema honest

Agents

Supervisor → one specialist (triage / planner / coach / researcher); no fan-out; capture and planning are tool-free single calls with slim typed schemas; per-run RunLog; streaming replies with Stop

Tools

db_tools read/write layer shared by agents, the MCP server and the CLI; coach/researcher get read+write+memory+notify capabilities; write tools are approval-gated by tier

Skills

Markdown-first (SKILL.md) with progressive disclosure, hot reload and optional Python tools.py gated by allowed-tools; loaded on demand by coach/researcher, skipped by the fast capture/plan paths

Memory

Mem0 over a local Chroma server, API embeddings with a configurable fallback chain, worker-only writes, lazily constructed (no cost unless used)

Scheduling

Cron, event subscriptions and safe registered condition predicates; cooldowns, timezone-aware, quiet hours

Telegram

Native Bot API 10.3: streaming drafts, rich HTML, inline keyboards, approvals, polls, reactions, deep links, inline mode

MCP

Config-driven client registry (default-deny allowlists, namespacing) and a curated stdio server with approval-aware writes

Safety

Read free · mutate auto+audited · irreversible requires approval; single-user allowlist; path-sandboxed vault; no shell

Ops

apollo doctor, migrations, pause/resume kill switch, job queue inspection, structured logs, opt-in OTel/Logfire tracing

Quality

Ruff, Pyright, 135 tests, pydantic-evals suites, CI on every push

Architecture

Split processes, one SQLite database as the source of truth, and a transactional outbox for every side effect.

                 ┌──────────────────────────────────────────────┐
                 │  SQLite (WAL)   data/apollo.db               │
                 │  domain · events(outbox) · jobs              │
                 │  pending_actions · notifications · audit     │
                 │  vault_fts (FTS5 index of markdown vault)    │
                 └──────────────────────────────────────────────┘
                   ▲         ▲          ▲          ▲         ▲
        writes     │         │          │          │         │
   ┌───────────────┴──┐ ┌────┴─────┐ ┌──┴───────┐ ┌┴─────────┴───┐
   │ apollo telegram  │ │apollo-   │ │apollo-   │ │ apollo-mcp  │
   │ long polling     │ │worker    │ │dispatcher│ │ stdio (per  │
   │ in→events/jobs   │ │agents    │ │tick →    │ │ MCP client) │
   │ out←notifications│ │skills    │ │jobs      │ │ read tools +│
   └──────────────────┘ │memory    │ │retries   │ │ approval-   │
                        └────┬─────┘ └──────────┘ │ aware writes│
                             │                    └─────────────┘
                     ┌───────┴────────┐     ┌──────────────────┐
                     │ Chroma (server)│     │ MCP client       │
                     │ localhost:8000 │     │ registry →       │
                     └────────────────┘     │ calendar/web/…   │
                                            └──────────────────┘

Process

Command

Responsibility

Telegram bot

apollo telegram

Inbound messages → events/jobs; drains notifications; renders approvals

Worker

apollo worker

Claims jobs, runs agents/skills, owns Mem0 writes

Dispatcher

apollo dispatcher

Scheduler tick, event drain, condition predicates, retries

MCP server

apollo mcp

stdio server for Claude Desktop / Kilo / Cursor

CLI

apollo …

Admin, migrations, inspection, one-off runs

Concurrency discipline. WAL + busy_timeout, every write in a short BEGIN IMMEDIATE transaction, atomic job claim (UPDATE … RETURNING), leases with heartbeat, exponential backoff to dead-letter, and a DB advisory lock enforcing a single dispatcher. Side effects are never performed inside a transaction — domain mutation and its outbox event commit together.

Request flow

One message becomes at most two model calls. There is no parallel fan-out: the supervisor picks a specialist and only that specialist runs.

Telegram text
   │
   ├─ capture phrasing ("remind me…", "add…", "log…")  ──► triage      (1 call)
   │        tool-free · slim CaptureResult · parent ids injected
   │
   └─ anything else ──► supervisor (1 call) ──► one of:
                            triage    tool-free · slim CaptureResult
                            planner   tool-free · slim nested PlanResult
                            coach     tools + memory + skills
                            researcher tools + memory + skills + MCP toolsets
   │
   ▼  typed output
apply (capture/plan)  or  tools (coach/researcher)   ← writes happen here, with events
   │
   ├─ events → outbox reactions (memory.add, notify.send, approval.resume)
   └─ notifications → Telegram drainer → sendMessage (Markdown → safe HTML)

Specialist

Tools

Skills

Output

supervisor

none

–

RouteDecision

triage

none (ids + current time injected)

–

CaptureResult (task / checkin / metric / note / reminder)

planner

none (existing goal/task ids injected)

–

PlanResult (nested goal→practice→projects→tasks)

coach

read, write, memory.recall, notify

summaries + on-demand load_skill

CoachResult

researcher

read, memory.recall, notify, approval + MCP toolsets

summaries + load_skill

ResearchResult

Tools are real and still central — db_tools is what the MCP server exposes to Claude Desktop/Kilo/Cursor, and coach/researcher use it as agent tools. Triage and planner are deliberately tool-free fast paths: a tool-call loop cost 120 s timeouts and duplicated writes, so they emit one typed object instead. Skills remain fully functional for coach/researcher (progressive disclosure: only summaries are in the prompt until load_skill is called).

Performance

Measured on the maintainer's setup (OpenRouter deepseek/deepseek-v4-flash-0731, local Chroma), message → final Telegram reply:

Path

Avg

Range

Notes

Capture (“remind me to …”)

3.4 s

2.5–4.9 s

1 model call, ~2.2 s run, rest is claim + drain + send

Question / chat

~+1 call

—

supervisor routing adds one model call (~1.5–6 s, provider-bound)

Goal / replan

~55 s

—

one larger call (~1.2k output tokens) + one retry

Iterations that got there: 14 s → 7.9 s (tool-free agents + lazy memory + faster poll/drain) → 3.4 s (slim capture schema). Reproduce with:

scripts/run.sh start
uv run python scripts/bench_capture.py --count 4

The big levers, in order: number of tool-call round trips, output schema size, eager service construction (Mem0), and poll/drain intervals. A single provider trip measures ~1.3 s and varies; provider latency is the floor.

Quick start

# 1. Install (Python 3.11+; 3.12 recommended)
uv sync --extra memory          # add --extra voice, --extra local-embeddings as needed

# 2. Configure
cp .env.example .env && chmod 600 .env   # provider key, model ids, Telegram token
$EDITOR apollo.toml                       # timezone, tiers, autonomy, MCP servers

# 3. Verify everything is reachable
uv run apollo doctor

# 4. Start the whole stack (Chroma + dispatcher + worker + Telegram)
scripts/run.sh start

That's it — scripts/run.sh is the single entrypoint. It installs dependencies if needed, creates the database, starts Chroma and waits for it to be healthy, then brings up the dispatcher, worker and Telegram bot, and reports status.

scripts/run.sh              # start everything (idempotent)
scripts/run.sh status       # ●/○ for each component
scripts/run.sh logs worker  # tail a component log
scripts/run.sh stop         # stop everything
scripts/run.sh restart

Send the bot a message and talk normally: "remind me to call the bank tomorrow", "log a 6 for sleep", "what should I focus on today?"

Why Chroma runs as a server (not embedded)

Chroma's embedded mode (PersistentClient(path=…)) takes an exclusive file lock and is single-process only. Apollo is deliberately split across processes that all touch memory — worker (writes), dispatcher, and mcp/agents (reads) — so an embedded store can't be shared. Chroma therefore runs as a local server on loopback, and Mem0 connects over HTTP.

That is still local-first: the vector store never leaves your machine. scripts/run.sh starts it for you with uv run chroma run --path ./data/chroma --port 8000 (version-matched to uv.lock); no container runtime is required. See docs/FEATURES.md for details.

Configuration

Non-secret settings live in apollo.toml; secrets live in .env (0600, git-ignored).

[app]
timezone = "America/New_York"

[provider]
tiers = { triage = "…", plan = "…", coach = "…", research = "…" }

[autonomy]
default_tier = "mutate"
irreversible = ["vault.delete", "web.send", "calendar.delete_event"]

[telegram]
topic_routing = false        # single chat by default; topics are optional
quiet_hours = { start = "22:00", end = "07:00" }

[memory]
enabled = true
chroma_url = "http://localhost:8000"
APOLLO_PROVIDER__BASE_URL=https://openrouter.ai/api/v1
APOLLO_PROVIDER__API_KEY=…
APOLLO_PROVIDER__CHAT_MODEL_ID=deepseek/deepseek-v4-flash-0731
APOLLO_PROVIDER__LIGHT_MODEL_ID=openai/gpt-oss-20b
APOLLO_PROVIDER__EMBEDDING_MODEL_ID=openai/text-embedding-3-small
APOLLO_TELEGRAM__BOT_TOKEN=…
APOLLO_TELEGRAM__ALLOWED_USER_IDS=[123456789]

Any OpenAI-compatible endpoint works. CHAT_MODEL_ID overrides all tiers; LIGHT_MODEL_ID handles the simple schemas only (supervisor routing, memory extraction) — capture and planning stay on the chat model because the large command union is unreliable on tiny models. EMBEDDING_MODEL_ID overrides [embeddings].model. If a provider serves no embeddings, Apollo falls back to a second provider or local fastembed.

A typical capture (message → final Telegram reply) lands in ~2–5 s end to end; planning (goal decomposition) is heavier at ~30–60 s. Both are tool-free single-call agents with slim schemas, and the supervisor is skipped for obvious captures. Measure with uv run python scripts/bench_capture.py --count 4.

Telegram setup

Apollo ships in single-chat mode: natural language is routed by the supervisor, so you just talk. Private-chat topics are optional (nine-topic information architecture) — enable them in @BotFather and set topic_routing = true.

In @BotFather:

  • /mybots → your bot → Bot Settings → Topics in Private Chats (optional)

  • /setinline — enable inline mode (share your day from any chat)

  • /setdescription, /setabouttext, /setuserpic — profile polish

Commands and the menu button are configured automatically at startup. allowed_updates is set to message, edited_message, callback_query, poll_answer, inline_query, stopped_message_generation.

MCP

Consume external servers (default-deny — only allowlisted tools reach an agent, namespaced):

[mcp.servers.calendar]
transport = "stdio"
command = ["npx", "-y", "@modelcontextprotocol/server-google-calendar"]
allowlist = ["list_events", "create_event"]
namespace = "calendar"

Expose Apollo to any MCP client:

{ "mcpServers": { "apollo": { "command": "uv", "args": ["run", "apollo", "mcp"] } } }

Reads are free; writes return applied or pending_approval, and skills are exposed as MCP prompts. See docs/FEATURES.md for the full tool list.

Skills

A skill is one markdown file with YAML frontmatter:

---
name: weekly-review
description: Summarize practices, surface drift, propose adjustments.
triggers: ["schedule:weekly_review", "user:review", "event:review.due"]
allowed-tools: ["db.read", "db.write", "memory.recall", "notify.telegram"]
model-tier: coach
---
<instructions / methodology / output format>

The registry loads frontmatter only, keeping prompts small; full instructions load on demand. Coach and researcher see the skill summaries and can call load_skill; the tool-free capture/plan paths skip skills for speed. Optional skills/<slug>/tools.py adds Python tools gated by allowed-tools. Built-ins: capture, daily-briefing, evening-checkin, weekly-review, goal-decompose, drift-detect, deep-research.

Scheduling

Built-in routines: morning briefing 07:00 · evening check-in 21:00 · weekly review Sun 18:00 · drift check daily · overdue sweep hourly · vault sync every 5 minutes. Add your own with cron, event, or safe registered condition predicates (no_checkin_days, metric_below_target, habit_streak_broken, goal_no_progress_days, overdue_tasks).

One-off reminders are supported: “remind me to drink water in 30 minutes” creates a reminder.fire job scheduled at that time, which sends an urgent notification when it fires.

Quiet hours buffer proactive notifications until the window ends, but never a reply: an answer to a message you just sent is transactional and goes out immediately, even at 2am.

Privacy & security

  • Everything local: SQLite, markdown vault and Chroma on loopback; single-user Telegram allowlist rejects everyone else.

  • Autonomy tiers: read free · mutate auto-applied, audited and notified · irreversible requires approval and auto-denies on expiry.

  • Secrets are never logged; .env, data/ and editor configs that may embed keys are git-ignored.

  • MCP and web output is treated as untrusted data; the trust boundary is stated to every agent. No arbitrary shell execution in v1.

See SECURITY.md for the threat model and reporting process.

CLI reference

apollo version | doctor | init-db | upgrade | schema
apollo pause | resume | status
apollo worker [--once] | dispatcher [--once] | telegram | mcp
apollo run --agent <triage|planner|coach|researcher> "text"
apollo queue list|retry|replay|purge
apollo schedule list|toggle
apollo vault sync|search
apollo memory recall|reindex
apollo mcp-servers list|check
apollo topics list|reset

Development

uv run ruff check .     # lint
uv run pyright          # types
uv run pytest           # unit + integration (98 tests)
uv run pytest -m eval   # evaluations (live model)

CI runs lint, types and tests on every push. Contributions are welcome — see CONTRIBUTING.md.

Project layout

apollo/
  apollo.toml  .env.example  pyproject.toml  alembic.ini
  assets/                    logo & brand assets
  docs/                      features & use cases
  scripts/                   run.sh (one-command stack), launchd/systemd templates
  skills/                    built-in skills (markdown + optional tools.py)
  src/apollo/
    domain/  db/  queue/  agents/  skills/  tools/
    mcp/  memory/  scheduler/  telegram/  observability/
  tests/                     unit · integration · evals · fakes

License

MIT — see LICENSE.

Related MCP Connectors

Related MCP Servers

  • F
    license
    A
    quality
    B
    maintenance
    MCP server for Personal OS API that enables managing tasks, notes, projects, collections, reviews, and more through typed tools over stdio.
    66
    -
  • A
    license
    A
    quality
    B
    maintenance
    MCP server for local-first agent handoff: enables AI sessions to publish curated context, list/read updates, route handoffs to specialists, and advance per-consumer cursors, with SQLite persistence and no shell or network access.
    8
    Apache 2.0
  • A
    license
    A
    quality
    C
    maintenance
    Provides local persistent working memory for AI agents, allowing them to load rules, preferences, and project state before tasks and save validated experiences after successful completion. Works with stdio MCP clients via a local JSON file.
    5
    MIT