opencode-telegram-mirror 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +196 -0
- package/README.md +230 -0
- package/bun.lock +67 -0
- package/package.json +27 -0
- package/src/config.ts +99 -0
- package/src/database.ts +120 -0
- package/src/diff-service.ts +176 -0
- package/src/log.ts +23 -0
- package/src/main.ts +1182 -0
- package/src/message-formatting.ts +202 -0
- package/src/opencode.ts +306 -0
- package/src/permission-handler.ts +242 -0
- package/src/question-handler.ts +391 -0
- package/src/system-message.ts +73 -0
- package/src/telegram.ts +705 -0
- package/test/fixtures/commands-test.json +157 -0
- package/test/fixtures/sample-updates.json +9098 -0
- package/test/mock-server.ts +271 -0
- package/test/run-test.ts +160 -0
- package/tsconfig.json +26 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Guidance for agentic coding assistants working in this repo.
|
|
4
|
+
Scope: repository root (applies to all files).
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
- Project: Telegram mirror for OpenCode sessions.
|
|
8
|
+
- Primary runtime: Bun (root)
|
|
9
|
+
- Language: TypeScript with ESM modules and strict type checking.
|
|
10
|
+
- Keep changes small and aligned with existing style.
|
|
11
|
+
|
|
12
|
+
## Repo Layout
|
|
13
|
+
- `src/`: Bun-based Telegram mirror bot.
|
|
14
|
+
- `tsconfig.json`: root TS config (strict, ESM).
|
|
15
|
+
|
|
16
|
+
## Commands (Root - Telegram Mirror)
|
|
17
|
+
Install dependencies:
|
|
18
|
+
- `bun install`
|
|
19
|
+
Run the bot:
|
|
20
|
+
- `bun run start`
|
|
21
|
+
Run from source (explicit):
|
|
22
|
+
- `bun run src/main.ts`
|
|
23
|
+
Typecheck:
|
|
24
|
+
- `bun run typecheck`
|
|
25
|
+
|
|
26
|
+
## Tests
|
|
27
|
+
|
|
28
|
+
The test harness enables **autonomous testing feedback loops** by mocking external dependencies. Instead of hitting real Telegram servers, the bot talks to a local mock server that:
|
|
29
|
+
1. Serves pre-recorded updates from fixture files
|
|
30
|
+
2. Captures all outgoing Telegram API calls for inspection
|
|
31
|
+
|
|
32
|
+
This allows fully automated test runs without any external dependencies.
|
|
33
|
+
|
|
34
|
+
### Test Files
|
|
35
|
+
- `test/fixtures/sample-updates.json` - Mock Telegram updates
|
|
36
|
+
- `test/mock-server.ts` - Mock server (serves updates + captures API calls)
|
|
37
|
+
- `test/run-test.ts` - Test runner (starts mock server + bot together)
|
|
38
|
+
|
|
39
|
+
### Running Tests
|
|
40
|
+
|
|
41
|
+
**Quick test run** (30 seconds, uses mock OpenCode if no OPENCODE_URL):
|
|
42
|
+
```bash
|
|
43
|
+
bun run test:run
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**With custom timeout**:
|
|
47
|
+
```bash
|
|
48
|
+
bun run test/run-test.ts test/fixtures/sample-updates.json 60
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Manual testing** (two terminals):
|
|
52
|
+
```bash
|
|
53
|
+
# Terminal 1: Start mock server
|
|
54
|
+
bun run test:mock-server
|
|
55
|
+
|
|
56
|
+
# Terminal 2: Start bot with mock endpoints
|
|
57
|
+
TELEGRAM_UPDATES_URL=http://localhost:3456/updates \
|
|
58
|
+
TELEGRAM_SEND_URL=http://localhost:3456 \
|
|
59
|
+
TELEGRAM_BOT_TOKEN=test:token \
|
|
60
|
+
TELEGRAM_CHAT_ID=-1003546563617 \
|
|
61
|
+
bun run start
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Mock Server Control Endpoints
|
|
65
|
+
|
|
66
|
+
The mock server at `http://localhost:3456` provides:
|
|
67
|
+
- `GET /_control?action=status` - Server stats (updates served, requests captured)
|
|
68
|
+
- `GET /_control?action=captured` - All captured Telegram API requests
|
|
69
|
+
- `POST /_control?action=inject` - Inject new updates dynamically
|
|
70
|
+
- `GET /_control?action=reset` - Reset update pointer and clear captured requests
|
|
71
|
+
|
|
72
|
+
### Adding Test Fixtures
|
|
73
|
+
|
|
74
|
+
Add new fixture files to `test/fixtures/` as JSON arrays of updates in the DO format:
|
|
75
|
+
```json
|
|
76
|
+
[
|
|
77
|
+
{
|
|
78
|
+
"update_id": 123,
|
|
79
|
+
"chat_id": "-1003546563617",
|
|
80
|
+
"payload": { "update_id": 123, "message": { ... } }
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Single-File Checks
|
|
86
|
+
- Root: `bun run typecheck` validates `src/**/*`.
|
|
87
|
+
- Lint single file: `npx next lint --file app/page.tsx`.
|
|
88
|
+
- Use `npm run lint -- --file <path>` if you prefer npm.
|
|
89
|
+
|
|
90
|
+
## TypeScript & Module Conventions
|
|
91
|
+
- TypeScript `strict` is enabled in both packages.
|
|
92
|
+
- ESM modules are used; prefer `import`/`export` syntax.
|
|
93
|
+
- Use `import type` for type-only imports (see `src/telegram.ts`).
|
|
94
|
+
- Prefer explicit return types for exported/public functions.
|
|
95
|
+
- Avoid `any`; use `unknown` + narrowing when needed.
|
|
96
|
+
- Prefer `interface` for object shapes and `type` for unions.
|
|
97
|
+
- Keep JSON parsing typed (cast to known interfaces).
|
|
98
|
+
- Prefer `const` over `let`; keep functions small and focused.
|
|
99
|
+
|
|
100
|
+
## Formatting
|
|
101
|
+
- Indentation: 2 spaces.
|
|
102
|
+
- Semicolons are omitted in existing files.
|
|
103
|
+
- Root `src/` uses double quotes.
|
|
104
|
+
- Keep line length readable (roughly 100–120 chars).
|
|
105
|
+
- Use trailing commas in multi-line objects/arrays.
|
|
106
|
+
- Separate logical blocks with blank lines.
|
|
107
|
+
|
|
108
|
+
## Imports
|
|
109
|
+
- Order: Node built-ins (`node:`), external deps, internal modules.
|
|
110
|
+
- Keep internal imports relative (no absolute paths in root).
|
|
111
|
+
- Avoid unused imports; remove when refactoring.
|
|
112
|
+
|
|
113
|
+
## Naming
|
|
114
|
+
- Files: kebab-case or lower-case with hyphens (existing pattern).
|
|
115
|
+
- Functions: `camelCase`.
|
|
116
|
+
- Classes: `PascalCase`.
|
|
117
|
+
- Types/interfaces: `PascalCase`.
|
|
118
|
+
- Constants: `UPPER_SNAKE_CASE` when truly constant.
|
|
119
|
+
- Prefer descriptive names (e.g., `sessionId`, `updatesUrl`).
|
|
120
|
+
|
|
121
|
+
## Error Handling
|
|
122
|
+
- Use `try/catch` around network and IO calls.
|
|
123
|
+
- Log errors with context and return safe defaults.
|
|
124
|
+
- Throw `Error` for fatal conditions (e.g., invalid bot token).
|
|
125
|
+
- Use `String(error)` when logging unknown errors.
|
|
126
|
+
- Prefer early returns for invalid state.
|
|
127
|
+
- Use `console.error` + `process.exit(1)` only for fatal startup errors.
|
|
128
|
+
|
|
129
|
+
## Logging
|
|
130
|
+
- Root uses `createLogger()` and `log(level, message, extra)`.
|
|
131
|
+
- Use `log("info" | "warn" | "error" | "debug", ...)` in bot code.
|
|
132
|
+
- Avoid `console.log` except for fatal startup errors.
|
|
133
|
+
|
|
134
|
+
## Async & Concurrency
|
|
135
|
+
- Use `async/await` for clarity.
|
|
136
|
+
- Avoid blocking loops; use `await Bun.sleep(...)` for backoff.
|
|
137
|
+
- If retrying, log retry context and keep delays reasonable.
|
|
138
|
+
|
|
139
|
+
## Data & API Handling
|
|
140
|
+
- Validate request payloads before use.
|
|
141
|
+
- When reading env vars, allow file config to be overridden.
|
|
142
|
+
- Normalize ids to strings for comparisons.
|
|
143
|
+
- Keep Telegram API interactions resilient (retry without Markdown).
|
|
144
|
+
|
|
145
|
+
## Config & Environment
|
|
146
|
+
- Root config loads from:
|
|
147
|
+
- `~/.config/opencode/telegram.json`
|
|
148
|
+
- `<repo>/.opencode/telegram.json`
|
|
149
|
+
- Environment variables override config:
|
|
150
|
+
- `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`, `TELEGRAM_UPDATES_URL`, `TELEGRAM_SEND_URL`
|
|
151
|
+
- Diff viewer base URL uses `VERCEL_URL` or `NEXT_PUBLIC_BASE_URL`.
|
|
152
|
+
|
|
153
|
+
## Dependency Management
|
|
154
|
+
- Root uses Bun; avoid adding npm scripts unless required.
|
|
155
|
+
- Keep dependency upgrades minimal and justified.
|
|
156
|
+
|
|
157
|
+
## Generated Files
|
|
158
|
+
- Do not commit `dist/` or `node_modules/`.
|
|
159
|
+
- Root build output goes to `dist/` (see `tsconfig.json`).
|
|
160
|
+
|
|
161
|
+
## Code Organization
|
|
162
|
+
- Prefer small helpers over large monolithic functions.
|
|
163
|
+
- Keep side effects near the edges (IO, network).
|
|
164
|
+
- Keep types co-located with their usage when small.
|
|
165
|
+
|
|
166
|
+
## Updating This File
|
|
167
|
+
- Update commands if scripts change.
|
|
168
|
+
- Add new tool or lint rules as they are introduced.
|
|
169
|
+
- Keep this file around ~150 lines.
|
|
170
|
+
|
|
171
|
+
## Cursor/Copilot Rules
|
|
172
|
+
- No `.cursor/rules/`, `.cursorrules`, or `.github/copilot-instructions.md` found.
|
|
173
|
+
- If added later, summarize them here.
|
|
174
|
+
|
|
175
|
+
<!-- opensrc:start -->
|
|
176
|
+
|
|
177
|
+
## Source Code Reference
|
|
178
|
+
|
|
179
|
+
Source code for dependencies is available in `opensrc/` for deeper understanding of implementation details.
|
|
180
|
+
|
|
181
|
+
See `opensrc/sources.json` for the list of available packages and their versions.
|
|
182
|
+
|
|
183
|
+
Use this source code when you need to understand how a package works internally, not just its types/interface.
|
|
184
|
+
|
|
185
|
+
### Fetching Additional Source Code
|
|
186
|
+
|
|
187
|
+
To fetch source code for a package or repository you need to understand, run:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
npx opensrc <package> # npm package (e.g., npx opensrc zod)
|
|
191
|
+
npx opensrc pypi:<package> # Python package (e.g., npx opensrc pypi:requests)
|
|
192
|
+
npx opensrc crates:<package> # Rust crate (e.g., npx opensrc crates:serde)
|
|
193
|
+
npx opensrc <owner>/<repo> # GitHub repo (e.g., npx opensrc vercel/ai)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
<!-- opensrc:end -->
|
package/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# OpenCode Telegram Mirror
|
|
2
|
+
|
|
3
|
+
A standalone bot that mirrors OpenCode sessions to Telegram topics, enabling collaborative AI-assisted coding conversations in Telegram.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g opencode-telegram-mirror
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
1. **Create a Telegram Bot**:
|
|
14
|
+
- Message [@BotFather](https://t.me/BotFather) on Telegram
|
|
15
|
+
- Send `/newbot` and follow instructions
|
|
16
|
+
- Copy your bot token
|
|
17
|
+
|
|
18
|
+
2. **Get your Chat ID**:
|
|
19
|
+
- Message [@userinfobot](https://t.me/userinfobot)
|
|
20
|
+
- Copy your chat ID
|
|
21
|
+
|
|
22
|
+
3. **Run the mirror**:
|
|
23
|
+
```bash
|
|
24
|
+
opencode-telegram-mirror
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
4. **Configure environment variables**:
|
|
28
|
+
```bash
|
|
29
|
+
export TELEGRAM_BOT_TOKEN="your-bot-token"
|
|
30
|
+
export TELEGRAM_CHAT_ID="your-chat-id"
|
|
31
|
+
# Optional: export TELEGRAM_THREAD_ID="your-thread-id"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
That's it! Your OpenCode sessions will now be mirrored to Telegram.
|
|
35
|
+
|
|
36
|
+
## How it works
|
|
37
|
+
|
|
38
|
+
The Telegram mirror streams OpenCode session interactions (questions, answers, tool usage, and file edits) to Telegram topics. This enables:
|
|
39
|
+
|
|
40
|
+
- **Collaborative coding**: Share your coding sessions with team members
|
|
41
|
+
- **Remote pair programming**: Get real-time feedback on your code
|
|
42
|
+
- **Session persistence**: Keep conversations going across devices
|
|
43
|
+
- **Rich formatting**: Code blocks, diffs, and interactive buttons
|
|
44
|
+
|
|
45
|
+
### Architecture
|
|
46
|
+
|
|
47
|
+
Each instance of the `opencode-telegram-mirror` binary mirrors **one OpenCode session** to **one Telegram channel/thread**. This 1:1 mapping ensures clean separation between different coding conversations.
|
|
48
|
+
|
|
49
|
+
**OpenCode Server Connection**:
|
|
50
|
+
- **Without `OPENCODE_URL`**: The mirror spawns its own OpenCode server instance locally
|
|
51
|
+
- **With `OPENCODE_URL`**: The mirror connects to an existing OpenCode server at the specified URL
|
|
52
|
+
|
|
53
|
+
This flexibility allows you to either run self-contained mirrors (each with their own server) or connect to a shared/managed OpenCode server.
|
|
54
|
+
|
|
55
|
+
### Threading & Topics
|
|
56
|
+
|
|
57
|
+
Telegram supports threaded conversations in forum-style channels:
|
|
58
|
+
|
|
59
|
+
- **No thread ID**: Messages go to the main channel
|
|
60
|
+
- **With thread ID**: Messages go to a specific topic thread within a forum channel
|
|
61
|
+
|
|
62
|
+
Each mirror instance should be configured with a unique `TELEGRAM_THREAD_ID` to prevent cross-contamination between different coding sessions.
|
|
63
|
+
|
|
64
|
+
### Orchestration
|
|
65
|
+
|
|
66
|
+
While you can run a single mirror instance, production deployments typically require orchestration to support multiple concurrent sessions:
|
|
67
|
+
|
|
68
|
+
- **Multiple sessions**: Run separate mirror processes for different coding projects
|
|
69
|
+
- **Thread isolation**: Use unique thread IDs per session
|
|
70
|
+
- **Server sharing**: Point multiple mirrors to the same `OPENCODE_URL` for resource efficiency
|
|
71
|
+
- **Load balancing**: Distribute mirrors across multiple servers
|
|
72
|
+
|
|
73
|
+
Example orchestration might involve:
|
|
74
|
+
- Docker containers for each mirror instance
|
|
75
|
+
- Kubernetes deployments with unique environment configs
|
|
76
|
+
- Process managers like PM2 for local deployment
|
|
77
|
+
|
|
78
|
+
### Updates URL & Multi-Instance Deployments
|
|
79
|
+
|
|
80
|
+
**Single Instance (Simple Case)**:
|
|
81
|
+
If you're running one `opencode-telegram-mirror` instance with one Telegram bot and one channel, you don't need `TELEGRAM_UPDATES_URL`. The mirror will poll Telegram's API directly using `getUpdates`.
|
|
82
|
+
|
|
83
|
+
**Multi-Instance Deployments**:
|
|
84
|
+
When running multiple mirror instances (e.g., one per coding session or per team), Telegram only allows one webhook or one `getUpdates` poller per bot. To support multiple mirrors:
|
|
85
|
+
|
|
86
|
+
1. **Central Updates Collector**: Deploy a central service that polls `getUpdates` once and distributes updates to multiple mirrors
|
|
87
|
+
2. **Set `TELEGRAM_UPDATES_URL`**: Point each mirror to this central endpoint
|
|
88
|
+
3. **Thread Isolation**: Each mirror should have a unique `TELEGRAM_THREAD_ID`
|
|
89
|
+
|
|
90
|
+
**Updates URL API Contract**:
|
|
91
|
+
The central updates endpoint must accept GET requests with query parameters:
|
|
92
|
+
- `since`: Last processed `update_id` (for pagination)
|
|
93
|
+
- `chat_id`: Filter updates to specific chat
|
|
94
|
+
- `thread_id`: Filter to specific thread (optional)
|
|
95
|
+
|
|
96
|
+
Response format (JSON):
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"updates": [
|
|
100
|
+
{
|
|
101
|
+
"payload": {
|
|
102
|
+
"update_id": 123,
|
|
103
|
+
"message": {
|
|
104
|
+
"message_id": 456,
|
|
105
|
+
"message_thread_id": 789,
|
|
106
|
+
"date": 1640995200,
|
|
107
|
+
"text": "Hello world",
|
|
108
|
+
"from": { "id": 123456, "username": "user" },
|
|
109
|
+
"chat": { "id": -1001234567890 }
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"update_id": 123
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The `payload` contains the standard [Telegram Update object](https://core.telegram.org/bots/api#update). Basic authentication is supported via URL credentials (`https://user:pass@example.com/updates`).
|
|
119
|
+
|
|
120
|
+
## Usage
|
|
121
|
+
|
|
122
|
+
### Basic Usage
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
opencode-telegram-mirror [directory] [session-id]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- `directory`: Working directory (defaults to current directory)
|
|
129
|
+
- `session-id`: Existing OpenCode session ID to resume
|
|
130
|
+
|
|
131
|
+
### Environment Variables
|
|
132
|
+
|
|
133
|
+
| Variable | Description | Required |
|
|
134
|
+
|----------|-------------|----------|
|
|
135
|
+
| `TELEGRAM_BOT_TOKEN` | Bot token from [@BotFather](https://t.me/BotFather) | Yes |
|
|
136
|
+
| `TELEGRAM_CHAT_ID` | Chat ID from [@userinfobot](https://t.me/userinfobot) | Yes |
|
|
137
|
+
| `TELEGRAM_THREAD_ID` | Thread/topic ID for forum channels | No |
|
|
138
|
+
| `TELEGRAM_UPDATES_URL` | Central updates endpoint for multi-instance deployments | No |
|
|
139
|
+
| `TELEGRAM_SEND_URL` | Custom Telegram API endpoint (defaults to api.telegram.org) | No |
|
|
140
|
+
| `OPENCODE_URL` | External OpenCode server URL (if not set, spawns local server) | No |
|
|
141
|
+
|
|
142
|
+
### Configuration Files
|
|
143
|
+
|
|
144
|
+
The bot loads configuration from (in order of priority):
|
|
145
|
+
|
|
146
|
+
1. Environment variables (highest priority)
|
|
147
|
+
2. `~/.config/opencode/telegram.json`
|
|
148
|
+
3. `<repo>/.opencode/telegram.json`
|
|
149
|
+
|
|
150
|
+
Example config file:
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"botToken": "your-bot-token",
|
|
154
|
+
"chatId": "your-chat-id",
|
|
155
|
+
"threadId": 123,
|
|
156
|
+
"sendUrl": "https://api.telegram.org/bot",
|
|
157
|
+
"updatesUrl": "https://your-durable-object-endpoint"
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Advanced Features
|
|
162
|
+
|
|
163
|
+
### Session Control
|
|
164
|
+
|
|
165
|
+
Send messages in Telegram to interact with OpenCode:
|
|
166
|
+
- **Text messages**: Sent as prompts to OpenCode
|
|
167
|
+
- **Photos**: Attached as image files to prompts
|
|
168
|
+
- **"x"**: Interrupt the current session
|
|
169
|
+
- **"/connect"**: Get the OpenCode server URL
|
|
170
|
+
|
|
171
|
+
### Interactive Controls
|
|
172
|
+
|
|
173
|
+
The bot provides inline keyboard controls for:
|
|
174
|
+
- **Interrupt**: Stop the current session
|
|
175
|
+
- **Mode switching**: Toggle between "plan" and "build" modes
|
|
176
|
+
- **Questions**: Answer multiple-choice questions from OpenCode
|
|
177
|
+
- **Permissions**: Grant/deny file access permissions
|
|
178
|
+
|
|
179
|
+
### Diff Viewer
|
|
180
|
+
|
|
181
|
+
When OpenCode makes file edits, the bot:
|
|
182
|
+
1. Generates a visual diff
|
|
183
|
+
2. Uploads it to a diff viewer
|
|
184
|
+
3. Shares a link to view the changes
|
|
185
|
+
|
|
186
|
+
## Development
|
|
187
|
+
|
|
188
|
+
### Prerequisites
|
|
189
|
+
|
|
190
|
+
- Node.js 18+
|
|
191
|
+
- npm or bun
|
|
192
|
+
|
|
193
|
+
### Local Development
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Clone the repo
|
|
197
|
+
git clone <repository-url>
|
|
198
|
+
cd opencode-telegram-mirror
|
|
199
|
+
|
|
200
|
+
# Install dependencies
|
|
201
|
+
bun install
|
|
202
|
+
|
|
203
|
+
# Run in development
|
|
204
|
+
bun run start
|
|
205
|
+
|
|
206
|
+
# Run tests
|
|
207
|
+
bun run test:run
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Building
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# Type check
|
|
214
|
+
bun run typecheck
|
|
215
|
+
|
|
216
|
+
# Run mock server for testing
|
|
217
|
+
bun run test:mock-server
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Why Telegram?
|
|
221
|
+
|
|
222
|
+
- **Cross-platform**: Works on any device with Telegram
|
|
223
|
+
- **Instant sync**: Real-time message delivery
|
|
224
|
+
- **Rich formatting**: Markdown, code blocks, and media support
|
|
225
|
+
- **Free**: No rate limits or costs
|
|
226
|
+
- **Persistent**: Message history is always available
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
[Add your license here]
|
package/bun.lock
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 0,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "opencode-telegram-mirror",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@opencode-ai/sdk": "latest",
|
|
9
|
+
"better-result": "^2.0.0",
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/bun": "latest",
|
|
13
|
+
"typescript": "^5.9.3",
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"bun": ">=1.0.0",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
"packages": {
|
|
21
|
+
"@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="],
|
|
22
|
+
|
|
23
|
+
"@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="],
|
|
24
|
+
|
|
25
|
+
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.15", "", {}, "sha512-9G0FTWELgyMKWKSAihiyNu/h352L5smrMOSexS05EG+knU+aBTGil3pytbgo69OgJGUWpY0onfCg2QwYaFGUug=="],
|
|
26
|
+
|
|
27
|
+
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8GvNtMo0NINM7Emk9cNAviCG3teEgr3BUX9be0+GD029zIagx2Sf54jMui1Eu1IpFm7nWHODuLEefGOQNaJ0gQ=="],
|
|
28
|
+
|
|
29
|
+
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-r33eHQOHAwkuiBJIwmkXIyqONQOQMnd1GMTpDzaxx9vf9+svby80LZO9Hcm1ns6KT/TBRFyODC/0loA7FAaffg=="],
|
|
30
|
+
|
|
31
|
+
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-p5q3rJk48qhLuLBOFehVc+kqCE03YrswTc6NCxbwsxiwfySXwcAvpF2KWKF/ZZObvvR8hCCvqe1F81b2p5r2dg=="],
|
|
32
|
+
|
|
33
|
+
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-zkcHPI23QxJ1TdqafhgkXt1NOEN8o5C460sVeNnrhfJ43LwZgtfcvcQE39x/pBedu67fatY8CU0iY00nOh46ZQ=="],
|
|
34
|
+
|
|
35
|
+
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-HKBeUlJdNduRkzJKZ5DXM+pPqntfC50/Hu2X65jVX0Y7hu/6IC8RaUTqpr8FtCZqqmc9wDK0OTL+Mbi9UQIKYQ=="],
|
|
36
|
+
|
|
37
|
+
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-n7zhKTSDZS0yOYg5Rq8easZu5Y/o47sv0c7yGr2ciFdcie9uYV55fZ7QMqhWMGK33ezCSikh5EDkUMCIvfWpjA=="],
|
|
38
|
+
|
|
39
|
+
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FeCQyBU62DMuB0nn01vPnf3McXrKOsrK9p7sHaBFYycw0mmoU8kCq/WkBkGMnLuvQljJSyen8QBTx+fXdNupWg=="],
|
|
40
|
+
|
|
41
|
+
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-XkCCHkByYn8BIDvoxnny898znju4xnW2kvFE8FT5+0Y62cWdcBGMZ9RdsEUTeRz16k8hHtJpaSfLcEmNTFIwRQ=="],
|
|
42
|
+
|
|
43
|
+
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-TJiYC7KCr0XxFTsxgwQOeE7dncrEL/RSyL0EzSL3xRkrxJMWBCvCSjQn7LV1i6T7hFst0+3KoN3VWvD5BinqHA=="],
|
|
44
|
+
|
|
45
|
+
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.5", "", { "os": "win32", "cpu": "x64" }, "sha512-T3xkODItb/0ftQPFsZDc7EAX2D6A4TEazQ2YZyofZToO8Q7y8YT8ooWdhd0BQiTCd66uEvgE1DCZetynwg2IoA=="],
|
|
46
|
+
|
|
47
|
+
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.5", "", { "os": "win32", "cpu": "x64" }, "sha512-rtVQB9/1XK8FWJgFtsOthbPifRMYypgJwxu+pK3NHx8WvFKmq7HcPDqNr8xLzGULjQEO7eAo2aOZfONOwYz+5g=="],
|
|
48
|
+
|
|
49
|
+
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
|
50
|
+
|
|
51
|
+
"@types/node": ["@types/node@25.0.6", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q=="],
|
|
52
|
+
|
|
53
|
+
"better-result": ["better-result@2.0.0", "", { "dependencies": { "@clack/prompts": "^0.11.0" }, "bin": { "better-result": "bin/cli.mjs" } }, "sha512-72GfHw5FQp2GylNeoQPAmXDQj2e0HBeMuyGwVd1eM5HahvC2pysdBjCOuRASfQ2ZLeykiFaVOs3455a78+Dh3Q=="],
|
|
54
|
+
|
|
55
|
+
"bun": ["bun@1.3.5", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.5", "@oven/bun-darwin-x64": "1.3.5", "@oven/bun-darwin-x64-baseline": "1.3.5", "@oven/bun-linux-aarch64": "1.3.5", "@oven/bun-linux-aarch64-musl": "1.3.5", "@oven/bun-linux-x64": "1.3.5", "@oven/bun-linux-x64-baseline": "1.3.5", "@oven/bun-linux-x64-musl": "1.3.5", "@oven/bun-linux-x64-musl-baseline": "1.3.5", "@oven/bun-windows-x64": "1.3.5", "@oven/bun-windows-x64-baseline": "1.3.5" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-c1YHIGUfgvYPJmLug5QiLzNWlX2Dg7X/67JWu1Va+AmMXNXzC/KQn2lgQ7rD+n1u1UqDpJMowVGGxTNpbPydNw=="],
|
|
56
|
+
|
|
57
|
+
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
|
58
|
+
|
|
59
|
+
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
|
60
|
+
|
|
61
|
+
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
|
62
|
+
|
|
63
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
64
|
+
|
|
65
|
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
66
|
+
}
|
|
67
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-telegram-mirror",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Standalone bot that mirrors OpenCode sessions to Telegram topics",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/main.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"opencode-telegram-mirror": "./src/main.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "bun run src/main.ts",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"test:mock-server": "bun run test/mock-server.ts",
|
|
14
|
+
"test:run": "bun run test/run-test.ts"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@opencode-ai/sdk": "latest",
|
|
18
|
+
"better-result": "^2.0.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/bun": "latest",
|
|
22
|
+
"typescript": "^5.9.3"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"bun": ">=1.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bot configuration loading
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile } from "node:fs/promises"
|
|
6
|
+
import { join } from "node:path"
|
|
7
|
+
import { Result, TaggedError } from "better-result"
|
|
8
|
+
import type { LogFn } from "./log"
|
|
9
|
+
|
|
10
|
+
export interface BotConfig {
|
|
11
|
+
botToken?: string
|
|
12
|
+
chatId?: string
|
|
13
|
+
threadId?: number
|
|
14
|
+
// URL to poll for updates (Cloudflare DO endpoint)
|
|
15
|
+
updatesUrl?: string
|
|
16
|
+
// URL to send messages (defaults to Telegram API if not set)
|
|
17
|
+
sendUrl?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ConfigLoadError extends TaggedError("ConfigLoadError")<{
|
|
21
|
+
path: string
|
|
22
|
+
message: string
|
|
23
|
+
cause: unknown
|
|
24
|
+
}>() {
|
|
25
|
+
constructor(args: { path: string; cause: unknown }) {
|
|
26
|
+
const message = `Failed to load config at ${args.path}`
|
|
27
|
+
super({ ...args, message })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ConfigLoadResult = Result<BotConfig, ConfigLoadError>
|
|
32
|
+
|
|
33
|
+
export async function loadConfig(directory: string, log?: LogFn): Promise<ConfigLoadResult> {
|
|
34
|
+
const config: BotConfig = {}
|
|
35
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || ""
|
|
36
|
+
|
|
37
|
+
const configPaths = [
|
|
38
|
+
join(homeDir, ".config", "opencode", "telegram.json"),
|
|
39
|
+
join(directory, ".opencode", "telegram.json"),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
log?.("debug", "Checking config file paths", { paths: configPaths })
|
|
43
|
+
|
|
44
|
+
for (const configPath of configPaths) {
|
|
45
|
+
const fileResult = await Result.tryPromise({
|
|
46
|
+
try: async () => {
|
|
47
|
+
const content = await readFile(configPath, "utf-8")
|
|
48
|
+
return JSON.parse(content) as BotConfig
|
|
49
|
+
},
|
|
50
|
+
catch: (error) => new ConfigLoadError({ path: configPath, cause: error }),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
if (fileResult.status === "ok") {
|
|
54
|
+
Object.assign(config, fileResult.value)
|
|
55
|
+
log?.("info", "Loaded config file", {
|
|
56
|
+
path: configPath,
|
|
57
|
+
keys: Object.keys(fileResult.value),
|
|
58
|
+
})
|
|
59
|
+
} else {
|
|
60
|
+
log?.("debug", "Config file not found or invalid", {
|
|
61
|
+
path: configPath,
|
|
62
|
+
error: String(fileResult.error).slice(0, 100),
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Environment variables override file config
|
|
68
|
+
const envOverrides: string[] = []
|
|
69
|
+
|
|
70
|
+
if (process.env.TELEGRAM_BOT_TOKEN) {
|
|
71
|
+
config.botToken = process.env.TELEGRAM_BOT_TOKEN
|
|
72
|
+
envOverrides.push("TELEGRAM_BOT_TOKEN")
|
|
73
|
+
}
|
|
74
|
+
if (process.env.TELEGRAM_CHAT_ID) {
|
|
75
|
+
config.chatId = process.env.TELEGRAM_CHAT_ID
|
|
76
|
+
envOverrides.push("TELEGRAM_CHAT_ID")
|
|
77
|
+
}
|
|
78
|
+
if (process.env.TELEGRAM_THREAD_ID) {
|
|
79
|
+
const parsed = Number(process.env.TELEGRAM_THREAD_ID)
|
|
80
|
+
if (!Number.isNaN(parsed)) {
|
|
81
|
+
config.threadId = parsed
|
|
82
|
+
envOverrides.push("TELEGRAM_THREAD_ID")
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (process.env.TELEGRAM_UPDATES_URL) {
|
|
86
|
+
config.updatesUrl = process.env.TELEGRAM_UPDATES_URL
|
|
87
|
+
envOverrides.push("TELEGRAM_UPDATES_URL")
|
|
88
|
+
}
|
|
89
|
+
if (process.env.TELEGRAM_SEND_URL) {
|
|
90
|
+
config.sendUrl = process.env.TELEGRAM_SEND_URL
|
|
91
|
+
envOverrides.push("TELEGRAM_SEND_URL")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (envOverrides.length > 0) {
|
|
95
|
+
log?.("info", "Environment variable overrides applied", { variables: envOverrides })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return Result.ok(config)
|
|
99
|
+
}
|