Metadata-Version: 2.4
Name: link-project-to-chat
Version: 9.4.1
Summary: Link a project directory to a Telegram bot that chats with Claude
Project-URL: Homepage, https://github.com/rimnadze/link-project-to-chat
Project-URL: Repository, https://github.com/rimnadze/link-project-to-chat
Project-URL: Issues, https://github.com/rimnadze/link-project-to-chat/issues
Author-email: Rezo Imnadze <rezo@imnadze.ge>
License-Expression: MIT
License-File: LICENSE
Keywords: ai,bot,claude,claude-code,telegram
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications :: Chat
Classifier: Topic :: Software Development
Requires-Python: >=3.11
Requires-Dist: click>=8.0
Requires-Dist: packaging>=23.0
Requires-Dist: python-telegram-bot>=22.0
Provides-Extra: all
Requires-Dist: aiosqlite>=0.19; extra == 'all'
Requires-Dist: claude-agent-sdk>=0.2.87; extra == 'all'
Requires-Dist: fastapi[standard]>=0.111; extra == 'all'
Requires-Dist: google-auth>=2.0; extra == 'all'
Requires-Dist: httpx>=0.27; extra == 'all'
Requires-Dist: jinja2>=3.1; extra == 'all'
Requires-Dist: openai>=1.30; extra == 'all'
Requires-Dist: slack-bolt>=1.18; extra == 'all'
Requires-Dist: slack-sdk>=3.27; extra == 'all'
Requires-Dist: telethon>=1.36; extra == 'all'
Provides-Extra: claude-sdk
Requires-Dist: claude-agent-sdk>=0.2.87; extra == 'claude-sdk'
Provides-Extra: create
Requires-Dist: httpx>=0.27; extra == 'create'
Requires-Dist: telethon>=1.36; extra == 'create'
Provides-Extra: dev
Requires-Dist: diff-cover>=9.0; extra == 'dev'
Requires-Dist: mypy==2.1.0; extra == 'dev'
Requires-Dist: pip-audit>=2.7; extra == 'dev'
Requires-Dist: pip-tools>=7.4; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-benchmark>=4.0; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: ruff==0.15.12; extra == 'dev'
Provides-Extra: google-chat
Requires-Dist: fastapi[standard]>=0.111; extra == 'google-chat'
Requires-Dist: google-auth>=2.0; extra == 'google-chat'
Requires-Dist: httpx>=0.27; extra == 'google-chat'
Provides-Extra: openai-api
Requires-Dist: openai>=1.30; extra == 'openai-api'
Provides-Extra: slack
Requires-Dist: slack-bolt>=1.18; extra == 'slack'
Requires-Dist: slack-sdk>=3.27; extra == 'slack'
Provides-Extra: voice
Requires-Dist: openai>=1.30; extra == 'voice'
Provides-Extra: web
Requires-Dist: aiosqlite>=0.19; extra == 'web'
Requires-Dist: fastapi[standard]>=0.111; extra == 'web'
Requires-Dist: jinja2>=3.1; extra == 'web'
Provides-Extra: whatsapp
Requires-Dist: neonize>=0.4.1; extra == 'whatsapp'
Description-Content-Type: text/markdown

# 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](https://claude.ai/code) installed and authenticated (`claude` on PATH) for the default backend; optional Codex CLI (`codex` on PATH), OpenAI API key (`OPENAI_API_KEY` with `[openai-api]`), Gemini CLI, Mistral Vibe CLI, Grok Build CLI, or Claude SDK extra for alternate `/backend` choices
- For Telegram: a Telegram bot token — create a bot via [@BotFather](https://t.me/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](docs/slack-app-manifest.yaml) and the "Slack transport" section below

## Install

```bash
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:

```bash
pipx install "link-project-to-chat[all]"
```

### Contributor / from-source install

```bash
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]`.

```bash
link-project-to-chat start --path /path/to/project --token YOUR_BOT_TOKEN --username your_telegram_username
```

## Setup with config

```bash
# 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.

```bash
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:

1. 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.
2. 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.
3. 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`.
4. 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_prompt` keyword), 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_DIALOG` instead.
- Outbound file and voice upload is deferred. Google Chat `media.upload`
  requires user OAuth scopes, while this transport uses service-account app
  auth (`chat.bot`); `send_file` and `send_voice` return 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:

1. Go to [api.slack.com/apps](https://api.slack.com/apps) → **Create
   New App** → **From an app manifest**.
2. Select the target workspace, then paste the contents of
   [docs/slack-app-manifest.yaml](docs/slack-app-manifest.yaml). Slack
   pre-fills name, scopes, the `/lp2c` slash command, Socket Mode
   toggle, and the event subscriptions.
3. 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:write` scope (starts
     with `xapp-…`) from the "Basic Information" → "App-Level Tokens"
     section. Create one if none exists.
4. In the manager Telegram bot, open `/bots` and create a new bot with
   transport kind **Slack**. The wizard asks for a JSON credential blob
   containing `bot_token` and `app_token`; it calls `auth.test` to
   validate the tokens, then persists the credentials and starts the
   supervised Slack subprocess. To tear down, open the bot detail in
   `/bots` and delete it. (The per-project `/configure_slack` /
   `/remove_slack` wizard was **removed in v5.0.0**; manage Slack bots
   via the manager `/bots` UI — `manager/bot_admin.py`.)
5. From your Slack workspace, DM the bot OR `@mention` it 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:

1. Create a Slack app from scratch at
   [api.slack.com/apps](https://api.slack.com/apps).
2. Enable **Socket Mode** under "Settings → Socket Mode" and generate
   an **App-Level Token** with the `connections:write` scope.
3. 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:read`
   - `chat:write`
   - `channels:history`
   - `channels:read`
   - `commands`
   - `files:read`
   - `files:write`
   - `groups:history`
   - `groups:read`
   - `im:history`
   - `im:read`
   - `mpim:history`
   - `mpim:read`
   - `users:read`
   - `users.profile:read`
4. 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.)
5. Under **Slash Commands**, register `/lp2c` (description and usage
   hint are up to you).
6. Under **Interactivity & Shortcuts**, enable interactivity.
7. 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
   `/bots` UI. (The per-project `/configure_slack` wizard was
   **removed in v5.0.0**.)

**Per-project config** lives under `projects.<name>.slack`:

```json
{
  "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's `mrkdwn`.
- **File upload** via `files_upload_v2`, threaded to the originating
  mention.
- **Threading.** Outbound replies inherit `thread_ts` from the
  inbound mention; top-level replies stay top-level.
- **Identity namespacing.** `Identity.transport_id =
  "slack:T_workspace_id"` so two workspaces never collide via
  `AuthMixin`.

**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:

```json
{"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_relay` is 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`, and `get_chat_member` return empty / None
  pending Slack `member_joined_channel` wiring (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_KEY` in 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 `--agent` flag (`default`, `plan`, `accept-edits`, `auto-approve`).
  - Session continuity via Vibe's `-c` flag 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 call `MistralBackend.set_model()` directly OR edit `~/.vibe/config.toml` manually. Fix planned for v2.0.9.

- **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_KEY` in the bot's process env (xAI Console → API Keys).
    Falls back to interactive OAuth creds in `~/.grok/auth.json` if the env var is unset.
  - Model: `grok-build` (stable channel). `grok models` may list `grok-build-latest`
    too, but access depends on your xAI team's ACL — the picker ships only the
    universally-available stable slug.
  - `/permissions` accepts Claude-shape values verbatim: `default`, `acceptEdits`,
    `auto`, `dontAsk`, `bypassPermissions`, `plan`. (`dangerously-skip-permissions`
    aliases to `bypassPermissions`.)
  - `/effort` accepts `low`, `medium`, `high`, `max` and is a SEPARATE orthogonal
    flag (`--reasoning-effort`) — it does NOT override `/permissions`.
  - Session continuity via stream-captured `session_id` + `--resume <id>` per chat.

## 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):**
1. Per-project: `<project_path>/.claude/skills/<name>.md`
2. App global: `~/.link-project-to-chat/skills/<name>.md`
3. Claude Code user skills: `~/.claude/skills/<name>.md` (including `<name>/SKILL.md` directories)

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):**
1. Per-project: `<project_path>/.claude/personas/<name>.md`
2. 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`:
```markdown
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)

```bash
link-project-to-chat setup --stt-backend whisper-api --openai-api-key YOUR_KEY
```

### Setup with local whisper.cpp

Requires [whisper.cpp](https://github.com/ggerganov/whisper.cpp) and `ffmpeg` installed:

```bash
link-project-to-chat setup --stt-backend whisper-cli --whisper-model base
```

### Language hint

For better accuracy with non-English audio:

```bash
link-project-to-chat setup --whisper-language ka
```

### Install with voice extra

```bash
pipx install "link-project-to-chat[voice]"
```

## Multi-user support

Multiple Telegram users can access the same bot:

```bash
# 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:

```json
{
  "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

```python
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`:

```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:

```json
"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.
- `/commands` work in groups with the same role gate as DM. Telegram
  routes `/cmd@MyBot` only 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`:

```python
# 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

```bash
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:

1. Pick a repo provider (GitHub or GitLab)
2. Select a repo (browse your repos or paste a URL)
3. Automatically create a Telegram bot via BotFather
4. Clone the repo
5. 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:**
- **`gh` CLI** (optional, for GitHub repo browse/clone in the manager wizard) — `gh auth login`. Alternatively set a GitHub PAT via `/setup`.
- **`glab` CLI** (optional, for GitLab repo browse/clone) — `glab auth login`. For a self-hosted GitLab instance, also set `gitlab_host` in your config: `link-project-to-chat configure --gitlab-host gitlab.example.com` (bare hostname, no scheme). Alternatively set a GitLab PAT via `link-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:**

```bash
# 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

```bash
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`:

```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:

1. **prune** — delete rows older than `history_retention_days` from the chat
   history and conversation-log stores.
2. **checkpoint** — truncate the SQLite WAL file (cheap; frees disk without
   rewriting the database).
3. **vacuum** (optional) — run `VACUUM` to 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_days` too 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:

```python
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 `/backend` command 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_sdk` when the `claude-sdk` extra is installed); all selectable via `/backend`.
- Provider-aware `/status`, `/context` cross-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
