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.
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 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.
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.
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.
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.
$ commandsContent & 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) | 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 |
Key technical
choices.
$ build decisions
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.
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.
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.
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.