Skip to content

Lucerna-Systems/LucernaPA

Repository files navigation

Lucerna Telegram Assistant

A thin TypeScript Telegram bot that relays your messages into your locally installed Claude Code CLI (authenticated via your Claude Pro/Max login — no API key involved) so you can manage GitHub issues, GitHub Projects boards, Google Calendar events, and Teamwork timers/task assignment just by texting a bot. Nothing here is tied to one person's setup — every credential, name, and repo/project reference is read from your own config, so anyone can clone this and point it at their own accounts.

Licensed under PolyForm Noncommercial 1.0.0 — free to use, fork, and modify for personal, educational, nonprofit, or other non-commercial purposes. Commercial use (selling it, running it as part of a paid product/service, or using it inside a for-profit company's revenue-generating operations) requires the copyright holder's permission.

Table of contents

Quick start

If you already have Claude Code installed and logged in, and the three MCP servers set up:

npm install
cp .env.example .env
# edit .env: at minimum set TELEGRAM_BOT_TOKEN and ALLOWED_CHAT_IDS
npm run build
npm start

Message your bot on Telegram to confirm it replies, then read Setup below for the full walkthrough (group-chat mode, per-chat repo/project defaults) and MCP_SETUP.md for the one-time GitHub/Calendar/Teamwork credential setup this depends on.

How it works

  1. You send a message to the bot.
  2. The bot runs claude -p "<your message>" --output-format json --model claude-haiku-4-5-20251001 --append-system-prompt "<headless context>" --allowedTools <26 allowed tools> (adding --resume <session_id> if this chat has talked to Claude before).
  3. Claude Code either calls one of the allowed MCP tools or just replies conversationally.
  4. The bot sends Claude's result text back to you on Telegram, and remembers the session_id for that chat so "actually make that 3pm" works as a follow-up.

What's reachable from Telegram

See ALLOWED_TOOLS in src/claudeRunner.ts for the exact list:

  • GitHub issues: create, edit/close/reopen, look up a specific issue, list, search (including "assigned to me"), and comment.
  • GitHub Projects (project boards): list projects and their items, look up a project/item/field, add an issue or PR to a board, move an item between statuses, post a status update. Requires extra setup — see MCP_SETUP.md for the PAT scope and Docker toolset changes this needs.
  • Google Calendar events: create, edit, cancel, list, search, check free/busy, and respond to invitations.
  • Teamwork timers and task assignment: search for a project or task by name (no need to know numeric IDs), list timers, create a timer for a task, start/pause/complete a timer, look up a task, and assign yourself to a task. Requires extra setup — see MCP_SETUP.md's Teamwork section; there's no official Teamwork MCP server, so this project includes a small custom one (teamwork-mcp-server/). Assignment is specifically built to only ever add you to a task's assignees, never replace the list — it can't accidentally bump a teammate off a task.

Everything else Claude Code can normally do (file edits, bash, browsing, pull requests, other MCP tools) is locked out by --allowedTools.

Because this runs headlessly (no terminal, no browser), a tool call that isn't on the allowlist is simply denied — there's no way to "approve" it from Telegram after the fact. --append-system-prompt tells Claude this explicitly, so instead of asking you to approve something that can never be approved (a confusing loop hit during development), it just says what it can't do. If you hit a capability gap, the fix is always to add the specific tool it needs to ALLOWED_TOOLS in src/claudeRunner.ts, not to reply "yes" to it.

Prerequisites

  • macOS with Node.js 18+ (Linux should work too; not tested on Windows).

  • Claude Code installed and logged in with your Claude Pro/Max subscription (claude works in your terminal without any ANTHROPIC_API_KEY set).

  • Three MCP servers registered at user scope in Claude Code, named github, google-calendar, and teamwork, each connected via a long-lived credential (not an interactive OAuth connector — see MCP_SETUP.md for why and exactly how to set these up). In short: this bot invokes Claude Code headlessly, so any MCP server that needs a browser popup or in-chat "grant permission" click to (re)authenticate will silently stall. Local, token-authenticated servers (a GitHub PAT via Docker; a cached Google OAuth refresh token; a Teamwork API key for the custom teamwork-mcp-server/) avoid that entirely.

    Confirm all three are connected with:

    claude mcp list

⚠️ Verify the exact tool names before relying on this

src/claudeRunner.ts's ALLOWED_TOOLS follows Claude Code's mcp__<server-name>__<tool-name> convention, and was checked against each server's live tool docs on 2026-07-06 — including catching a real mistake: GitHub's MCP server doesn't have a standalone create_issue tool anymore, it's been consolidated into issue_write with a method param. GitHub in particular has changed tool names across releases before, so re-verify after any Docker image update. Your local server names (the github / google-calendar / teamwork part of each tool string) come from whatever you called them in claude mcp add, so double check by running:

claude mcp list

and confirming the server names match. If they don't, edit ALLOWED_TOOLS in src/claudeRunner.ts to match. If a name is wrong, nothing breaks loudly — --allowedTools just won't match, and Claude Code will decline to use that tool or ask for permission, which will surface as an odd reply from the bot.

Teamwork's tools are less battle-tested than GitHub's/Calendar's — two of the nine (create_timer's request body, and the pause_timer/complete_timer endpoint paths) were built from documented conventions rather than an independently confirmed live call. Test each one directly per MCP_SETUP.md before trusting Telegram with it.

Setup

npm install
cp .env.example .env

Edit .env:

  • TELEGRAM_BOT_TOKEN — create a bot via @BotFather on Telegram (/newbot) and paste the token it gives you.
  • ALLOWED_CHAT_IDS — strongly recommended. Message your new bot once, then check the logs (or use @userinfobot) to find your numeric chat ID, and put it here. Without this, anyone who finds your bot's username can trigger it.
  • CLAUDE_BIN — leave as claude unless the CLI isn't on your PATH.
  • CLAUDE_TIMEOUT_MS — how long to wait for a single Claude Code call before giving up (default 2 minutes).
  • CLAUDE_MODEL — defaults to claude-haiku-4-5-20251001. Haiku is fast/cheap and plenty for issue/event creation; bump to claude-sonnet-5 if you want stronger reasoning.
  • OWNER_ID — optional, enables team-group-chat mode. See below.
  • OWNER_NAME / BOT_NAME — optional, purely cosmetic. OWNER_NAME shows up in the message anyone outside ALLOWED_CHAT_IDS sees (e.g. "wired up for <OWNER_NAME>'s personal use"); BOT_NAME shows up in the startup log and the bot's own self-introduction. Both fall back to generic text ("the bot owner" / "Telegram Assistant") if left blank — set them so the bot reads naturally for whoever's actually running it.

Using this in a shared group chat

If you add the bot to a team group, add that group's chat ID to ALLOWED_CHAT_IDS and set OWNER_ID to your own Telegram user ID (find both via @userinfobot). With OWNER_ID set:

  • Messages from anyone else in the group are silently ignored — the bot won't react, reply, or run Claude Code for them at all.
  • Your own messages in the group still run normally, but the reply doesn't always get posted there. Claude decides whether the result is worth the whole team seeing (an error, something needing someone else's input, a result that affects the team) — if not, the group just gets a 👍 reaction and the full reply comes to you as a private DM from the bot instead. Errors are always posted in the group regardless of Claude's judgment, so a failure never silently disappears into a DM only you see.
  • Your private 1:1 chat with the bot is unaffected either way — always a full reply, no reactions.

Leave OWNER_ID unset if you'd rather every allowed chat just get full replies, same as before.

Per-chat defaults: repo, project board, and team context

Copy groups.config.example.json to groups.config.json (gitignored, since it's specific to your teams) and add an entry per chat you want defaults for, keyed by that chat's numeric Telegram id (as a string, including the - for groups):

{
  "groups": {
    "-1001234567890": {
      "label": "Website Team",
      "githubRepo": "your-org/your-repo",
      "githubProject": { "owner": "your-org", "number": 1 },
      "context": "We label bugs/features/chores. Board columns: Backlog, In Progress, Review, Done. Default assignee is whoever opens the ticket unless someone else is named."
    }
  }
}

With this in place, a plain "create a ticket for the login bug" in that group resolves to your-org/your-repo and project 1 automatically — you don't have to name the repo/board every time, and the context field lets Claude follow that team's conventions (labels, columns, default assignee, anything else worth telling it once). All fields are optional; a message can still name a different repo/board explicitly and Claude will use that instead of the default. The file is re-read on every message, so edits take effect immediately — no restart needed. Any chat without an entry just behaves as before (Claude asks for the repo/board if it's not obvious).

Run

npm run build
npm start

Or for local development with auto-reload:

npm run dev

Message your bot on Telegram. Try things like:

  • "create a github issue in your-org/your-repo titled 'fix login bug' with a short description"
  • "what's assigned to me in your-org/your-repo?"
  • "close issue #42 in your-org/your-repo as a duplicate of #10"
  • "add a comment to issue #7 saying I'll pick this up tomorrow"
  • "what's on the Website Redesign project board?" (needs the GitHub Projects setup in MCP_SETUP.md)
  • "move item X to In Progress on the your-org project 1 board"
  • "add a calendar event tomorrow at 2pm called Dentist"
  • "actually make that 3pm" (follow-up, same chat — edits the existing event via update-event instead of creating a duplicate)
  • "am I free at 3pm tomorrow?"
  • "what's on my calendar this week?"
  • "start a Teamwork timer for task 12345, mark it billable"
  • "what timers do I have running right now?"
  • "assign me to the PT10-25-P060 task" (looks the task up by name/reference via search_tasks, no numeric ID needed, and adds you to the assignee list without removing anyone already on it)
  • "what tasks are assigned to me on the Website Redesign project?" (resolves the project name via search_projects first)
  • "complete my current Teamwork timer"
  • "what's the capital of France?" (no tool matches, so Claude just replies)
  • Reply to a teammate's message with "create tickets for this" — the quoted message's text (and any photo it has) gets included as context.
  • Send a photo of a whiteboard or screenshot with a caption like "make an issue for each of these" — the image gets downloaded and Claude looks at it directly.

What happens with non-text messages

  • Photos (yours, or one you're replying to) are downloaded to a temp folder and Claude gets a narrowly-scoped permission to Read just that one file — not a standing filesystem grant — then the file is deleted once the call finishes.
  • Replies to another message include that message's text/caption as quoted context, so "create tickets for this" works when "this" is something someone else said.
  • Everything else (voice notes, video, stickers, documents, locations, contacts, polls, GIFs) gets a short explanation of what it can't do instead of silence — silence in a group chat is indistinguishable from "you're not the owner" or "the bot crashed," so it always says something.

Project layout

  • src/index.ts — Telegram polling loop, per-chat session map, per-chat request queue.
  • src/claudeRunner.ts — spawns the claude CLI, parses its JSON output, extracts result and session_id.
  • src/messageContent.ts — turns a Telegram message (text, caption, reply-to, photo) into the prompt text and any extra Read permissions sent to Claude.
  • src/groupConfig.ts — loads groups.config.json and formats a chat's repo/board/context defaults for the system prompt.
  • src/types.ts — TypeScript shapes for the CLI's JSON output.
  • teamwork-mcp-server/ — separate small Node/TS package: a custom local MCP server for Teamwork timers and safe task assignment (no official Teamwork MCP server exists). Its own package.json, builds independently, runs as its own process. See src/index.ts (tool registrations) and src/teamworkClient.ts (the actual API calls, including the safe assignee-merge logic).
  • MCP_SETUP.md — one-time setup for the three MCP servers this bot depends on (GitHub, Google Calendar, Teamwork).
  • .env.example / groups.config.example.json — copy these to .env / groups.config.json (both gitignored) and fill in your own values; see Setup above.
  • LICENSE — PolyForm Noncommercial 1.0.0 (free for non-commercial use; commercial use needs permission).

Known limitations (v1)

  • Sessions are stored in memory only (Map<chatId, sessionId>) — restarting the bot loses conversation continuity for all chats. No DB, by design, per the v1 scope.
  • One Claude Code call per chat runs at a time; concurrent messages in the same chat are queued rather than parallelized, to avoid racing on the same --resume session.
  • Photos are supported (downloaded + given to Claude via a scoped Read grant); voice, video, documents, and other media types are not — the bot explains this rather than staying silent. Adding real support for those would need transcription (voice/video) or document parsing, which is out of scope for now.
  • Teamwork support is newer and less verified than GitHub/Calendar: create_timer's request body shape and the pause_timer/complete_timer endpoint paths were inferred from documented conventions, not confirmed against a live call, since Teamwork's own API docs render some schemas client-side in a way that wasn't fetchable while building this. Test these directly (see MCP_SETUP.md) before relying on them.
  • This is a single-user (or single-team) personal tool by design: it runs against one Claude Code login and one set of MCP credentials on one machine. It's not multi-tenant — if a teammate wants their own instance, they need their own Claude subscription, their own GitHub/Calendar/Teamwork credentials, and their own copy of this repo running on their own machine (see MCP_SETUP.md).

About

No description, website, or topics provided.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors