$ open-source project · built & run in production, since retired

Personal AI Assistant: no GUI, just commands.

An always-on personal assistant I built and ran, now retired. This case records the architecture, workflows and lessons from that version: one Telegram bot with 28 commands and tiered Claude orchestration, a custom Google Workspace CLI behind one OAuth flow, and a parallel Hermes agent, all on a single $5 VM. The code lives on as one public, recruiter-readable repo.

Python Claude Haiku + Sonnet Telegram Notion API Google Workspace Oracle VM Hermes
View source on GitHub →
$ documentation
GitHub →
COMMAND ROUTING, USER FLOW User sends Telegram msg Keyword Router bot/router.py Notion API #idea, #post GWS CLI #cal, #scan, #drive Obsidian #capture, #note Claude Haiku freetext catch-all → No keyword match fell through to Claude Haiku, Tavily-grounded for factual queries
SYSTEM ARCHITECTURE, PERSONAL AI ASSISTANT assistant Python · Oracle VM systemd + cron Telegram Bot API Inbound messages Hermes parallel agent Notion 7 databases GWS CLI 5 Google APIs Obsidian Vault via rclone Tavily · GitHub grounding + status Claude API Haiku + Sonnet A hand-built keyword router and a parallel Hermes agent ran side by side on the same VM
LLM ORCHESTRATION, TIERED MODELS + TWO-CALL #apply Message command / text HAIKU, CHEAP / HIGH VOLUME Intent routing (freetext) Extraction / classify #apply · score_fit Salary script You approve? fit score first GO → SONNET, RESERVED FOR HARD GEN #apply · generate_doc email / cover / bullets Post writer anti-AI-voice Haiku handles the high-volume work. Sonnet is only spent after a cheap fit score and your explicit approval.

This page is the build record for Personal AI Assistant. It covers the context-switching problem, the router + tiered-model architecture, the two agents that ran side by side, the command surface, the daily cron automations, and the honest state of the project: it ran in production and is now retired.

Save as PDF →

Context-switching
was the tax.

$ the problem

Every personal workflow lived in a different app. Saving an idea meant opening Notion. Logging a thought meant opening Obsidian. Checking today's calendar meant opening Google Calendar. Writing a LinkedIn post meant opening a browser, a notes app, and an image tool in sequence.

The cost wasn't the tasks themselves, it was the friction of switching between six-plus apps to do anything meaningful. I wanted one interface that could reach all of them, and I wanted to build it well enough that the design decisions underneath would hold up to scrutiny.

28keyword commands
5daily cron automations
10integrated services
$5/mosingle always-on VM

This case study covers the open-source consolidation of three of my own components into one public repo: assistant (the Telegram bot), gws-cli (a custom Google Workspace CLI), and hermes (my customizations on a parallel agent runtime). View the source →

One entry point,
any system.

$ architecture

A single bot listener ran on an Oracle Cloud free-tier VM (systemd, always-on). Every Telegram message went through a keyword router in bot/router.py. Matched commands dispatched to typed handlers; unmatched messages fell through to Claude Haiku, Tavily-grounded for factual queries. Adding a new command is one import and one list entry, and the /help text is generated from the same registry so docs never drift from code.

Layered design:
bot/handlers/  → thin: parse input, call a pipeline, format reply
pipelines/     → multi-step LLM flows; model selection lives here
integrations/  → one adapter per service; the only layer doing I/O
prompts/       → prompt engineering, isolated from control flow

Google Workspace operations ran through gws-cli as a subprocess using system Python, keeping Google's client libraries out of the bot's async virtualenv. It called five Google APIs: Gmail and Calendar from the first version in April 2026, then Drive, Contacts and Tasks, added in May 2026. All async Telegram handlers wrap sync I/O with asyncio.to_thread(). Local state is tracked in a small SQLite “Memory OS” with atomic writes.

Tiered models:
spend where it counts.

$ llm orchestration

The system was deliberately model-tiered. The vast majority of calls (intent routing, extraction, classification, fit scoring) ran on Claude Haiku: cheap and fast enough to feel synchronous over chat. Claude Sonnet was reserved for the one class of artifact whose quality actually changes an outcome: a generated application document or a published post.

The #apply two-call pattern. Pasting a job description ran score_fit on Haiku, returning a JSON fit score with strengths and gaps. Only if I chose to proceed did generate_doc spend a Sonnet call to write the email, cover letter, or résumé bullets. The split was both a cost control and a human-in-the-loop gate: a premium call was never burned on a role I wasn't going to pursue.

Prompt engineering was treated as a first-class concern in prompts/: content-type-aware post generation, a forbidden-phrase list that strips the tell-tale “AI voice,” JSON-only extraction contracts, and a profile-grounded application writer.

A hand-built router
and an agent, side by side.

$ two agents

The primary bot was a deterministic keyword router: fast, predictable, cheap, and debuggable. When a command misbehaved I could read exactly one handler. But “should this be an agent framework instead?” is a real build-vs-adopt question a PM should answer with evidence, not a slide.

So I ran Hermes, the open-source agent from Nous Research, as a second, parallel agent on the same VM. Both are now retired. The repo contains only my customizations on top of it: a custom #think skill (structured Problem → Options → Recommendation output, ported from the bot), a memory template, and the systemd unit that ran the gateway. Running both let me compare an agentic runtime against hand-written handlers on the same tasks, and see per task where the agent actually won.

Why this matters: the interesting part of this project isn't that it worked. It's the product judgment underneath. What to route where, which model to spend on, where to keep a human in the loop, and what to not build.

What it did.

$ commands

Content & LinkedIn Pipeline

  • #idea, Run the idea refiner → save to the Notion Post Ideas DB
  • #post, Generate a LinkedIn draft (type picker) → Notion Posts DB
  • #mine, Mine newsletters + Obsidian notes into fresh post ideas
  • #scan, Gmail newsletter digest, relevance-scored via Claude
  • URL drop, Auto-summarise → Notion Saved Links (YouTube URLs enriched with title + channel)

Job-Application Subsystem

  • #apply <JD>, Haiku fit score → Sonnet document generator (email / cover / bullets) → salary prep
  • #jd <JD or file>, Full interview prep + GO/NO-GO (accepts .txt / .pdf / .docx)
  • #hunt, Weekly strategy + priorities
  • #followup, applications pending > 3 days

Second Brain & Productivity

  • #capture / #note / #journal / #career, life & career logging to Obsidian + Notion
  • #think, structured decision output with past decisions injected as memory
  • #task <project> <desc>, triple-write to Notion + Obsidian + Google Tasks
  • #remind · #cal [week] · #drive <query> · #gh · #search

5 daily
cron jobs.

$ automations

Beyond on-demand commands, five scheduled jobs ran automatically every day on the VM until the bot was retired.

Time (IST)JobOutput
06:30GWS auth checkTelegram alert if the OAuth token is near expiry
08:00Newsletter pipelineGmail → Obsidian → idea miner → Notion (silent)
08:30Morning briefTelegram: calendar · focus · tasks · GitHub · news
09:00Content digestTelegram: newsletters + RSS picks, relevance-scored
17:00Evening postLinkedIn post generation from the idea queue

Key technical
choices.

$ build decisions
01

Keyword router over an agent framework

The primary bot was deterministic and cheap by design; the agentic approach ran in parallel as Hermes so the tradeoff was measured, not assumed.

02

Tiered models, gated spend

Haiku for volume, Sonnet only for the artifact that changes an outcome, and never before a cheap fit score and an explicit human GO.

03

GWS CLI as a subprocess, not a library

One OAuth flow covered five Google APIs (Gmail, Calendar, Drive, Contacts, Tasks), keeping auth complexity fully contained and Google's client libraries out of the bot's async process.

04

Anthropic SDK is sync: never await it

Async handlers wrap Claude calls in asyncio.to_thread(). This was a silent failure mode early on; after that the rule was enforced throughout.

Shipped, retired,
and open source.

$ status

retired (source public)

All 28 commands and the 5 cron automations ran unattended on the Oracle VM. The bot is now retired, and it became the brief for life-os, a personal operating-system app in build. The consolidated, sanitized template is public at github.com/argaur/personal-ai-assistant. Secrets and personal data are git-ignored, example profiles ship instead, and the test suite passes from a clean clone.