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 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
+ }