Link a project directory to a Telegram bot that chats with Claude
Project description
link-project-to-chat
Chat with an LLM agent about a project via Telegram or a local web UI. Links a local directory to a chat surface — send messages, get responses with full project context. Claude is the default backend; Codex, OpenAI API, Gemini, Mistral, Grok, and Claude SDK are selectable via /backend.
Security warning
This tool exposes agent CLIs and a /run command for shell execution. It is effectively a remote shell on your machine. On Telegram, only use it with bot tokens you control and never share them. With the web UI, bind to 127.0.0.1 and don't expose the port to the public internet without your own auth in front.
On Telegram, access is restricted to configured usernames. On first contact, the bot locks each user's numeric Telegram ID — subsequent requests are validated by ID, not username, so a username change cannot bypass access. Multiple users are supported.
Requirements
- Python 3.11+
- Claude Code installed and authenticated (
claudeon PATH) for the default backend; optional Codex CLI (codexon PATH), OpenAI API key (OPENAI_API_KEYwith[openai-api]), Gemini CLI, Mistral Vibe CLI, Grok Build CLI, or Claude SDK extra for alternate/backendchoices - For Telegram: a Telegram bot token — create a bot via @BotFather on Telegram
- For local Web UI: nothing extra beyond the
[web]install extra - For Google Chat: the
[google-chat]install extra and a readable Google service-account JSON file - For Slack: the
[slack]install extra and a Slack app with a Bot User OAuth Token (xoxb-…) plus an App-Level Token (xapp-…) — see docs/slack-app-manifest.yaml and the "Slack transport" section below
Install
pipx install link-project-to-chat
PyPI status: verified published on PyPI 2026-06-25.
Optional extras:
| Extra | Pulls in | Use it for |
|---|---|---|
[create] |
httpx, telethon |
/create_project, /create_team, BotFather automation |
[voice] |
openai |
OpenAI Whisper API voice transcription |
[openai-api] |
openai |
Direct OpenAI API coding-agent backend (/backend openai) |
[web] |
fastapi[standard], jinja2, aiosqlite |
Local browser UI transport |
[google-chat] |
fastapi[standard], httpx, google-auth |
Google Chat HTTP endpoint transport |
[slack] |
slack_bolt, slack_sdk |
Slack Socket Mode transport |
[claude-sdk] |
claude-agent-sdk |
Claude SDK backend (/backend claude_sdk) |
[all] |
all of the above, including [claude-sdk] |
Everything |
Example:
pipx install "link-project-to-chat[all]"
Contributor / from-source install
pip install -e "." # core only
pip install -e ".[all]" # all optional deps
pip install -e ".[dev]" # + ruff / mypy / pytest-cov / diff-cover (quality gate)
# Quality gate (run before every commit)
make check # blocking: ruff lint, mypy baseline gate, pytest
Quick start
The ad-hoc --path/--token flow bypasses the config file, so pass the
one-shot allowed Telegram username on the same command line. For saved bots,
prefer configure --add-user USER[:ROLE].
link-project-to-chat start --path /path/to/project --token YOUR_BOT_TOKEN --username your_telegram_username
Setup with config
# Add your Telegram username (defaults to executor role)
link-project-to-chat configure --add-user your_telegram_username
# Add a project
link-project-to-chat projects add --name myproject --path /path/to/project --token YOUR_BOT_TOKEN
# Start the bot
link-project-to-chat start
Web UI (alternative to Telegram)
Run the same project bot in your browser instead of Telegram. No bot token required.
pipx install "link-project-to-chat[web]"
link-project-to-chat start --project myproject --transport web --port 8080
# then open http://localhost:8080
The web UI is local-only — bind to 127.0.0.1 and never expose it to the public internet without your own auth in front, since the same /run shell-execution surface is exposed.
Web auth rate-limiting. POST /auth applies per-source backoff: after 5
bad tokens from the same source within a 5-minute window the endpoint returns
429 Too Many Requests with a Retry-After header; a successful login clears
the counter. If the web transport is deployed behind a reverse proxy, ensure the
proxy forwards the real client IP via X-Forwarded-For — without it, all
clients share one counter and one bad actor can lock out everyone.
Web bootstrap token. The one-time bootstrap token is printed to stdout
(operator console only) on startup; only a non-reversible sha256(token)[:12]
fingerprint appears in the application log. Never paste the raw token into a
log-aggregation system.
Windows secrets at rest. On Windows, config.json, the telethon session
file, and per-project meta_dir receive a current-user-only, inheritance-removed
DACL (via pywin32 SetNamedSecurityInfo, or an icacls fallback) at the same
choke points as the POSIX 0o600/0o700 chmod. Migration: a Windows
deployment where a second local account legitimately read the config must
re-grant access explicitly via icacls or run under a shared service account.
Google Chat transport
Google Chat bots are managed by the existing link-project-to-chat.service
manager unit. To add a Google Chat bot for a project:
- Create a Cloud Console Chat app on a GCP project and download the service-account JSON. One Chat app per GCP project is a Google constraint, so each project needs its own GCP project + Chat app + service-account JSON.
- In the manager Telegram bot, open the project view and tap
[Add Google Chat]. The wizard collects the SA path, port, public URL, and slash-command ID. - The wizard prints a ready-to-paste nginx vhost snippet. Deploy it on your
reverse proxy, run certbot for the new subdomain, then
sudo nginx -t && sudo systemctl reload nginx. - From the project's Google Chat space, DM the bot. The reply arrives via the manager-spawned google_chat subprocess on the configured port.
Per-project config lives under projects.<name>.google_chat. Operational
defaults (host, TTLs, byte caps, audience type) come from the top-level
google_chat block. Each project still requires its own GCP project +
Chat app + service-account JSON; one Chat app per GCP project is a Google
constraint.
rebuild.sh restarts the manager, which in turn restarts every supervised
google_chat subprocess — no separate link-project-to-chat-gchat-*.service
units anymore.
Google Chat v1.2 supports text, slash commands (/lp2c ...), card buttons with
HMAC-signed callbacks, thread-aware replies, attachment download
(uploaded-content, capped by attachment_max_bytes), prompt dialogs with
form-input submissions, and both endpoint_url and project_number audience
verification modes.
Known v1.2 limitations (carried forward from the v1 design spec):
- The HMAC secret for callback tokens is per-process (
secrets.token_bytes(32)at start). Any card or prompt posted before a bot restart becomes unverifiable afterward. Re-trigger the prompt on the user's next message. - Prompt submissions are space-bound by default. The transport supports
sender-binding via
expected_sender_native_id(open_promptkeyword), but bot.py wiring to thread the originating user through is a follow-up. - The duplicate-event cache is in-memory only; a restart resets the seen-event set, so a Google retry that arrives across a restart could double-dispatch.
- Native inline
REQUEST_DIALOG(where the bot returns a dialog synchronously from the HTTP route) is intentionally deferred because it conflicts with the fast-ack queue model. v1.2 uses card-button +SUBMIT_DIALOGinstead. - Outbound file and voice upload is deferred. Google Chat
media.uploadrequires user OAuth scopes, while this transport uses service-account app auth (chat.bot);send_fileandsend_voicereturn a thread-aware text fallback instead.
Google Chat — room awareness scope requirement. For the bot to receive
all space messages (not just mentions) on Google Chat spaces, the app must be
configured in the Google Chat API console to receive MESSAGE events for
all space messages. In the Google Cloud Console under "Google Chat
API > Configuration > App features", enable "Receive 1:1 messages" AND
"Join spaces and group conversations", and grant the bot the
https://www.googleapis.com/auth/chat.bot scope. If the app is only set up
to receive app-mention events, drive-by chatter will silently never reach
the bot, and room-awareness context will be empty.
Slack transport
Slack bots are managed by the existing link-project-to-chat.service
manager unit. Because Slack uses Socket Mode (an outbound persistent
WebSocket), no inbound HTTPS endpoint, port, or reverse-proxy snippet
is required — bots connect outward to Slack and receive events over
the same socket.
One-click install via app manifest (recommended). Operators with admin rights on their workspace can paste the bundled manifest:
- Go to api.slack.com/apps → Create New App → From an app manifest.
- Select the target workspace, then paste the contents of
docs/slack-app-manifest.yaml. Slack
pre-fills name, scopes, the
/lp2cslash command, Socket Mode toggle, and the event subscriptions. - After creating the app, install it to the workspace ("Install to
Workspace" button), accept the scope grants, and copy:
- the Bot User OAuth Token (starts with
xoxb-…) from the "OAuth & Permissions" page, and - an App-Level Token with
connections:writescope (starts withxapp-…) from the "Basic Information" → "App-Level Tokens" section. Create one if none exists.
- the Bot User OAuth Token (starts with
- In the manager Telegram bot, open
/botsand create a new bot with transport kind Slack. The wizard asks for a JSON credential blob containingbot_tokenandapp_token; it callsauth.testto validate the tokens, then persists the credentials and starts the supervised Slack subprocess. To tear down, open the bot detail in/botsand delete it. (The per-project/configure_slack//remove_slackwizard was removed in v5.0.0; manage Slack bots via the manager/botsUI —manager/bot_admin.py.) - From your Slack workspace, DM the bot OR
@mentionit in a channel where the bot is a member. Replies arrive via the manager-spawned slack subprocess over the Socket Mode connection.
Manual install walkthrough (operators who don't paste the manifest). The manifest path is preferred, but the following steps reproduce the same end state for operators who need to customize the app on the "Basic Information" / "OAuth & Permissions" / "Event Subscriptions" pages directly:
- Create a Slack app from scratch at api.slack.com/apps.
- Enable Socket Mode under "Settings → Socket Mode" and generate
an App-Level Token with the
connections:writescope. - Under OAuth & Permissions → Scopes → Bot Token Scopes, add the
following 15 scopes — these are the minimum set required by the
transport per spec §9.5:
app_mentions:readchat:writechannels:historychannels:readcommandsfiles:readfiles:writegroups:historygroups:readim:historyim:readmpim:historympim:readusers:readusers.profile:read
- Under Event Subscriptions → Subscribe to bot events, add:
message.im,app_mention,message.channels,message.groups,message.mpim,file_shared,member_joined_channel,member_left_channel. (Socket Mode does not require a Request URL.) - Under Slash Commands, register
/lp2c(description and usage hint are up to you). - Under Interactivity & Shortcuts, enable interactivity.
- Install the app to the workspace and copy the bot OAuth token
(
xoxb-…) and app-level token (xapp-…), then follow step 4 of the manifest path above to register the credentials via the manager/botsUI. (The per-project/configure_slackwizard was removed in v5.0.0.)
Per-project config lives under projects.<name>.slack:
{
"slack": {
"bot_token": "xoxb-...",
"app_token": "xapp-...",
"workspace_id": "T0123ABCD",
"channel_allowlist": []
}
}
Operational defaults (channel allowlist) come from the top-level
slack block when the project field is empty. channel_allowlist
empty = permissive (matches Telegram); non-empty gates channel
chatter + @-mentions but never DMs.
rebuild.sh restarts the manager, which in turn restarts every
supervised Slack subprocess — no separate
link-project-to-chat-slack-*.service units required.
The Slack transport supports:
- DMs (
message.im), addressed by-default. - Channel @-mentions (
app_mention) — drive-by channel chatter is also dispatched when the channel is on the allowlist, mirroring the Telegram room-binding behavior (see Group support below). - Slash commands (
/lp2c <subcommand> [args]) routed through the same handler table as Telegram/commands. - Modal-driven prompts for TEXT / SECRET / CHOICE / CONFIRM.
- Block Kit formatting with
**bold**/*italic*/[label](url)rewriting to Slack'smrkdwn. - File upload via
files_upload_v2, threaded to the originating mention. - Threading. Outbound replies inherit
thread_tsfrom the inbound mention; top-level replies stay top-level. - Identity namespacing.
Identity.transport_id = "slack:T_workspace_id"so two workspaces never collide viaAuthMixin.
Slack auth and identity locking. Auth flows through the immutable
slack:<workspace_id>:<user_id> lock key — a workspace member cannot
rename themselves to a privileged username and win the first-contact
role race. Username-only AllowedUser entries (e.g.
{"username": "alice", "role": "executor"}) are
spoofable-until-locked: the window closes the moment Alice's first
message atomically appends "slack:T0123ABCD:U9876XYZ" to her
locked_identities. To eliminate the window entirely, pre-author the
locked_identities field before Alice's first contact:
{"username": "alice", "role": "executor", "locked_identities": ["slack:T0123ABCD:U9876XYZ"]}
Known v1 limitations:
- No team-mode-on-Slack relay. Cross-bot team routing on a Slack
workspace is a v2 follow-up —
enable_team_relayis a no-op stub for now. Single-bot DMs + channels work end-to-end. - Stub for chat-member events.
on_chat_member_event,list_chat_admins, andget_chat_memberreturn empty / None pending Slackmember_joined_channelwiring (v2.1+).
Example session
You: what does the auth module do?
Agent: The auth module handles JWT token validation and...
You: /run npm run dev
(runs in background, check output with /tasks → Log)
You: add a test for expired token handling
Agent: I'll add a test for that. [edits file]...
You: /tasks
> #1 npm run dev
+ #2 [claude] add a test for expired token...
How it works
/run commands execute in parallel with agent work. Agent turns within a single bot session are serialized so they keep one consistent shared session and can't step on each other during interactive follow-ups.
Project bot commands
| Command | Description |
|---|---|
| (message) | Chat with the active backend in the project context |
/run <cmd> |
Run a shell command in the background |
/tasks |
List tasks with per-task buttons (log, cancel) |
/backend [claude|codex|gemini|mistral|grok|claude_sdk|openai] |
Show or switch the active backend |
/model [name] |
Set backend model |
/effort low/medium/high/xhigh/max |
Set backend reasoning depth |
/thinking on/off |
Stream live reasoning when the active backend supports it |
/context [on|off|N] |
Show or set per-chat conversation history depth |
/permissions <mode> |
Set backend permission / sandbox mode |
/skills |
List available skills |
/use [skill] |
Activate a skill (system prompt) — or pick from list |
/stop_skill |
Deactivate current skill |
/create_skill <name> |
Create a new skill (project or global) |
/delete_skill <name> |
Delete a skill |
/persona [name] |
Activate a persona (per-message) — or pick from list |
/stop_persona |
Deactivate current persona |
/create_persona <name> |
Create a new persona (project or global) |
/delete_persona <name> |
Delete a persona |
/voice |
Show voice transcription status |
/compact |
Compress session context |
/reset |
Clear the active backend session |
/status |
Show bot and backend status |
/version |
Show version |
/help |
Show available commands |
Claude remains the default backend. Codex is opt-in via /backend codex and supports /model, /effort, /permissions, session resume, and token usage reporting in /status. Some commands stay capability-gated: for example, live /thinking, /compact, skills, personas, and allowed/disallowed tool lists are Claude-only unless another backend exposes equivalent support.
Backends
-
Mistral (
/backend mistral, opt-in)- Install the Vibe CLI:
curl -LsSf https://mistral.ai/vibe/install.sh | bash - Set
MISTRAL_API_KEYin the bot's process env. - Default model:
mistral-medium-3.5. Other models:devstral-small,codestral-25.11(paid),mistral-large-latest(paid). - Permission/effort presets map to Vibe's
--agentflag (default,plan,accept-edits,auto-approve). - Session continuity via Vibe's
-cflag with VIBE_HOME isolation (per-project). - Known limitation (v2.0.8):
/model <id>updates in-memory state but does NOT auto-persist to<project>/.vibe/config.toml— operators must callMistralBackend.set_model()directly OR edit~/.vibe/config.tomlmanually. Fix planned for v2.0.9.
- Install the Vibe CLI:
-
Grok Build (
/backend grok, opt-in)- Install the Grok Build CLI:
curl -fsSL https://x.ai/cli/install.sh | bash - Set
GROK_CODE_XAI_API_KEYin the bot's process env (xAI Console → API Keys). Falls back to interactive OAuth creds in~/.grok/auth.jsonif the env var is unset. - Model:
grok-build(stable channel).grok modelsmay listgrok-build-latesttoo, but access depends on your xAI team's ACL — the picker ships only the universally-available stable slug. /permissionsaccepts Claude-shape values verbatim:default,acceptEdits,auto,dontAsk,bypassPermissions,plan. (dangerously-skip-permissionsaliases tobypassPermissions.)/effortacceptslow,medium,high,maxand is a SEPARATE orthogonal flag (--reasoning-effort) — it does NOT override/permissions.- Session continuity via stream-captured
session_id+--resume <id>per chat.
- Install the Grok Build CLI:
Skills
Skills are markdown files passed as system prompt via --append-system-prompt. Claude sees them as background context, like Claude Code's native skill handling. A skill and a persona can be active at the same time.
Locations (highest priority first):
- Per-project:
<project_path>/.claude/skills/<name>.md - App global:
~/.link-project-to-chat/skills/<name>.md - Claude Code user skills:
~/.claude/skills/<name>.md(including<name>/SKILL.mddirectories)
Higher-priority skills override lower ones with the same name. Claude Code user skills are automatically available.
Use /create_skill <name> to create — you'll choose project or global scope, then send the content. /use without arguments shows an inline picker.
Personas
Personas are markdown files prepended to every message. Claude sees them with each message, good for enforcing a specific voice or role.
Locations (highest priority first):
- Per-project:
<project_path>/.claude/personas/<name>.md - App global:
~/.link-project-to-chat/personas/<name>.md
Use /create_persona <name> to create. /persona without arguments shows an inline picker.
Example: Create ~/.link-project-to-chat/personas/reviewer.md:
You are a senior code reviewer. Focus on bugs, security issues,
and performance problems. Be direct and concise.
Then /persona reviewer to activate.
Voice messages
Send voice messages in Telegram and they'll be transcribed and sent to the active backend as text.
Setup with OpenAI Whisper API (recommended)
link-project-to-chat setup --stt-backend whisper-api --openai-api-key YOUR_KEY
Setup with local whisper.cpp
Requires whisper.cpp and ffmpeg installed:
link-project-to-chat setup --stt-backend whisper-cli --whisper-model base
Language hint
For better accuracy with non-English audio:
link-project-to-chat setup --whisper-language ka
Install with voice extra
pipx install "link-project-to-chat[voice]"
Multi-user support
Multiple Telegram users can access the same bot:
# Add users (defaults to executor; append `:viewer` for read-only role)
link-project-to-chat configure --add-user alice
link-project-to-chat configure --add-user bob:viewer
# Remove a user
link-project-to-chat configure --remove-user bob
Users can also be managed from the Manager Bot via /add_user and /remove_user.
Plugins
Plugins extend the project bot with custom commands, message handlers,
task hooks, button handlers, and Claude prompt context. They are external
Python packages discovered via the lptc.plugins entry point group, and
they're transport-portable: the same plugin works on Telegram, on the
Web UI, and on any future Discord/Slack/Google Chat transport.
Activating plugins for a project
Add them to the project's config entry:
{
"projects": {
"myproject": {
"path": "/path/to/project",
"telegram_bot_token": "...",
"plugins": [
{"name": "in-app-web-server"},
{"name": "diff-reviewer"}
]
}
}
}
Or toggle them in the manager bot: open a project → Plugins → tap a plugin. Restart the bot after changes.
Writing a plugin
from link_project_to_chat.plugin import Plugin, BotCommand
class MyPlugin(Plugin):
name = "my-plugin"
depends_on = []
async def start(self):
...
async def stop(self):
...
async def on_message(self, msg):
# msg is an IncomingMessage — text, sender, chat, files all available
return False # True consumes; the agent (Claude/Codex) is skipped
def get_context(self):
# Only used when the active backend is Claude; ignored for Codex/Gemini.
return "Extra system-prompt context"
def commands(self):
async def hello(invocation):
# invocation is a CommandInvocation
await self._ctx.transport.send_text(invocation.chat, "hi")
return [BotCommand(command="hello", description="say hi", handler=hello)]
Expose it via your plugin package's pyproject.toml:
[project.entry-points."lptc.plugins"]
my-plugin = "my_package:MyPlugin"
Role-based access
Set allowed_users on a project to enable per-user roles:
"allowed_users": [
{"username": "alice", "role": "executor"},
{"username": "bob", "role": "viewer"}
]
Viewers can use /tasks (including the per-task Log button it surfaces),
/status, /help, /version, /skills (listing), /context (display), and
any plugin command flagged viewer_ok. Executors have the full command set.
allowed_users is the sole auth source — an empty list means no one is
authorized (fail-closed). The first request from each user atomically appends
their identity to locked_identities and writes it back to the config so
subsequent requests validate by native ID rather than username (preserves the
username-spoof protection from the pre-v1.0 model).
Group support (Telegram)
As of v7.0.0, a bot answers in a Telegram room if and only if that room is
bound to it — a ChatBinding with chat_kind="room" stored in the bot
config. Binding is the opt-in; unbinding is the off-switch. The legacy
respond_in_groups per-project flag was retired v7.0.0 and is silently
dropped on next config save.
To bind a Telegram group to a bot, use the manager /bots UI: open the bot
detail, tap Bind room, and forward any message from the target group. The
bot will start answering in that room immediately — no restart required.
When a room is bound:
- The bot responds when
@<bot_username>appears in a group message OR the message replies to one of the bot's prior messages. - All other group messages are silently ignored — no auth check runs, nothing is logged.
- The
@<bot_username>mention is stripped from the prompt before the agent sees it. /commandswork in groups with the same role gate as DM. Telegram routes/cmd@MyBotonly to the addressed bot in multi-bot rooms.- The bot ignores all other bots' messages, including
@<bot_username>from a peer bot (loop defense).
Safety prompt (v1.2.0+)
By default, the bot is given a guardrail asking it to describe-and-ask
before destructive action — no kills, no restarts, no rm without
your explicit go-ahead in the current message. This is the same
guardrail that ships with the GitLab fork.
Per-project knobs (projects edit <name> safety_prompt <value>):
| Value | Effect |
|---|---|
default (or omit the key) |
Use the built-in guardrail |
Empty string "" |
Disable safety entirely — agent acts without asking |
| Any other string | Replace the built-in with your custom text |
CLI: link-project-to-chat projects add ... --safety-prompt "...".
Manager bot: the field appears in the project-edit text wizard.
Hot-reload of allowed_users (v1.2.0+)
When you /add_user, /remove_user, /promote_user, or
/demote_user from the manager bot, the running project bots pick up
the change within 5 s — no restart required. The reload is debounced
and clobber-safe; it never wipes in-flight first-contact identity
locks. Other config fields (token, model) still need a bot restart to apply.
Inter-message group context (v1.2.0+)
When the bot answers in a bound room, it sees the chatter that happened
between its previous LLM call and the current one — [Recent discussion]
is prepended to the prompt. Buffer is per-chat, 200-message ring, in-memory
only (cleared on restart). Works for any room-kind chat across transports.
The current local Web UI is direct browser-to-bot chat and emits DM-kind
messages, so room context does not apply there until a multi-user Web room
model exists.
meta_dir config field (v1.2.0+)
Top-level meta_dir in config.json redirects per-bot / per-plugin
storage to a different filesystem volume. Default is
~/.link-project-to-chat/meta/. Operators upgrading to v1.2.0+ don't
need to migrate existing data; the field is only consulted when bots
start.
Open-core extension contracts (v9.4.0)
The 9.4.0 release ships the additive open-core Stage A contract foundation: the public surfaces third-party extensions and future hosted tooling build against. It is purely additive — every existing adapter and command keeps working unchanged.
Extension descriptors & conformance kits
Extensions are declared as versioned descriptors in
link_project_to_chat.extensions and discovered through three entry-point
groups: lptc.plugins, lptc.backends, and lptc.transports. Discovery
is metadata-only — an extension your configuration doesn't select is
never imported — and descriptor validation or selection failures close
loudly (DescriptorError, DuplicateExtensionError) rather than falling
back to a silent guess.
Extension authors can run the same contract suites the first-party
adapters pass via the importable conformance kits in
link_project_to_chat.testing:
# In your extension's test module:
from link_project_to_chat.testing.transport_contract import (
make_transport_contract_tests,
)
globals().update(make_transport_contract_tests({"my_transport": MyTransport}))
Equivalent factories exist for backends (make_backend_contract_tests)
and plugins (make_plugin_contract_tests). The kits never import a
private adapter — all adapter-specific construction stays in the consuming
repository.
Generic transport options
BotConfig.transport_options is a generic per-transport options dict, so
new transports no longer need a dedicated config field for every knob.
Existing flat credential blocks (slack, google_chat, web, Telegram
token fields) keep loading byte-for-byte via legacy dual-read.
Atomic onboarding
link-project-to-chat init, link-project-to-chat link, and
link-project-to-chat projects add share one core-owned atomic
registration API (config/registration.py): validate, mutate, then
fsync-replace config.json in one step — concurrent or interrupted
onboarding can't leave a torn config.
Config schema-version refusal
Every config.json writer checks the on-disk schema_version before
mutating and refuses to write a file stamped with a future or invalid
version (produced by a newer build or hand edit), instead of silently
downgrading it. Successful writes stamp the supported version.
Per-bot SQLite versioning
Each bot's SQLite database carries a PRAGMA user_version schema marker.
The build understands the newest SQLITE_SCHEMA_VERSION and applies N/N-1
migrations atomically (migration + version stamp in one transaction); a
database stamped by a newer build refuses to open instead of risking
schema drift.
Dormant bot supervision
link_project_to_chat.supervision defines a public BotSupervisor
protocol keyed strictly by bot name — team processes, the relay service,
and project workloads are deliberately out of its namespace. The manager's
bot lifecycle flows through this seam (ProcessManager accepts an injected
supervisor; the default is subprocess-based). Stage A ships a dormant
SystemdBotSupervisor implementation plus an inert systemd unit template
for operator opt-in only: nothing installs, enables, or starts any unit.
The template is package data inside the wheel — locate it via
importlib.resources.files("link_project_to_chat") / "packaging" / "systemd" / "link-project-to-chat-bot@.service" (in a source checkout:
src/link_project_to_chat/packaging/systemd/).
Manager bot
The manager bot controls multiple project bots from a single Telegram chat — start, stop, view logs, add/remove projects, and create new projects automatically.
Manager-started bots inherit the active --config path, so custom config files work consistently for project bots, team bots, personas, and team relay bootstrap.
Setup
link-project-to-chat configure --add-user your_telegram_username --manager-token MANAGER_TOKEN
link-project-to-chat start-manager
Manager commands
| Command | Description |
|---|---|
/projects |
List projects and project actions |
/bots |
List bots and their chat/project bindings |
/backends [backend] |
Show backend install/version/auth status and setup actions |
/start_all |
Start all projects |
/stop_all |
Stop all projects |
/create_project |
Create a project (GitHub or GitLab repo + auto bot creation) |
/create_team |
Create a multi-bot team in a group chat (interactive wizard; picks GitHub or GitLab) |
/delete_team |
Delete a team and clean up its bots |
/setup |
Configure GitHub PAT, GitLab PAT/host, and Telegram API credentials |
/users |
List authorized users |
/add_user <username> |
Add an authorized user |
/remove_user <username> |
Remove an authorized user |
/models [backend] |
List the model catalog |
/version |
Show version |
/help |
Show commands |
Automated project creation
The /create_project command automates the full project setup:
- Pick a repo provider (GitHub or GitLab)
- Select a repo (browse your repos or paste a URL)
- Automatically create a Telegram bot via BotFather
- Clone the repo
- Configure everything
/create_team opens with the same GitHub / GitLab picker before the
browse + paste-URL + clone steps. Self-hosted GitLab is supported via
Config.gitlab_host (default gitlab.com).
When you later remove a project from the manager, manager-created projects are cleaned up on a best-effort basis: the manager will try to stop the bot, delete the owned repo checkout, and revoke the owned bot via BotFather. If the project path was changed after creation, the repo is left in place intentionally.
Requirements:
ghCLI (optional, for GitHub repo browse/clone in the manager wizard) —gh auth login. Alternatively set a GitHub PAT via/setup.glabCLI (optional, for GitLab repo browse/clone) —glab auth login. For a self-hosted GitLab instance, also setgitlab_hostin your config:link-project-to-chat configure --gitlab-host gitlab.example.com(bare hostname, no scheme). Alternatively set a GitLab PAT vialink-project-to-chat configure --gitlab-pat ...or/setup.- BotFather automation: Telegram API credentials + Telethon session
- Team creation/deletion automation: install the optional create extras with
pipx install "link-project-to-chat[create]"
Setup credentials:
# Interactive setup (recommended)
link-project-to-chat setup
# Or via the Manager Bot
/setup
The CLI setup command handles Telethon phone authentication interactively — this cannot be done through the bot due to Telegram security restrictions.
CLI reference
link-project-to-chat configure --add-user USER[:ROLE] [--remove-user USER]
[--reset-user-identity USER[:TRANSPORT]]
[--manager-token TOKEN]
[--gitlab-pat PAT] [--gitlab-host HOST]
link-project-to-chat setup [--github-pat PAT] [--gitlab-pat PAT] [--gitlab-host HOST]
[--telegram-api-id ID] [--telegram-api-hash HASH] [--phone PHONE]
link-project-to-chat projects list
link-project-to-chat projects add --name NAME --path PATH --token TOKEN [--username USER] [--model MODEL]
link-project-to-chat projects remove <name>
link-project-to-chat projects edit <name> <field> <value>
link-project-to-chat start [--project NAME] [--path PATH --token TOKEN] [--username USER] [--model MODEL]
[--permission-mode MODE] [--dangerously-skip-permissions]
[--allowed-tools TOOLS] [--disallowed-tools TOOLS]
[--transport telegram|web|google_chat] [--port PORT]
[--google-chat-host HOST] [--google-chat-port PORT]
[--google-chat-public-url URL]
link-project-to-chat start-manager
Config is stored at ~/.link-project-to-chat/config.json.
Deployment
Run as a systemd service
sudo tee /etc/systemd/system/link-project-to-chat.service << 'EOF'
[Unit]
Description=Link Project to Chat Manager Bot
After=network.target
[Service]
User=botuser
ExecStart=/home/botuser/.local/bin/link-project-to-chat start-manager
Restart=always
RestartSec=5
Environment=HOME=/home/botuser
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable --now link-project-to-chat
History retention & maintenance
By default, no chat history is ever deleted — the database grows unboundedly. To
enable automatic pruning, set history_retention_days on a project in
config.json:
{
"projects": {
"myproject": {
"history_retention_days": 90
}
}
}
When history_retention_days is a positive integer the bot starts an opt-in
maintenance scheduler (default cadence: once every 24 hours, off-peak). Each run
executes three steps in order:
- prune — delete rows older than
history_retention_daysfrom the chat history and conversation-log stores. - checkpoint — truncate the SQLite WAL file (cheap; frees disk without rewriting the database).
- vacuum (optional) — run
VACUUMto compact the database file and return disk space to the OS. Disabled by default because it rewrites the whole file; enable by also setting"history_vacuum_on_maintenance": true.
Caution: Setting
history_retention_daystoo low permanently loses messages older than the threshold — the prune is irreversible. Choose a value that balances storage costs against how far back you need anaphoric context ("can you do it again?") to reach.
Back up and restore bot databases
Use SQLite's sqlite3.Connection.backup online backup API while a bot is
running:
import sqlite3
with sqlite3.connect("bot.db") as source, sqlite3.connect("bot.backup.db") as target:
source.backup(target)
Do not copy only a live .db file: committed data may still be in its -wal
sidecar. Stop every bot that uses the database before restoring, copy the backup
into the configured database path, then verify PRAGMA integrity_check and
PRAGMA user_version before restarting.
Roadmap
Shipped:
- Telegram transport
- Local Web UI transport (FastAPI + SSE + SQLite)
- Voice messages (Whisper API or local whisper.cpp)
- Multi-project manager bot, team chats, group/team relay
- Skills and personas
- Pluggable agent backend with
/backendcommand and capability-gated commands. Seven backends ship: Claude (default), Codex, OpenAI API, Gemini, Mistral, Grok (added v7.x), and Claude SDK (opt-in via/backend claude_sdkwhen theclaude-sdkextra is installed); all selectable via/backend. - Provider-aware
/status,/contextcross-backend conversation history, Codex/model, Codex/effort, and Codex/permissions. - Slack transport (Socket Mode, v2.0.22)
- Google Chat transport (service-account app auth, spec #4)
Planned (designed, not yet implemented):
- Discord transport
Contributions welcome.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file link_project_to_chat-9.4.1.tar.gz.
File metadata
- Download URL: link_project_to_chat-9.4.1.tar.gz
- Upload date:
- Size: 3.3 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
596b90e347307fb51e0dd8fb33cadd52b534a3e790c9b3f35f3a93f84d38b966
|
|
| MD5 |
4b65ad92c2c40b3a16b3d32814e5a124
|
|
| BLAKE2b-256 |
7f9b8ed182864251ec56f3031cece4debf61b6a8fe6504977cbbe18ba76e4247
|
File details
Details for the file link_project_to_chat-9.4.1-py3-none-any.whl.
File metadata
- Download URL: link_project_to_chat-9.4.1-py3-none-any.whl
- Upload date:
- Size: 587.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b6709efdbdb96b45aa5ae5e8742b94b3746a8d4a6fffe76c5cb3742de8a05d3
|
|
| MD5 |
518bc89c9a93100013fc381775cc1de9
|
|
| BLAKE2b-256 |
e94a3992b1cb18c1254297b1a607ffc23dcde6bd12d3d8ea003f6d0feee02cc2
|