$ open-source project · built & run in production

Personal AI Assistant — no GUI, just commands.

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.

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 falls 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 run 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 running side by side, the command surface, the daily cron automations, and the honest state of what's shipped and running in production.

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 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.

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 (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.

Tiered models —
spend where it counts.

$ llm orchestration

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.

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

$ two agents

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.

What it does.

$ 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 run automatically every day on the VM.

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 is deterministic and cheap by design; the agentic approach runs in parallel as Hermes so the tradeoff is 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 covers five Google APIs, 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; the rule is now enforced throughout.

Shipped, running,
and open source.

$ status

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.