A personal AI operating system I designed, built, and run 24/7 on a single $5 VM. One Telegram bot with 28 commands and tiered Claude orchestration, a custom Google Workspace CLI behind one OAuth flow, and a parallel Hermes agent — consolidated into one public, recruiter-readable repo.
View source on GitHub →This page is the build record for Personal AI Assistant. It covers the context-switching problem, the router + tiered-model architecture, the two agents running side by side, the command surface, the daily cron automations, and the honest state of what's shipped and running in production.
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.
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 →
A single bot listener runs on an Oracle Cloud free-tier VM (systemd, always-on). Every
Telegram message goes through a keyword router in bot/router.py. Matched
commands dispatch to typed handlers; unmatched messages fall 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.
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 (Gmail, Calendar, Drive, Tasks, Contacts) run through
gws-cli as a subprocess using system Python — keeping Google's client
libraries out of the bot's async virtualenv. 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.
The system is deliberately model-tiered. The vast majority of calls — intent routing, extraction, classification, fit scoring — run on Claude Haiku: cheap and fast enough to feel synchronous over chat. Claude Sonnet is 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. Paste a job description and
score_fit runs on Haiku, returning a JSON fit score with strengths and gaps.
Only if I choose to proceed does generate_doc spend a Sonnet call to write the
email, cover letter, or résumé bullets. The split is simultaneously a cost
control and a human-in-the-loop gate — a premium call is never burned on a role I
wasn't going to pursue.
Prompt engineering is 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.
The primary bot is a deterministic keyword router: fast, predictable, cheap, and debuggable — when a command misbehaves I can 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 run Hermes — the open-source agent from Nous Research —
as a second, parallel agent on the same VM. 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 runs the gateway. Running both lets me compare an agentic runtime against
hand-written handlers on the same tasks, and see per-task where the agent actually wins.
Why this matters: the interesting part of this project isn't that it works — 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.
#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#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#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 · #searchBeyond on-demand commands, five scheduled jobs run automatically every day on the VM.
| Time (IST) | Job | Output |
|---|---|---|
| 06:30 | GWS auth check | Telegram alert if the OAuth token is near expiry |
| 08:00 | Newsletter pipeline | Gmail → Obsidian → idea miner → Notion (silent) |
| 08:30 | Morning brief | Telegram: calendar · focus · tasks · GitHub · news |
| 09:00 | Content digest | Telegram: newsletters + RSS picks, relevance-scored |
| 17:00 | Evening post | LinkedIn post generation from the idea queue |
The primary bot is deterministic and cheap by design; the agentic approach runs in parallel as Hermes so the tradeoff is measured, not assumed.
Haiku for volume, Sonnet only for the artifact that changes an outcome — and never before a cheap fit score and an explicit human GO.
One OAuth flow covers five Google APIs, keeping auth complexity fully contained and Google's client libraries out of the bot's async process.
Async handlers wrap Claude calls in asyncio.to_thread(). This was a silent
failure mode early on; the rule is now enforced throughout.
active (running)
All 28 commands and the 5 cron automations are in daily use, running unattended on the Oracle VM. 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. The private, live version keeps running untouched.