Agentero
Connect your own Agent, keep research context in a local Vault. Seamlessly migrate from Zotero, read, understand, link, and reuse your research context.
AI translation
Write or speak one line about your day. Memo sorts it, pulls out the numbers and builds charts, goals and a weekly retro. Works in Telegram and on the web.
ProductivityGitHub @andriihukcoOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Repository information has not been collected yet.
No updates detected yet. GitHub releases will appear here.
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗Memo is a Telegram bot + mini-app that acts as an intelligent personal diary. You send it text or voice messages about anything — food, workouts, expenses, feelings, thoughts — and it automatically classifies, stores, and analyzes everything. A built-in Telegram Mini App gives you a dashboard, knowledge graph, and retrospective reports.
Bot side (Telegram):
/report, /report daily, /report monthly)Mini App side (Telegram WebApp):
Telegram User
│
▼
Telegram Bot API
│ webhook POST
▼
/api/telegram/webhook (Edge runtime, grammY)
│
├─ classify() ← Gemini 2.5 Flash (intent + metrics extraction)
├─ handleTextMessage()
│ ├─ answerQuestion() ← RAG: embed → pgvector search → Gemini answer
│ ├─ handleAction() ← delete entries / create widget
│ └─ generateConverseReply() ← Gemini conversational reply
│
├─ handleVoiceMessage()
│ └─ classifyAudio() ← Gemini multimodal (transcribe + classify)
│
└─ embedEntry() [async, non-blocking]
├─ generateEmbedding() ← gemini-embedding-001 (768-dim)
├─ findSimilarEntries() ← pgvector cosine similarity RPC
├─ generateInsight() ← Gemini insight from similar entries
└─ generateConversationalReply() ← pattern-aware reply
Supabase (PostgreSQL + pgvector)
├─ profiles
├─ entries (+ vector index HNSW)
├─ insights
├─ categories
├─ reports
├─ reminders
└─ todos
Vercel Cron Jobs
├─ /api/cron/process (daily 00:00 UTC)
│ ├─ clusterEntries() ← pgvector union-find clustering
│ ├─ buildAndSaveWidgets() ← Gemini widget summaries
│ └─ autoIncrementStreaks()
└─ /api/cron/reports (daily 09:00 UTC)
Telegram Mini App
└─ Next.js pages under /miniapp/*
├─ /miniapp → Feed
├─ /miniapp/dashboard → Dashboard
├─ /miniapp/graph → Knowledge Graph
├─ /miniapp/reports → Retrospectives
└─ /miniapp/settings → Passcode / Privacy
| Layer | Technology |
|---|---|
| Framework | Next.js 14 (App Router) |
| Language | TypeScript |
| Bot library | grammY |
| AI / LLM | Google Gemini 2.5 Flash (gemini-2.5-flash) |
| Embeddings | Google gemini-embedding-001 (768 dimensions) |
| Database | Supabase (PostgreSQL) |
| Vector search | pgvector (HNSW index, cosine similarity) |
| Auth | Supabase Auth (synthetic email/password per Telegram user) |
| Frontend | React 18, Tailwind CSS, shadcn/ui, Radix UI |
| Graph visualization | D3.js (force simulation) |
| Validation | Zod |
| Deployment | Vercel (Edge + Node.js runtimes) |
| Cron | Vercel Cron Jobs |
src/
├── app/
│ ├── api/
│ │ ├── auth/telegram/route.ts # Telegram initData verification → Supabase JWT
│ │ ├── categories/route.ts # User categories CRUD
│ │ ├── entries/route.ts # Entries GET / PATCH / DELETE
│ │ ├── graph/route.ts # Knowledge graph payload
│ │ ├── reports/route.ts # Retrospective reports CRUD + generation
│ │ ├── cron/
│ │ │ ├── process/route.ts # Daily processing cron
│ │ │ └── reports/route.ts # Daily report cron
│ │ └── telegram/webhook/route.ts # Telegram bot webhook
│ └── miniapp/
│ ├── layout.tsx # Auth + tab bar + passcode lock
│ ├── page.tsx # Feed (entry list)
│ ├── dashboard/page.tsx # Metrics dashboard
│ ├── graph/page.tsx # D3 knowledge graph
│ ├── reports/page.tsx # Retrospective reports
│ └── settings/page.tsx # Passcode settings
├── components/ui/ # shadcn/ui components + custom
│ ├── edit-drawer.tsx # Slide-up entry editor
│ ├── lock-button.tsx # Manual lock trigger
│ └── passcode-screen.tsx # 4-digit PIN screen
└── lib/
├── bot/
│ ├── commands.ts # /start /help /report handlers
│ ├── conversational.ts # Pattern-aware reply (with similar entries)
│ ├── converse.ts # General conversational reply (tone learning)
│ ├── qa.ts # RAG question answering
│ ├── retrospective.ts # Report generation + DB persistence
│ └── handlers/
│ ├── action.ts # Action commands (delete, create widget)
│ ├── text.ts # Text message handler (main flow)
│ └── voice.ts # Voice message handler
├── processing/
│ ├── loop.ts # Clustering + streak auto-increment
│ └── widgets.ts # Dashboard widget generation
├── classifier.ts # Gemini classification (intent + metrics)
├── embedding.ts # Embedding generation + RAG pipeline trigger
├── env.ts # Zod-validated env vars (lazy proxy)
├── insight.ts # Insight generation + pgvector search
├── passcode.ts # PIN hash + lock timer (localStorage)
├── profile.ts # Supabase profile resolve/create
├── supabase/
│ ├── auth-context.tsx # React auth context (access token)
│ └── client.ts # Supabase browser client
└── utils.ts # cn(), sanitizeMarkdown()
supabase/migrations/ # Ordered SQL migrations
scripts/
└── set-webhook.ts # One-time webhook registration script
profilesStores one row per Telegram user. The id is the Supabase Auth UUID so RLS (auth.uid()) works for both bot and mini app.
| Column | Type | Notes |
|---|---|---|
id | UUID PK | = Supabase Auth user UUID |
telegram_id | BIGINT UNIQUE | Telegram user ID |
username | TEXT | Telegram username |
settings | JSONB | Dashboard widgets, report schedule, pending deletes, etc. |
created_at / updated_at | TIMESTAMPTZ |
entriesCore diary entries. Every message that isn't a question, action, or smalltalk becomes an entry.
| Column | Type | Notes |
|---|---|---|
id | UUID PK | |
user_id | UUID FK → profiles | |
content | TEXT | Cleaned entry text |
category | TEXT | Open-ended snake_case (thoughts, workout, sleep, etc.) |
metadata | JSONB | Structured data: dashboard_metrics[], goal_metrics[], expense amount/currency, food calories, bot_msg_id, etc. |
raw_media_url | TEXT | Reserved for future media attachments |
embedding | vector(768) | Gemini embedding for semantic search |
embedding_status | TEXT | pending / done / failed |
branch_id | UUID | Cluster ID assigned by the processing loop |
thread_id | UUID | Groups a conversation thread |
bot_reply | TEXT | Bot's reply stored alongside the entry |
reply_to_entry_id | UUID FK → entries | Thread parent link |
created_at / updated_at | TIMESTAMPTZ |
Indexes: user_id, category, branch_id, thread_id, reply_to_entry_id, HNSW vector index on embedding.
insightsAI-generated insights linking a new entry to semantically similar past entries.
| Column | Type | Notes |
|---|---|---|
id | UUID PK | |
user_id | UUID FK → profiles | |
entry_id | UUID FK → entries | The entry that triggered this insight |
insight_text | TEXT | Generated insight text |
branch_id | UUID | Cluster this insight belongs to |
created_at | TIMESTAMPTZ |
categoriesPer-user category registry. Built-in categories are seeded on first use; new categories are created dynamically by the AI.
| Column | Type | Notes |
|---|---|---|
id | UUID PK | |
user_id | UUID FK → profiles | |
name | TEXT | snake_case key (e.g. sleep, dreams) |
label_ua | TEXT | Human-readable label in user's language |
color | TEXT | Tailwind class string |
icon | TEXT | Lucide icon name |
reportsAI-generated agile retrospective reports.
| Column | Type | Notes |
|---|---|---|
id | UUID PK | |
user_id | UUID FK → profiles | |
period_type | TEXT | daily / weekly / monthly / custom |
period_from / period_to | TIMESTAMPTZ | Report date range |
content | TEXT | Full Telegram Markdown report |
summary | TEXT | 1-2 sentence TLDR |
went_well | TEXT | Retro section: what went well |
didnt_go_well | TEXT | Retro section: what didn't work |
start_stop_continue | TEXT | Retro section: actions |
experiment | TEXT | Retro section: next experiment |
lesson | TEXT | Retro section: main lesson |
insights | JSONB | Structured insight objects array |
reminders / todosStandard reminder and todo tables with RLS. Used for future bot features.
find_similar_entriesfind_similar_entries(p_user_id, p_embedding, p_exclude_id, p_top_k)
→ TABLE(id, content, category, created_at, similarity)
Returns top-K entries ordered by cosine similarity (1 - (embedding <=> p_embedding)).
src/lib/classifier.ts)Every incoming message goes through classify() (text) or classifyAudio() (voice). Uses Gemini 2.5 Flash with a detailed system prompt that returns a strict JSON schema validated by Zod.
Output fields:
intent: save_entry | question | converse | smalltalk | actioncategory: open-ended snake_case stringis_new_category: whether to create a new category rowcontent: cleaned textmetadata: structured data (expense amount, food calories, etc.)dashboard_metrics[]: all measurable quantities extracted and derived (e.g. 5km run → distance_km, kcal_burned, active_min, steps_count)goal_metrics[]: stated goals/targets (e.g. "want to run 100km this month")action_type + action_params: for action intentsThe classifier applies decomposition intelligence — from a single input like "ran 5km" it derives 4+ metrics using built-in formulas (running: 1km ≈ 80 kcal, 6 min, 1200 steps). Nutrition entries get full macro breakdowns.
src/lib/embedding.ts)After saving an entry, embedEntry() runs asynchronously (non-blocking):
gemini-embedding-001 to generate a 768-dimensional vectorentries.embedding with embedding_status = 'done'findSimilarEntries() via pgvector RPCsrc/lib/bot/qa.ts)RAG pipeline for intent = "question":
gemini-embedding-001src/lib/bot/converse.ts)For all saved entries, a natural reply is generated using:
src/lib/bot/retrospective.ts)Generates agile-style retrospectives with 5 structured sections:
Uses all entries in the period with their metrics. Output is structured JSON with both a full Telegram Markdown report and individual section fields for the mini app UI.
Entry point: src/app/api/telegram/webhook/route.ts (Edge runtime)
The bot uses grammY with a middleware that resolves/creates the user profile before any handler runs.
Commands:
/start — welcome message with feature overview/help — full command reference/report [daily|weekly|monthly] — generate retrospective with rotating status messagesMessage flow (text):
question → RAG answer (shows "thinking" message, edits it with answer)smalltalk → conversational reply, no saveaction → execute (delete entries / create widget), no savesave_entry / converse → resolve thread → save entry → generate reply → embed asyncMessage flow (voice):
classifyAudio() — Gemini multimodal transcription + classification in one callThread system:
bot_msg_id in entry.metadatareply_to_message.message_idbot_msg_id to find the threadThe mini app is a Next.js app served under /miniapp/*. It runs inside Telegram as a WebApp.
Layout (src/app/miniapp/layout.tsx):
window.Telegram.WebApp.ready() and .expand()/api/auth/telegram using initDataFeed (/miniapp):
Dashboard (/miniapp/dashboard):
dashboard_metrics from all entries in rangekcal_intake and kcal_burned existgoal_metrics targetsGraph (/miniapp/graph):
/api/graphReports (/miniapp/reports):
Settings (/miniapp/settings):
localStorageAll mini app routes require Authorization: Bearer <supabase_jwt>.
| Method | Path | Runtime | Description |
|---|---|---|---|
| POST | /api/telegram/webhook | Edge | grammY webhook handler |
| POST | /api/auth/telegram | Edge | Verify Telegram initData → return Supabase JWT |
| GET | /api/entries | Edge | List entries (pagination, category, date range filters) |
| PATCH | /api/entries | Edge | Update entry content/category (re-computes metrics via Gemini) |
| DELETE | /api/entries | Edge | Bulk delete by IDs |
| GET | /api/graph | Edge | Knowledge graph nodes + edges |
| GET | /api/reports | Node.js | List reports |
| POST | /api/reports | Node.js | Generate new report (60s timeout) |
| DELETE | /api/reports | Node.js | Delete report |
| GET | /api/categories | Edge | List user categories |
| GET | /api/cron/process | Node.js | Daily processing (cron, requires CRON_SECRET) |
| GET | /api/cron/reports | Node.js | Daily report delivery (cron, requires CRON_SECRET) |
Daily cron at 00:00 UTC — /api/cron/process:
clusterEntries(userId) — For each entry with an embedding, calls find_similar_entries RPC to find neighbors (similarity > 0.75). Builds a union-find graph and assigns shared branch_id UUIDs to clusters of ≥ 3 entries. Updates both entries.branch_id and insights.branch_id.
buildAndSaveWidgets(userId) — Generates dashboard widget configs and saves them to profiles.settings.dashboard_widgets:
calories_today — sum of today's calorie entriesexpenses_today — sum by currencymood_trend — Gemini analysis of feeling entries (improving / stable / declining)life_theme — per-cluster theme label generated by Geminidaily_summary / weekly_summary / monthly_summary — narrative summariesinsight_cluster — summarized insight clustersautoIncrementStreaks(userId) — Finds entries from yesterday with aggregate=last streak metrics (keys ending in _days). If no entry exists today for that category, creates a new entry with value+1.
Daily cron at 09:00 UTC — /api/cron/reports:
Delivers scheduled reports to users who have enabled auto-reports in profiles.settings.report_schedule.
The system uses a synthetic Supabase Auth account per Telegram user:
Bot side: resolveOrCreateProfile() in src/lib/profile.ts creates a Supabase Auth user with email telegram_<id>@memo.app and a deterministic password. The profile id is set to the Supabase Auth UUID so auth.uid() in RLS policies matches entries.user_id.
Mini App side: /api/auth/telegram verifies the Telegram initData HMAC-SHA256 signature (with 24-hour expiry check), then signs in with the same synthetic credentials to get a real Supabase session JWT. This JWT is stored in React context and sent as Authorization: Bearer on all API calls.
RLS: All tables have Row Level Security enabled. Policies use auth.uid() to ensure users can only access their own data.
# Telegram
TELEGRAM_BOT_TOKEN= # Bot token from @BotFather
# Supabase
SUPABASE_URL= # https://<project>.supabase.co
SUPABASE_SERVICE_ROLE_KEY= # Service role key (server-side only)
NEXT_PUBLIC_SUPABASE_ANON_KEY= # Anon key (used in browser/edge with user JWT)
# Google AI
GEMINI_API_KEY= # Google AI Studio API key
# Cron security
CRON_SECRET= # Secret for /api/cron/* routes
All server-side vars are validated at request time via a Zod schema in src/lib/env.ts (lazy proxy — fails fast on first use if missing).
The project is deployed on Vercel.
Connect your GitHub repo to Vercel. Framework preset: Next.js.
In Vercel project settings → Environment Variables, add all vars from the section above.
Vercel auto-deploys on push. The build runs next build.
After first deploy, run the webhook registration script once:
TELEGRAM_BOT_TOKEN=<token> WEBHOOK_URL=https://<your-domain>/api/telegram/webhook npx ts-node scripts/set-webhook.ts
Or call the Telegram API directly:
https://api.telegram.org/bot<TOKEN>/setWebhook?url=https://<your-domain>/api/telegram/webhook
vector extension in the SQL editor: CREATE EXTENSION IF NOT EXISTS vector;supabase/migrations/In @BotFather:
/newapp or edit existing bothttps://<your-domain>/miniappDefined in vercel.json:
{
"crons": [
{ "path": "/api/cron/process", "schedule": "0 0 * * *" },
{ "path": "/api/cron/reports", "schedule": "0 9 * * *" }
]
}
Vercel automatically calls these with Authorization: Bearer <CRON_SECRET> (set in env vars).
# Install dependencies
npm install
# Copy env file and fill in values
cp .env.example .env.local
# Run dev server
npm run dev
For local bot testing, use ngrok to expose localhost and register the webhook:
ngrok http 3000
# Then set webhook to https://<ngrok-id>.ngrok.io/api/telegram/webhook
The mini app requires Telegram context (window.Telegram.WebApp.initData) to authenticate. For local UI development you can mock the auth context in src/lib/supabase/auth-context.tsx.
# Lint
npm run lint
# Build
npm run build