Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read
Expand All @@ -20,14 +20,14 @@ jobs:
strategy:
fail-fast: false
matrix:
# package engines: node >=20; cover LTS current + previous
node: [20, 22]
# package engines: node >=20
node: [20, 22, 24]
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}
cache: npm
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2026 HenryYannis
Copyright (c) 2026 程小宝 (Henry Yannis)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
131 changes: 131 additions & 0 deletions README.en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
[简体中文](README.md) | [繁體中文](README.zh-Hant.md) | [English](README.en.md)

# opencode-telegram-remote

用 Telegram 在手机上遥控 OpenCode,电脑端的回复实时推送到手机。
A Telegram remote control for [OpenCode](https://opencode.ai) V2: chat with your sessions from your phone and get desktop replies pushed back in real time.

[![CI](https://github.com/HenryYannis/opencode-telegram-remote/actions/workflows/ci.yml/badge.svg)](https://github.com/HenryYannis/opencode-telegram-remote/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

## Features

- **Native OpenCode V2 plugin.** Built with `Plugin.define` from `@opencode/plugin` (plugin id `telegram`). It runs inside the OpenCode server and starts and stops with it; there is no separate bridge process to keep alive.
- **Chat from your phone.** Plain text is sent as a prompt to the session your chat is bound to. The bot shows "typing…" while OpenCode works, then sends the reply back.
- **Desktop replies pushed to your phone.** The plugin listens to OpenCode's `session.text.ended` events, so replies produced in the desktop window are sent to every chat bound to that session. A delivery tracker makes sure the same reply is not sent twice.
- **`/current`** attaches your chat to the session you last viewed on the desktop, so you can switch between computer and phone in the same conversation.
- **Switch models** with `/model` (tap a button) or `/model <keyword>`.
- **Switch or create sessions** with `/sessions` and `/new`; stop a running task with `/abort`.
- **Permission prompts on your phone.** When OpenCode asks for permission in a bound session, you get the request with **Allow once / Always / Reject** buttons.
- **Allow-list.** Only the Telegram user IDs you list can use the bot; everyone else gets "Unauthorized".

## Requirements

- OpenCode V2 (the plugin is built against `@opencode/plugin` 2.2).
- Node.js 20 or newer (`engines.node` in `package.json`).
- A Telegram bot token from [@BotFather](https://t.me/BotFather) and your numeric Telegram user ID.

## Install

The package is not published to npm yet. Install it from GitHub with the OpenCode CLI:

```sh
opencode plugin add github:HenryYannis/opencode-telegram-remote
```

Or clone it and load it as a local plugin directory:

```sh
git clone https://github.com/HenryYannis/opencode-telegram-remote.git
cd opencode-telegram-remote
npm install
```

Then add the absolute path of the cloned folder to `~/.config/opencode/opencode.json(c)`:

```jsonc
{
"plugins": ["/absolute/path/to/opencode-telegram-remote"]
}
```

## Configuration

Create `~/.config/opencode/telegram.json`:

```json
{
"token": "YOUR_BOT_TOKEN",
"allowedUsers": [123456789]
}
```

| Key | Required | Description |
| :--- | :--- | :--- |
| `token` | yes | Bot token from [@BotFather](https://t.me/BotFather) (`/newbot`). |
| `allowedUsers` | yes, non-empty | Numeric Telegram user IDs allowed to use the bot. Get yours from [@userinfobot](https://t.me/userinfobot). |

- No chat ID is needed: the bot answers any chat where the sender is on the allow-list. A private chat with your bot is the simplest setup.
- If the file is missing, `token` is empty, or `allowedUsers` is empty, the plugin does not start the bot (OpenCode keeps running normally).
- ⚠️ Anyone on the allow-list can run prompts on your machine through OpenCode. Only add your own ID, and keep `telegram.json` private.
- On Windows the path is `%USERPROFILE%\.config\opencode\telegram.json`.

Optional environment variable:

| Variable | Effect |
| :--- | :--- |
| `TELEGRAM_PLUGIN_DEBUG` | When set, writes a debug log to `~/.config/opencode/telegram-plugin.log`. |

## Commands

| Command | Action |
| :--- | :--- |
| *(plain text)* | Send a prompt to the bound session |
| `/current` | Attach to the session you last viewed on the desktop |
| `/model` | List models as buttons; `/model <keyword>` switches to the first model whose ID contains the keyword |
| `/sessions` | List the 10 most recent sessions, with buttons to switch |
| `/new` | Create a new session and bind to it |
| `/abort` | Stop the task running in the bound session |
| `/status` | Show the working directory, bound session and last viewed session |
| `/start` | Show help |

**Which session does plain text go to?** The session your chat is bound to. If there is none yet, the plugin uses the session you last viewed on the desktop, then the most recently updated session, and otherwise creates a new "Telegram remote session".

**Where does the model list come from?** From OpenCode's model list (`ctx.model.list()`). If that returns nothing, it falls back to the model keys under `providers.<id>.models` in `~/.config/opencode/opencode.json`.

## How desktop → phone sync works

```
A reply finishes in the desktop window
↓ OpenCode emits session.text.ended (sessionID, assistantMessageID, ordinal, text)
The plugin finds the chats bound to that session
↓ splits long text into chunks of up to 3800 characters
bot.api.sendMessage → your phone
```

Replies can arrive two ways: the event stream, and a fallback that reads the session after `session.wait()` when you sent the prompt from Telegram. Both paths share one delivery tracker (keyed by message ID and text segment), so each reply is delivered once.

## Limits

- Text only; photos and voice messages are not supported yet.
- One bot per machine. OpenCode loads the plugin once per location; a lock file (`~/.config/opencode/telegram-plugin.lock`) makes sure only one instance polls Telegram.
- Chat-to-session bindings live in memory and are cleared when OpenCode restarts. Send `/current` or `/sessions` to bind again.

## FAQ

**The bot does not answer.**
Check that `telegram.json` is valid JSON with a `token` and a non-empty `allowedUsers`, and that your ID is in the list. Set `TELEGRAM_PLUGIN_DEBUG=1`, restart OpenCode, and read `~/.config/opencode/telegram-plugin.log`.

**How do I turn the plugin off without uninstalling it?**
Add `"-telegram"` to `plugins` in `opencode.json(c)`, or remove `telegram.json`.

## Development

```sh
npm ci
npm run ci # syntax check, unit tests, package validation, smoke import, pack dry-run
```

## License

[MIT](LICENSE) © 2026 Henry Yannis
156 changes: 90 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,107 +1,131 @@
# ⚡️ opencode-telegram-remote
[简体中文](README.md) | [繁體中文](README.zh-Hant.md) | [English](README.en.md)

**A Telegram remote control for [OpenCode](https://opencode.ai) — chat with any window from your phone, and get desktop replies pushed back in real time.**
# opencode-telegram-remote

> 把手机变成 OpenCode 的遥控器:无缝接管电脑窗口、双向实时同步、手机上切换模型和会话。
用 Telegram 在手机上遥控 OpenCode,电脑端的回复实时推送到手机。
A Telegram remote control for [OpenCode](https://opencode.ai) V2: chat with your sessions from your phone and get desktop replies pushed back in real time.

[中文说明见下方](#-中文说明)
[![CI](https://github.com/HenryYannis/opencode-telegram-remote/actions/workflows/ci.yml/badge.svg)](https://github.com/HenryYannis/opencode-telegram-remote/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

---
## 功能

## Why this plugin?
- **原生 OpenCode V2 插件**:基于 `@opencode/plugin` 的 `Plugin.define` 编写(插件 id 为 `telegram`),运行在 OpenCode 服务进程内,随 OpenCode 启停,不需要额外的常驻桥接进程。
- **手机发消息**:普通文本会作为提示词发送到当前聊天绑定的会话。OpenCode 处理期间机器人显示“正在输入…”,完成后把回复发回手机。
- **电脑端回复推送到手机**:插件监听 OpenCode 的 `session.text.ended` 事件,电脑窗口里产生的回复会发送到所有绑定该会话的聊天;内置去重,同一条回复不会发两次。
- **`/current`**:把手机接入你在电脑上最近查看的会话,电脑和手机可以在同一个对话里交替使用。
- **切换模型**:`/model` 点按钮切换,或用 `/model <关键词>`。
- **切换或新建会话**:`/sessions`、`/new`;用 `/abort` 中止正在运行的任务。
- **在手机上处理权限请求**:已绑定会话里 OpenCode 请求权限时,会收到带 **Allow once / Always / Reject** 按钮的消息。
- **白名单**:只有你列出的 Telegram 用户 ID 能使用机器人,其他人会收到 "Unauthorized"。

Other Telegram integrations for OpenCode are either **standalone bridge processes** or **plugins built for the V1 API that no longer load on OpenCode V2**. This plugin is a **native OpenCode V2 plugin** (`Plugin.define` + plugin context APIs) and adds the one thing nobody else has: **true bidirectional sync between the desktop window and your phone.**
## 环境要求

| Capability | This plugin | Typical bridge |
| :--- | :--- | :--- |
| Install as an OpenCode plugin (no extra process) | ✅ | ❌ separate Node process |
| Chat from phone → executed in OpenCode | ✅ | ✅ |
| **Desktop replies auto-pushed to phone** | ✅ real-time via event stream | mostly ❌ |
| `/current` — attach phone to the desktop's *current* window | ✅ | ❌ |
| Switch AI models from phone (`/model` + buttons) | ✅ | varies |
| Switch / create sessions from phone | ✅ | varies |
| Lifecycle bound to OpenCode itself | ✅ | ❌ |
- OpenCode V2(插件基于 `@opencode/plugin` 2.2 开发)。
- Node.js 20 及以上(见 `package.json` 的 `engines.node`)。
- 从 [@BotFather](https://t.me/BotFather) 获取的机器人 token,以及你的 Telegram 数字用户 ID。

## Install
## 安装

Requires **OpenCode V2** (2.2+). In `~/.config/opencode/opencode.json(c)`:
本包暂未发布到 npm。可以用 OpenCode CLI 直接从 GitHub 安装:

```jsonc
{
"plugins": ["opencode-telegram-remote"]
}
```sh
opencode plugin add github:HenryYannis/opencode-telegram-remote
```

or install from Git:
或者克隆到本地,作为本地插件目录加载:

```sh
opencode plugin add github:<you>/opencode-telegram-remote
git clone https://github.com/HenryYannis/opencode-telegram-remote.git
cd opencode-telegram-remote
npm install
```

## Configure
然后在 `~/.config/opencode/opencode.json(c)` 里加入克隆目录的绝对路径:

```jsonc
{
"plugins": ["/absolute/path/to/opencode-telegram-remote"]
}
```

Create `~/.config/opencode/telegram.json`:
## 配置

创建 `~/.config/opencode/telegram.json`:

```json
{
"token": "<your-bot-token-from-BotFather>",
"allowedUsers": [<your-numeric-telegram-user-id>]
"token": "YOUR_BOT_TOKEN",
"allowedUsers": [123456789]
}
```

- Get the token from [@BotFather](https://t.me/BotFather) (`/newbot`).
- Get your user ID from [@userinfobot](https://t.me/userinfobot).
- ⚠️ **Keep `allowedUsers` filled** — anyone not in the list is rejected. You are handing out shell-level access to your machine.
| 字段 | 是否必填 | 说明 |
| :--- | :--- | :--- |
| `token` | 必填 | 在 [@BotFather](https://t.me/BotFather) 用 `/newbot` 创建机器人后得到的 token。 |
| `allowedUsers` | 必填,不能为空 | 允许使用机器人的 Telegram 数字用户 ID 列表。可通过 [@userinfobot](https://t.me/userinfobot) 查看自己的 ID。 |

- 不需要填 chat ID:只要发送者在白名单里,机器人就会响应。最简单的用法是和你的机器人私聊。
- 如果文件不存在、`token` 为空或 `allowedUsers` 为空,插件不会启动机器人,OpenCode 照常运行。
- ⚠️ 白名单里的人可以通过 OpenCode 在你的电脑上执行任务。只填你自己的 ID,并妥善保管 `telegram.json`。
- Windows 下路径为 `%USERPROFILE%\.config\opencode\telegram.json`。

## Commands
可选环境变量:

| Command | Action |
| 变量 | 作用 |
| :--- | :--- |
| *(plain text)* | Send a prompt to the bound session |
| `/current` | Attach to the desktop's current active window |
| `/model` | List models (tappable buttons); `/model <keyword>` fuzzy-switch |
| `/sessions` | List recent sessions, switch with inline buttons |
| `/new` | Create a new session |
| `/abort` | Interrupt the running task |
| `/status` | Show bound session & working directory |
| `/start` | Help |
| `TELEGRAM_PLUGIN_DEBUG` | 设置后会把调试日志写入 `~/.config/opencode/telegram-plugin.log`。 |

## How the desktop→phone sync works
## 命令

| 命令 | 作用 |
| :--- | :--- |
| *(普通文本)* | 发送提示词到已绑定的会话 |
| `/current` | 接入你在电脑上最近查看的会话 |
| `/model` | 以按钮列出模型;`/model <关键词>` 切换到第一个 ID 包含该关键词的模型 |
| `/sessions` | 列出最近 10 个会话,点按钮切换 |
| `/new` | 新建会话并绑定 |
| `/abort` | 中止已绑定会话中正在运行的任务 |
| `/status` | 显示工作目录、已绑定会话和最近查看的会话 |
| `/start` | 显示帮助 |

The plugin subscribes to OpenCode's server event stream via `ctx.event.subscribe`:
**普通文本发到哪个会话?** 发到当前聊天绑定的会话。如果还没有绑定,依次使用:你在电脑上最近查看的会话 → 最近更新的会话 → 新建一个 "Telegram remote session"。

**模型列表从哪来?** 来自 OpenCode 的模型列表(`ctx.model.list()`)。如果为空,再读取 `~/.config/opencode/opencode.json` 中 `providers.<id>.models` 下的模型名。

## 电脑 → 手机同步原理

```
Desktop reply finished
↓ OpenCode broadcasts
session.text.ended (sessionID + assistantMessageID)
↓ plugin looks up chats bound to that session
fetches the final message text → splits to ≤4096 chars
↓
bot.api.sendMessage → your phone
电脑窗口中一条回复结束
↓ OpenCode 发出 session.text.ended(sessionID、assistantMessageID、ordinal、text)
插件找到绑定该会话的聊天
↓ 长文本按最多 3800 个字符分段
bot.api.sendMessage → 你的手机
```

A dedupe registry (`sentMessageIds`) guarantees the same assistant message is never delivered twice across the two delivery paths (event push + fallback fetch after `wait()`).
回复有两条送达路径:事件流;以及从 Telegram 发起提示时,在 `session.wait()` 之后读取会话的兜底路径。两条路径共用一个投递记录(按消息 ID 和文本段去重),所以每条回复只发一次。

## Notes & limits
## 限制

- Text only (no photos/voice yet — PRs welcome).
- One bot per machine (singleton lock); multi-location OpenCode loads are de-duplicated automatically.
- The plugin follows your `opencode.json` providers for `/model`; if parsing config fails it falls back to `ctx.model.list()`.
- 目前只支持文本,暂不支持图片和语音。
- 每台电脑只运行一个机器人。OpenCode 会为每个 location 加载一次插件,插件用锁文件(`~/.config/opencode/telegram-plugin.lock`)保证只有一个实例在轮询 Telegram。
- 聊天与会话的绑定保存在内存中,OpenCode 重启后会清空,发送 `/current` 或 `/sessions` 重新绑定即可。

## License
## 常见问题

MIT © 2026 HenryYannis
**机器人没有反应?**
确认 `telegram.json` 是合法 JSON,包含 `token` 和非空的 `allowedUsers`,且你的 ID 在其中。设置 `TELEGRAM_PLUGIN_DEBUG=1` 后重启 OpenCode,查看 `~/.config/opencode/telegram-plugin.log`。

---
**不卸载的情况下怎么关闭插件?**
在 `opencode.json(c)` 的 `plugins` 中加入 `"-telegram"`,或者删除 `telegram.json`。

## 🇨🇳 中文说明
## 开发

**把手机变成 OpenCode 的遥控器。**
```sh
npm ci
npm run ci # 语法检查、单元测试、插件包校验、导入冒烟测试、打包预演
```

- 手机发消息 → 电脑上的 OpenCode 执行 → 回复发回手机;
- **电脑窗口里的回复,实时自动推送到手机**(订阅 `session.text.ended` 事件流,带去重);
- `/current` 一键把手机接入电脑当前正在聊的窗口,两边无缝交替;
- `/model` 在手机上点按钮切换模型;`/sessions` 切换会话;`/new`、`/abort`、`/status` 齐全;
- 纯插件形态:跑在 OpenCode 进程内,随 OpenCode 启停,**不需要任何独立常驻进程**。
## 许可证

安装:在 `~/.config/opencode/opencode.json` 里加 `"plugins": ["opencode-telegram-remote"]`,再创建 `~/.config/opencode/telegram.json` 填入 BotFather 的 token 和你的数字 user ID 白名单即可。
[MIT](LICENSE) © 2026 程小宝
Loading
Loading