chore: project scaffolding
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Skill(superpowers:writing-plans)",
|
||||
"Skill(superpowers:subagent-driven-development)",
|
||||
"Bash(npm init *)",
|
||||
"Bash(npm install *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
TELEGRAM_TOKEN=your_telegram_bot_token
|
||||
TELEGRAM_CHAT_ID=your_numeric_chat_id
|
||||
OPENROUTER_API_KEY=your_openrouter_key
|
||||
DB_PATH=./data/assistant.db
|
||||
MORNING_REMINDER=05:30
|
||||
EVENING_REMINDER=16:30
|
||||
DEFAULT_MODEL=google/gemini-2.5-flash-preview
|
||||
TZ=Europe/Amsterdam
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
data/
|
||||
.env
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,235 @@
|
||||
# Training Assistant — Design Spec
|
||||
|
||||
**Date:** 2026-05-20
|
||||
**Status:** Approved
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
A personal Telegram bot that tracks anal training sessions, sends reminders, and uses an AI coach (via OpenRouter) to adaptively manage the training schedule and provide progress feedback.
|
||||
|
||||
Built with Node.js/TypeScript, Telegraf, SQLite, and node-cron. Designed so a web dashboard (Approach C) can be added later as a read-only layer on the same SQLite file.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
A single TypeScript process with four internal components:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Telegram Bot (Telegraf) │
|
||||
│ - command routing │
|
||||
│ - message handling │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
┌───────┼───────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌──────┐ ┌─────────────┐
|
||||
│ Session │ │ Cron │ │ AI Coach │
|
||||
│ Logger │ │ Jobs │ │ (OpenRouter)│
|
||||
└────┬─────┘ └──┬───┘ └──────┬──────┘
|
||||
│ │ │
|
||||
└──────────┴─────────────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ SQLite │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
**Components:**
|
||||
|
||||
- **Bot handler** — Telegraf v4, routes commands and free-text messages
|
||||
- **Session logger** — writes training sessions to SQLite, validates input
|
||||
- **Cron jobs** — `node-cron`, sends reminders at configured times
|
||||
- **AI coach** — OpenRouter via OpenAI-compatible client; reads history from DB, generates coaching and schedule proposals
|
||||
- **SQLite** — `better-sqlite3`, single file, easy to back up
|
||||
- **Config** — `.env` for Telegram token, OpenRouter API key, user chat ID, reminder times, preferred model
|
||||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
|
||||
### `sessions`
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | INTEGER PRIMARY KEY | |
|
||||
| `date` | TEXT | YYYY-MM-DD |
|
||||
| `duration_min` | INTEGER | |
|
||||
| `notes` | TEXT | Optional free text |
|
||||
| `created_at` | TEXT | ISO timestamp |
|
||||
|
||||
### `schedule`
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | INTEGER PRIMARY KEY | |
|
||||
| `date` | TEXT | YYYY-MM-DD |
|
||||
| `planned_min` | INTEGER | |
|
||||
| `status` | TEXT | `pending` \| `completed` \| `skipped` |
|
||||
| `skip_reason` | TEXT | Optional |
|
||||
| `ai_rationale` | TEXT | Why the AI scheduled this |
|
||||
| `created_at` | TEXT | ISO timestamp |
|
||||
|
||||
### `settings`
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `key` | TEXT PRIMARY KEY | |
|
||||
| `value` | TEXT | |
|
||||
|
||||
Used for: reminder times, preferred OpenRouter model, training goal, reminder enabled flag.
|
||||
|
||||
**Session logging** writes to `sessions` and updates the matching `schedule` row to `completed`. The AI reads both tables to understand history and the current plan.
|
||||
|
||||
The existing `.ics` calendar is not imported. The AI generates a fresh adaptive schedule on first run, seeded by onboarding answers.
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
### Logging (no AI, always fast)
|
||||
|
||||
| Command | Example | Action |
|
||||
|---|---|---|
|
||||
| `/log <duration>` | `/log 2h`, `/log 90m` | Record a completed session for today |
|
||||
| `/skip [reason]` | `/skip work trip` | Mark today as skipped |
|
||||
| `/snooze` | `/snooze` | Postpone today's reminder by 2 hours |
|
||||
|
||||
### Status & history
|
||||
|
||||
| Command | Action |
|
||||
|---|---|
|
||||
| `/status` | Today's plan, completion state, next reminder time |
|
||||
| `/history` | Last 7 sessions (date, duration, notes) |
|
||||
| `/streak` | Current streak and longest streak |
|
||||
|
||||
### AI-powered
|
||||
|
||||
| Command | Action |
|
||||
|---|---|
|
||||
| `/progress` | AI analyzes recent weeks, gives assessment and flags patterns |
|
||||
| `/next` | AI proposes the next training step (duration or frequency change) |
|
||||
| `/reschedule [note]` | AI regenerates upcoming schedule; optional context ("busy next week") |
|
||||
| `/chat <message>` | Free-form coaching conversation |
|
||||
|
||||
### Setup
|
||||
|
||||
| Command | Action |
|
||||
|---|---|
|
||||
| `/setup` | First-run onboarding: current level, typical availability, goal — generates initial schedule |
|
||||
| `/settings model <id>` | Switch OpenRouter model at runtime without redeploying |
|
||||
|
||||
**Fallback:** Unrecognized messages prompt the user to use `/chat` for free-form questions.
|
||||
|
||||
**Error handling:** AI commands show a clear error message if OpenRouter is unavailable. Logging commands (`/log`, `/skip`) always work regardless of AI availability.
|
||||
|
||||
---
|
||||
|
||||
## AI Integration
|
||||
|
||||
**Client:** `openai` npm package pointed at `https://openrouter.ai/api/v1`. Model is read from `settings` at runtime.
|
||||
|
||||
**Context sent on every AI call:**
|
||||
- Training goal (from setup)
|
||||
- Last 30 sessions (date, duration, notes)
|
||||
- Upcoming schedule with statuses
|
||||
- Recent skips and their reasons
|
||||
- Current streak
|
||||
|
||||
**Default model:** `google/gemini-2.5-flash-preview`
|
||||
Chosen for strong reasoning, low cost, fast responses, and large context window. Swappable at runtime via `/settings model <id>`.
|
||||
|
||||
**Alternative models (all via OpenRouter):**
|
||||
- `deepseek/deepseek-chat` — very cheap, good for analytical tasks
|
||||
- `meta-llama/llama-3.3-70b-instruct` — free on some providers
|
||||
- `openai/gpt-4o-mini` — reliable, cheap, structured-prompt-friendly
|
||||
- `qwen/qwen-2.5-72b-instruct` — strong instruction following, low cost
|
||||
|
||||
**Prompt strategy:**
|
||||
- Coaching prompts live in a `prompts/` directory — tweakable without touching logic
|
||||
- The AI is instructed to be encouraging but honest, explain its reasoning, and flag overtraining or undertraining
|
||||
- Schedule proposals are requested as JSON for reliable parsing:
|
||||
|
||||
```json
|
||||
[
|
||||
{ "date": "2026-05-22", "planned_min": 60, "rationale": "Continuing current level" },
|
||||
{ "date": "2026-05-24", "planned_min": 60, "rationale": "Rest day between" },
|
||||
{ "date": "2026-05-26", "planned_min": 75, "rationale": "Step up if 22nd went well" }
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reminder & Scheduling Logic
|
||||
|
||||
### Cron jobs
|
||||
|
||||
| Job | Default time | Condition |
|
||||
|---|---|---|
|
||||
| Morning reminder | **05:30** | Session scheduled today and status is `pending` |
|
||||
| Evening nudge | **16:30** | Session scheduled today and status still `pending` |
|
||||
| Snooze | 2 hours after `/snooze` | One-off, clears itself after firing |
|
||||
|
||||
No reminders fire on days with no scheduled session, or on days already `completed` or `skipped`.
|
||||
|
||||
### Adaptive schedule generation
|
||||
|
||||
`/reschedule` sends full history to the AI, which returns a JSON array of upcoming sessions (7–14 days). The bot parses this into `schedule` rows. Skip reasons are included in the next `/reschedule` call so the AI accounts for gaps.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
bot/ # Telegraf setup, command handlers
|
||||
db/ # SQLite schema, queries
|
||||
scheduler/ # node-cron jobs
|
||||
ai/ # OpenRouter client, context builder
|
||||
prompts/ # Prompt templates
|
||||
config.ts # Env loading and validation
|
||||
index.ts # Entry point
|
||||
.env.example
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
.dockerignore
|
||||
package.json
|
||||
tsconfig.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
### Local development
|
||||
|
||||
Run directly with `npm run dev` (ts-node or tsx). Uses `.env` file for config.
|
||||
|
||||
### Production — Docker
|
||||
|
||||
The app runs as a Docker container on the home server. SQLite data is persisted via a named volume mounted at `/app/data/`.
|
||||
|
||||
**Dockerfile** — multi-stage build:
|
||||
1. `builder` stage: installs all dependencies, compiles TypeScript to `dist/`
|
||||
2. `runner` stage: copies only `dist/` and production `node_modules`, runs as a non-root user
|
||||
|
||||
**docker-compose.yml** — single service definition:
|
||||
- Mounts a named volume for the SQLite file (`./data:/app/data`)
|
||||
- Loads env vars from `.env` (not baked into the image)
|
||||
- `restart: unless-stopped` so it survives reboots
|
||||
|
||||
**SQLite file location:** `/app/data/assistant.db` inside the container, mapped to `./data/assistant.db` on the host — easy to back up by copying that file.
|
||||
|
||||
**`.dockerignore`** excludes: `node_modules/`, `src/`, `.env`, `data/`, `*.md`.
|
||||
|
||||
**Timezone:** Container timezone set via `TZ` env var (e.g. `TZ=Europe/Amsterdam`) so cron jobs fire at the correct local times.
|
||||
|
||||
**Updating:** `docker compose pull && docker compose up -d --build` rebuilds from source and restarts with zero downtime for a single-container setup.
|
||||
|
||||
---
|
||||
|
||||
## Future Extension Points
|
||||
|
||||
- **Web dashboard (Approach C):** Read-only layer on the same SQLite file — no schema changes needed
|
||||
- **Multi-user:** `settings` and `schedule` tables can gain a `user_id` column
|
||||
- **Export:** Sessions table trivially exportable to CSV or JSON
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Config } from 'jest';
|
||||
|
||||
const config: Config = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/src/__tests__/**/*.test.ts'],
|
||||
clearMocks: true,
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
+5733
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "assistant",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"doc": "docs"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^12.10.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"node-cron": "^4.2.1",
|
||||
"openai": "^6.38.0",
|
||||
"telegraf": "^4.16.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^25.9.1",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"jest": "^30.4.2",
|
||||
"ts-jest": "^29.4.10",
|
||||
"tsx": "^4.22.3",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user