A local-first task manager with projects, tasks, attachments (images, audio, video, voice memos), and a Telegram AI bot — built with Deno, Fresh 2, and SQLite (or Turso).
Designed to also act as an MCP server so Claude (or Claude Desktop) can read and manage your tasks directly from conversations.
- Projects — organize work into projects
- Tasks — title, description, priority (
low/medium/high), status (todo/in_progress/done) - Board & list views — toggle between a kanban board (with drag & drop) and a grouped list view
- Attachments — drag & drop images, upload audio files (MP3, M4A), upload video files (MP4), or record voice memos per task
- Image lightbox — click any image attachment to view it full-screen with prev/next navigation
- Clickable links — URLs in task descriptions are automatically rendered as links
- Calendar — interactive calendar (FullCalendar) with events, notes, and reminders; month/week/day/year views; per-day event panel with inline create/edit/delete; stats bar
- Event notifications — Telegram message 10 min before timed events; optional phone call (xAI voice agent) 5 min before
- SQLite or Turso — local SQLite by default; switch to Turso cloud database via env vars
- REST API — clean JSON API for programmatic access
- MCP server — two modes: direct DB (local) or HTTP client (remote/Docker)
- Telegram bot — AI-powered bot (Claude or GPT-4o-mini) that manages tasks via natural language, supports voice transcription and media attachments
Requires Deno 2.2+.
git clone https://github.com/ddtch/lite-task
cd lite-task
deno task dev
# → http://localhost:8011The SQLite database (data/task-light.db) and uploads (data/uploads/) are created automatically on first run inside the data/ directory.
Arc browser users: the dev server uses a custom Vite protocol scheme that Arc blocks. Use
deno task preview(production build) for full functionality, or open in Chrome/Firefox.
No configuration needed. The database is created at data/task-light.db relative to the working directory on first run.
Turso is a libSQL-based cloud database. When TURSO_DB_URL and TURSO_API_KEY are both set, the app connects to Turso instead of local SQLite — no other changes needed.
TURSO_DB_URL=libsql://your-database.turso.io
TURSO_API_KEY=your-auth-tokenGet these from the Turso dashboard or the turso CLI:
turso db create lite-task
turso db show lite-task --url
turso db tokens create lite-taskWhen using Turso in Docker, the
data/task-light.dbfile is never written. Uploaded files (images, audio, video) still need thedata/uploads/volume — those are stored on disk regardless of DB mode.
# 1. Install dependencies
deno install --allow-scripts=npm:@tailwindcss/oxide,npm:esbuild,npm:sharp
# 2. Build the Fresh app
deno task build
# 3. Serve
deno task start
# → http://localhost:8011To change the port:
deno serve -A --port=3000 --host=0.0.0.0 _fresh/server.jsEvery push to main publishes a multi-arch image to
ghcr.io/ddtch/lite-task (linux/amd64 and linux/arm64), so a server never
builds anything: deno task build runs Vite, Tailwind and esbuild and will
exhaust a 1 GB VPS. Tagged releases publish vX.Y.Z and X.Y alongside
latest.
mkdir -p ~/lite-task && cd ~/lite-task
curl -O https://raw.githubusercontent.com/ddtch/lite-task/main/docker-compose.yml
curl -o .env https://raw.githubusercontent.com/ddtch/lite-task/main/.env.exampleFill in .env. At minimum set VOICE_API_TOKEN (openssl rand -hex 32) and
APP_BASE_URL; the Telegram, xAI and Turso blocks are all optional.
docker compose pull
docker compose up -dFour containers come up — the web app, the Telegram bot, and the two
schedulers — all reading the same .env and sharing ./data/:
./data/
task-light.db ← SQLite DB (local mode only; unused when Turso is active)
bot-messages.db ← Telegram message history (always local SQLite)
uploads/ ← uploaded images, audio, and video files
The directory and the SQLite files are created on first run.
The app has no login of its own. The web port is published on
127.0.0.1:8011 for exactly that reason: authentication and TLS belong to a
proxy on the host. Ready-made configs are in deploy/ —
Caddy, nginx. Both do
the same two things:
- password-protect the UI, the JSON API and
/mcpwith basic auth; - let
/api/voice/*and/mcpthrough without it, because the voice agent reaches those directly and cannot log in. They are guarded byVOICE_API_TOKENinstead, which the app accepts asAuthorization: Bearer,X-Lite-Task-Token, or a?token=query parameter — prefer a header, since a query parameter lands in access logs. Leave the variable unset and those routes are open to anyone who finds the domain; the app logs a warning at startup when that is the case.
Two ways, both printed by deno task calls:tools:
- As an MCP server (recommended). In the Voice Agent Builder choose
Add custom MCP server, point it at
https://your-domain/mcpand add the headerAuthorization: Bearer $VOICE_API_TOKEN, marked Secret. That is one entry for all sixteen tools, and anything added tomcp/toolkit.tslater shows up without touching the agent. xAI supports Streamable HTTP, which is what/mcpspeaks. - As HTTP tools. Nine voice-shaped tools declared one by one against
https://your-domain/api/voice/tool, with the same bearer header. They take project and task names instead of ids, which suits speech; the MCP tools are id-based, so over MCP the agent lists first and acts second.
Once the domain resolves, set APP_BASE_URL=https://your-domain and run
deno task calls:tools to get tool URLs with the token already embedded.
docker compose pull && docker compose up -dPin a release instead of tracking main by setting LITE_TASK_TAG=v1.2.3 in
.env; rolling back is then editing that line and re-running the same command.
For local development, or to run a patched build on a machine with ~2 GB of free memory:
git clone https://github.com/ddtch/lite-task
cd lite-task
cp .env.example .env
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --buildlite-task ships three MCP server modes. Pick one based on your setup.
The app exposes an MCP endpoint at /mcp using the Streamable HTTP transport. No subprocess, no env vars — just point your tool at the URL.
Works whenever the app is running (locally or in Docker).
~/.cursor/mcp.json:
{
"mcpServers": {
"lite-task": {
"url": "http://localhost:8011/mcp"
}
}
}That's it. Reload MCP servers in Cursor (Cmd+Shift+P → "MCP: Reload Servers").
Connects to any running lite-task instance over HTTP via stdio. Works whether the app runs locally, in Docker, or on a remote server. Can be compiled to a standalone binary — no Deno required on the machine running the AI tool.
cd task-light
deno task compile-mcp
# → produces ./lite-task-mcp
# Move somewhere permanent
mv lite-task-mcp ~/.local/bin/ # Linux / macOSOr skip compilation and use Deno directly — see configs below.
Config file locations:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json
With compiled binary:
{
"mcpServers": {
"lite-task": {
"command": "/Users/you/.local/bin/lite-task-mcp",
"env": {
"LITE_TASK_URL": "http://localhost:8011"
}
}
}
}With Deno (no compilation needed):
{
"mcpServers": {
"lite-task": {
"command": "deno",
"args": ["run", "-A", "/path/to/task-light/mcp/http-client.ts"],
"env": {
"LITE_TASK_URL": "http://localhost:8011"
}
}
}
}After editing, restart Claude Desktop. You should see the lite-task tools listed under the hammer icon in a new conversation.
Recommended: use Mode 1 (built-in HTTP endpoint) — just
"url": "http://localhost:8011/mcp"with no subprocess.
Alternatively, with the compiled binary or Deno:
{
"mcpServers": {
"lite-task": {
"command": "/Users/you/.local/bin/lite-task-mcp",
"env": {
"LITE_TASK_URL": "http://localhost:8011"
}
}
}
}Add to ~/.claude/settings.json:
{
"mcpServers": {
"lite-task": {
"command": "/Users/you/.local/bin/lite-task-mcp",
"env": {
"LITE_TASK_URL": "http://localhost:8011"
}
}
}
}You can also scope it to a single project by placing the same config in .claude/settings.json at the project root.
Restart Claude Code after editing. Run /mcp in a session to confirm the server is connected.
When lite-task and the AI tool run on the same machine, this mode skips HTTP entirely and reads SQLite directly — no web server needed.
{
"mcpServers": {
"lite-task": {
"command": "deno",
"args": ["run", "-A", "/path/to/task-light/mcp/server.ts"],
"cwd": "/path/to/task-light"
}
}
}The
cwdmust point totask-light/so the server resolvesdata/task-light.dbcorrectly. This mode does not support Turso — it always reads the local SQLite file.
| Tool | Description |
|---|---|
list_projects |
List all projects with task counts |
create_project |
Create a new project |
get_project |
Get a project with its tasks |
delete_project |
Delete a project and all its tasks |
list_tasks |
List tasks — filter by project_id, status, priority |
create_task |
Create a task in a project |
get_task |
Get task details including attachments |
update_task |
Update title, description, status, or priority |
delete_task |
Delete a task |
get_attachment |
Download an attachment image and return it as base64 |
list_events |
List calendar events — filter by month, project_id |
create_event |
Create a calendar event, note, or reminder (+ notify_call for phone reminder) |
get_event |
Get a calendar event by ID |
update_event |
Update event fields including notify_call |
delete_event |
Delete a calendar event |
The MCP HTTP client (mcp/http-client.ts) compiles to a standalone binary. The .github/workflows/release.yml workflow automates this: push a version tag and it cross-compiles for all five targets and creates a GitHub Release.
git tag v0.1.0
git push origin v0.1.0
# → GitHub Actions builds and publishes the release automaticallycd npm-package
npm publishUsers install with:
npm install -g lite-task-mcpHomebrew requires a tap repo named homebrew-lite-task. After each release, update the sha256 values in the formula using checksums.txt from the GitHub Release.
brew tap ddtch/lite-task
brew install lite-task-mcpcurl -fsSL https://raw.githubusercontent.com/ddtch/lite-task/main/install.sh | shlite-task ships a Telegram bot that accepts natural-language messages and uses an AI agent (Claude or GPT-4o-mini) to read and manage your tasks.
Examples:
- "List my projects"
- "Create a task called Fix login bug in project Personal with high priority"
- "What tasks are in progress?"
- "Add an event called Team standup on 2026-03-10 at 10:00"
- "Create a reminder for March 15 — submit tax forms"
- "What events do I have this month?"
- Send a voice message — it gets transcribed and the agent acts on the spoken words
- Send a photo or file — the agent can attach it to any task
Talk to @BotFather, send /newbot, follow the prompts, copy the token. To find your user ID, message @userinfobot.
cp .env.example .envEdit .env:
LITE_TASK_URL=http://localhost:8011 # or wherever lite-task runs
TELEGRAM_BOT_TOKEN=<token from BotFather>
BOT_HOST_ID=<your Telegram user ID> # restricts bot to you only
ANTHROPIC_API_KEY=<your key> # takes priority over OpenAI
OPENAI_API_KEY=<your key> # fallback agent; also enables voice transcription (Whisper)BOT_HOST_ID is required — the bot only responds to messages from this user ID, even in groups and channels it joins.
# Terminal 1 — web app
deno task dev
# Terminal 2 — Telegram bot
deno task botdocker compose up -dThe bot and event-scheduler services start automatically after the lite-task service passes its health check. All services share the ./data volume, so databases persist across restarts.
| Env var | Provider | Model |
|---|---|---|
ANTHROPIC_API_KEY |
Anthropic Claude | claude-sonnet-4-6 |
OPENAI_API_KEY |
OpenAI | gpt-4o-mini |
Set one or both. Anthropic takes priority if both are present.
When you send a voice message, it is automatically transcribed using OpenAI Whisper (whisper-1) and the transcript is passed to the agent — which can create tasks, update descriptions, or answer questions based on what you said. The audio file is also available to attach to any task.
Transcription requires OPENAI_API_KEY. Without it, the bot falls back to handling the voice file as a plain attachment.
| Telegram type | Stored as |
|---|---|
| Photo | image |
| Voice message | voice |
| Audio (MP3/M4A) | audio |
| Video (MP4) | video |
| Document/file | auto-detected by MIME type |
Add the bot to a Telegram group or channel. It saves all messages it sees to data/bot-messages.db. From your private chat with the bot, you can ask it to read group history:
- "What was discussed in the team group today?"
- "Create tasks from the last 20 messages in the project channel"
In groups, the bot only responds when @mentioned or when replying to its own message.
lite-task uses the xAI Voice Agent for voice-based task management: you call the agent's number, and it can call you back with reminders.
What you can do by voice:
- "Create a task called Fix login bug in project Personal"
- "What tasks are in progress?"
- "Mark the deploy task as done"
- "Remind me about the deadline tomorrow at 3pm"
The agent itself lives in the Voice Agent Builder
(console.x.ai → Voice → Agents), not in this repo, because the /v1/agents and
/v1/tools APIs answer 403 "agents endpoint is not enabled for this team" —
so the prompt and the tools cannot be pushed onto the agent over the API.
This repo stays the source of truth for both and prints them for pasting.
What the app drives over the API: outbound calls, phone-number configuration
(/v2/phone-numbers), call control (refer, hangup), ephemeral client
secrets, and the tool endpoint every tool call lands on.
Sign up at console.x.ai, create a voice agent in Voice → Agents, and attach a phone number to it. Each account includes one.
XAI_API_KEY=<your xAI API key>
XAI_AGENT_ID=<agent_... from the Builder>
APP_BASE_URL=<public URL for webhooks, e.g. https://your-domain.com or ngrok URL>Check what the account exposes and how each number is routed:
deno task calls:setupdeno task calls:toolsPaste the printed prompt into the agent, set its timezone to the printed value,
and add each tool with the printed schema, pointing every one at
$APP_BASE_URL/api/voice/tool. Re-run this whenever APP_BASE_URL changes —
the tool URLs embed it.
Optionally point the agent's webhook at $APP_BASE_URL/api/voice/webhook and put
the signing secret in XAI_WEBHOOK_SECRET so call events are logged and verified.
For outbound reminder calls, add:
REMINDER_TO_NUMBER=+1YYYYYYYYYY # your personal number
XAI_FROM_NUMBER=+1XXXXXXXXXX # only if the agent has more than one numberRun the reminder scheduler:
deno task calls:schedulerCheck the phone path without waiting for a reminder to come due:
deno task calls:test # calls REMINDER_TO_NUMBER
deno task calls:test +15551234567 # or any numberOutbound calls and the gated API. Placing a call goes through the same Voice Agent Builder API that is disabled for teams outside the beta, so the scheduler logs xAI's 403 verbatim and marks the reminder failed if your team is not enabled yet.
deno task calls:testreports the same 403 in one second, which is the quickest way to tell an account problem from a configuration one.XAI_OUTBOUND_PATHoverrides the endpoint path (default/v1/realtime/calls) if xAI publishes a different one.
Calendar events with a time set automatically get notifications:
- Telegram message — sent ~10 minutes before the event via the bot
- Phone call (optional) — triggered ~5 minutes before if "Call me" was checked when creating the event
Needs TELEGRAM_BOT_TOKEN and BOT_HOST_ID in .env (same as the bot).
Needs the xAI voice agent configured (see Voice Calling above) plus:
XAI_AGENT_ID=<your agent ID>
REMINDER_TO_NUMBER=+1YYYYYYYYYYdeno task events:schedulerIn Docker, the event-scheduler service starts automatically alongside the bot.
- The scheduler polls every 60 seconds for events approaching their
event_time - Events within 10 minutes get a Telegram notification (once per event)
- Events within 5 minutes with
notify_call = 1get a phone call (once per event) - Notification flags (
notified_telegram,notified_call) prevent duplicate sends
GET /api/projects → list all projects
POST /api/projects → create project { name, description? }
GET /api/projects/:id → get project + tasks
PUT /api/projects/:id → update project { name?, description? }
DELETE /api/projects/:id → delete project
GET /api/tasks → list tasks ?project_id=&status=&priority=
POST /api/tasks → create task { project_id, title, description?, priority?, status? }
GET /api/tasks/:id → get task + attachments
PUT /api/tasks/:id → update task { title?, description?, priority?, status? }
DELETE /api/tasks/:id → delete task
GET /api/events → list events ?month=YYYY-MM&project_id=
POST /api/events → create event { title, event_date, description?, event_time?, type?, project_id?, notify_call? }
GET /api/events/:id → get event
PUT /api/events/:id → update event { title?, description?, event_date?, event_time?, type?, project_id?, notify_call? }
DELETE /api/events/:id → delete event
Event types: event, note, reminder.
notify_call (boolean) — when true and event_time is set, the event scheduler will trigger a phone call 5 minutes before the event. Timed events always get a Telegram notification 10 minutes before, regardless of this flag.
POST /api/tasks/:id/upload → upload file (multipart, field: "file")
image/* → type "image"
audio/webm|ogg → type "voice" (recorded memo)
other audio/* → type "audio" (MP3, M4A, etc.)
video/* → type "video" (MP4, MOV, etc.)
GET /api/uploads/:filename → serve uploaded file
POST /api/voice/tool → function-calling dispatcher (called by the xAI agent)
POST /api/voice/webhook → xAI event webhook (realtime.call.incoming, call lifecycle)
GET /api/reminders → list reminders ?status=
POST /api/reminders → create reminder { message, remind_at, phone_number?, task_id?, project_id? }
GET /api/reminders/:id → get reminder
PUT /api/reminders/:id → update reminder { message?, remind_at?, phone_number?, status? }
DELETE /api/reminders/:id → delete reminder
task-light/
├── bot/
│ ├── main.ts # Telegram bot entry point (grammY)
│ ├── agent.ts # AI agent loop (Anthropic / OpenAI) + voice transcription
│ ├── tools.ts # Tool definitions and REST API executor
│ ├── media.ts # Telegram file download helpers
│ └── store.ts # SQLite store for group/channel message history (data/bot-messages.db)
├── calls/
│ ├── xai.ts # xAI voice API client (fetch-based)
│ ├── tools.ts # Voice agent tool definitions
│ ├── setup.ts # Reports account, numbers and agent routing
│ ├── manifest.ts # Prints prompt + tools to paste into the Builder
│ ├── scheduler.ts # Reminder scheduler — triggers outbound calls
│ └── event-scheduler.ts # Event notification scheduler (Telegram + phone calls)
├── db/
│ ├── database.ts # DB adapter — local SQLite (node:sqlite) or Turso (@libsql/client)
│ └── queries.ts # Async CRUD helpers (work with both adapters)
├── mcp/
│ ├── server.ts # MCP stdio server — direct SQLite access
│ └── http-client.ts # MCP stdio server — HTTP client (compilable)
├── routes/
│ ├── _app.tsx # Global layout
│ ├── index.tsx # → redirect to /projects
│ ├── calendar.tsx # Calendar page (FullCalendar, events/notes/reminders)
│ ├── calls.tsx # Voice agent page (reminders, call history)
│ ├── projects/
│ │ ├── index.tsx # Project list
│ │ └── [id]/
│ │ ├── index.tsx # Project detail (list + board view, view toggle)
│ │ └── tasks/
│ │ ├── new.tsx
│ │ └── [taskId]/
│ │ ├── index.tsx # Task detail (attachments, status update)
│ │ └── edit.tsx
│ ├── mcp.ts # MCP Streamable HTTP endpoint (/mcp)
│ └── api/
│ ├── projects/
│ ├── tasks/
│ │ └── [id]/upload.tsx
│ ├── uploads/
│ ├── events/ # Calendar event CRUD
│ ├── voice/ # xAI voice agent endpoints
│ │ ├── tool.ts # Function-calling dispatcher
│ │ └── webhook.ts # Call lifecycle events
│ └── reminders/ # Reminder CRUD
├── islands/ # Client-side Preact components (hydrated in browser)
│ ├── ProjectCreateModal.tsx
│ ├── KanbanBoard.tsx # Drag-and-drop board view
│ ├── ImageLightbox.tsx # Full-screen image viewer
│ ├── AttachmentUploader.tsx # Drag-drop file uploader (image / audio / video)
│ ├── Calendar.tsx # Interactive calendar (FullCalendar)
│ ├── VoiceRecorder.tsx # In-browser voice memo recorder
├── components/
│ └── Badge.tsx
├── data/ # Runtime data (gitignored) — DB files + uploads
│ ├── task-light.db # Main app SQLite database (local mode)
│ ├── bot-messages.db # Telegram message history
│ └── uploads/ # Uploaded files
├── Dockerfile
├── docker-compose.yml
├── .env.example
└── deno.json
| Layer | Technology |
|---|---|
| Runtime | Deno 2.2+ |
| Framework | Fresh 2.2 |
| Bundler | Vite 7 + @fresh/plugin-vite |
| Styling | Tailwind CSS v4 |
| Database | SQLite via node:sqlite (local) or Turso (@libsql/client) |
| Interactivity | Preact islands + @preact/signals |
| MCP | @modelcontextprotocol/sdk |
| Telegram bot | grammY |
| AI agent | Anthropic SDK / OpenAI SDK |
| Voice calling | xAI Voice Agent (Grok voice, SIP telephony) |
| Task | What it does |
|---|---|
deno task dev |
Dev server with HMR on port 8011 |
deno task build |
Build for production → _fresh/ |
deno task start |
Serve production build on port 8011 |
deno task preview |
Build + serve in one command (useful in Arc browser) |
deno task bot |
Run the Telegram bot (reads .env) |
deno task mcp |
MCP server — direct SQLite access |
deno task mcp:http |
MCP server — HTTP client mode |
deno task compile-mcp |
Compile MCP HTTP client to a standalone binary |
deno task calls:setup |
Report xAI account, phone numbers and agent routing |
deno task calls:tools |
Print prompt + tools to paste into the Builder |
deno task calls:test |
Place one real outbound call now, to test the path |
deno task calls:scheduler |
Run reminder scheduler for outbound calls |
deno task events:scheduler |
Run event notification scheduler (Telegram + calls) |
All runtime data lives under data/ (relative to the working directory):
| Path | Contents |
|---|---|
data/task-light.db |
Main app database (local SQLite mode only) |
data/bot-messages.db |
Telegram group/channel message history (always local) |
data/uploads/ |
Uploaded files (images, audio, video) |
All files are created automatically on first run. In Docker, the entire data/ directory is bind-mounted from the host (./data:/app/data), so data survives container restarts and image rebuilds.
When Turso is configured, data/task-light.db is never written — but data/uploads/ is still used for file storage.