opencode-remote-control 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,8 +1,19 @@
1
1
  # OpenCode Remote Control
2
2
 
3
- [中文文档](./README_CN.md)
3
+ <p align="center">
4
+ <a href="https://github.com/ceociocto/opencode-remote-control/actions/workflows/publish.yml?query=branch%3Amain"><img src="https://img.shields.io/github/actions/workflow/status/ceociocto/opencode-remote-control/publish.yml?branch=main&style=for-the-badge" alt="CI status"></a>
5
+ <a href="https://www.npmjs.com/package/opencode-remote-control"><img src="https://img.shields.io/npm/v/opencode-remote-control?style=for-the-badge" alt="npm version"></a>
6
+ <a href="https://github.com/ceociocto/opencode-remote-control/releases"><img src="https://img.shields.io/github/v/release/ceociocto/opencode-remote-control?include_prereleases&style=for-the-badge" alt="GitHub release"></a>
7
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge" alt="MIT License"></a>
8
+ </p>
4
9
 
5
- Control OpenCode from anywhere via Telegram.
10
+ <p align="center">
11
+ <a href="./README_CN.md">中文文档</a>
12
+ </p>
13
+
14
+ <p align="center">
15
+ Control OpenCode from anywhere via Telegram or Feishu.
16
+ </p>
6
17
 
7
18
  ## Installation
8
19
 
@@ -13,22 +24,11 @@ npm install -g opencode-remote-control
13
24
  pnpm install -g opencode-remote-control
14
25
  # or
15
26
  bun install -g opencode-remote-control
16
-
17
- # Run (will prompt for token on first run)
18
- opencode-remote
19
27
  ```
20
28
 
21
- ### Install from Source
29
+ ## Quick Start
22
30
 
23
- ```bash
24
- git clone https://github.com/ceociocto/opencode-remote-control.git
25
- cd opencode-remote-control
26
- bun install
27
- bun run build
28
- node dist/cli.js
29
- ```
30
-
31
- ## Setup
31
+ ### Telegram Setup
32
32
 
33
33
  On first run, you'll be prompted for a Telegram bot token:
34
34
 
@@ -38,16 +38,42 @@ On first run, you'll be prompted for a Telegram bot token:
38
38
 
39
39
  Token is saved to `~/.opencode-remote/.env`
40
40
 
41
- ## Commands
41
+ ### Feishu Setup
42
+
43
+ Run the config command for Feishu:
42
44
 
43
- **CLI:**
45
+ ```bash
46
+ opencode-remote config-feishu
44
47
  ```
45
- opencode-remote # Start the bot
46
- opencode-remote config # Reconfigure token
47
- opencode-remote help # Show help
48
+
49
+ Follow the interactive guide to configure your Feishu bot. For detailed setup instructions, see [Feishu Setup Guide](./docs/FEISHU_SETUP_EN.md) or [飞书配置指南](./docs/FEISHU_SETUP.md).
50
+
51
+ ## Usage
52
+
53
+ ```bash
54
+ opencode-remote # Start all configured bots
55
+ opencode-remote start # Start all configured bots
56
+ opencode-remote telegram # Start Telegram bot only
57
+ opencode-remote feishu # Start Feishu bot only
58
+ opencode-remote config # Configure a channel (interactive)
59
+ opencode-remote config-feishu # Configure Feishu directly
60
+ opencode-remote help # Show help
48
61
  ```
49
62
 
50
- **Telegram:**
63
+ ## Install from Source
64
+
65
+ ```bash
66
+ git clone https://github.com/ceociocto/opencode-remote-control.git
67
+ cd opencode-remote-control
68
+ bun install
69
+ bun run build
70
+ node dist/cli.js
71
+ ```
72
+
73
+ ## Bot Commands
74
+
75
+ Both Telegram and Feishu support the same commands:
76
+
51
77
  | Command | Description |
52
78
  |--------|-------------|
53
79
  | `/start` | Start the bot |
@@ -58,9 +84,12 @@ opencode-remote help # Show help
58
84
  | `/diff` | View pending diff |
59
85
  | `/files` | List changed files |
60
86
  | `/reset` | Reset session |
87
+ | `/retry` | Retry connection |
61
88
 
62
89
  ## How It Works
63
90
 
91
+ ### Telegram (Polling Mode)
92
+
64
93
  ```
65
94
  ┌─────────────────┐ ┌──────────────────┐
66
95
  │ Telegram App │ │ Telegram Server │
@@ -83,13 +112,38 @@ opencode-remote help # Show help
83
112
  └─────────────────────────────────────────────────────────┘
84
113
  ```
85
114
 
86
- The bot uses **Polling Mode** to fetch messages from Telegram servers, requiring no tunnel or public IP configuration.
115
+ The Telegram bot uses **Polling Mode** to fetch messages from Telegram servers, requiring no tunnel or public IP configuration.
116
+
117
+ ### Feishu (Webhook Mode)
118
+
119
+ ```
120
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
121
+ │ Feishu │───▶│ Feishu │───▶│ Webhook │
122
+ │ Client │ │ Server │ │ (ngrok) │
123
+ └─────────────┘ └─────────────┘ └──────┬──────┘
124
+
125
+
126
+ ┌─────────────┐
127
+ │ Feishu Bot │
128
+ │ (port 3001)│
129
+ └──────┬──────┘
130
+
131
+
132
+ ┌─────────────┐
133
+ │ OpenCode │
134
+ │ SDK │
135
+ └─────────────┘
136
+ ```
137
+
138
+ The Feishu bot uses **Webhook Mode** and requires a tunnel (ngrok/cloudflared) to receive messages.
87
139
 
88
140
  ## Requirements
89
141
 
90
142
  - Node.js >= 18.0.0
91
143
  - [OpenCode](https://github.com/opencode-ai/opencode) installed
92
- - Telegram account
144
+ - Telegram account (for Telegram bot)
145
+ - Feishu account (for Feishu bot)
146
+ - ngrok or cloudflared (for Feishu webhook)
93
147
 
94
148
  ## Contributing
95
149
 
package/dist/cli.js CHANGED
@@ -4,13 +4,14 @@ import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'fs';
4
4
  import { homedir } from 'os';
5
5
  import { join } from 'path';
6
6
  import { startBot } from './telegram/bot.js';
7
+ import { startFeishuBot } from './feishu/bot.js';
7
8
  const CONFIG_DIR = join(homedir(), '.opencode-remote');
8
9
  const CONFIG_FILE = join(CONFIG_DIR, '.env');
9
10
  function printBanner() {
10
11
  console.log(`
11
12
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12
13
  OpenCode Remote Control
13
- Control OpenCode from Telegram
14
+ Control OpenCode from Telegram or Feishu
14
15
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
15
16
  `);
16
17
  }
@@ -19,16 +20,45 @@ function printHelp() {
19
20
  Usage: opencode-remote [command]
20
21
 
21
22
  Commands:
22
- start Start the bot (default)
23
- config Configure Telegram bot token
24
- help Show this help message
23
+ start Start all configured bots (default)
24
+ telegram Start Telegram bot only
25
+ feishu Start Feishu bot only
26
+ config Configure a channel (interactive selection)
27
+ config-feishu Configure Feishu bot directly
28
+ help Show this help message
25
29
 
26
30
  Examples:
27
- opencode-remote # Start the bot
28
- opencode-remote start # Start the bot
29
- opencode-remote config # Configure token
31
+ opencode-remote # Start all bots
32
+ opencode-remote start # Start all bots
33
+ opencode-remote telegram # Start Telegram only
34
+ opencode-remote feishu # Start Feishu only
35
+ opencode-remote config # Interactive channel selection
36
+ opencode-remote config-feishu # Configure Feishu directly
30
37
  `);
31
38
  }
39
+ async function promptChannel() {
40
+ console.log('\n📝 Select a channel to configure:');
41
+ console.log('');
42
+ console.log(' 1. Telegram');
43
+ console.log(' 2. Feishu (飞书)');
44
+ console.log('');
45
+ process.stdout.write('Enter your choice (1 or 2): ');
46
+ const choice = await new Promise((resolve) => {
47
+ process.stdin.setEncoding('utf8');
48
+ process.stdin.once('data', (chunk) => {
49
+ resolve(chunk.toString().trim());
50
+ });
51
+ });
52
+ if (choice === '1' || choice.toLowerCase() === 'telegram') {
53
+ return 'telegram';
54
+ }
55
+ else if (choice === '2' || choice.toLowerCase() === 'feishu') {
56
+ return 'feishu';
57
+ }
58
+ // Default to telegram if invalid input
59
+ console.log('Invalid choice, defaulting to Telegram');
60
+ return 'telegram';
61
+ }
32
62
  async function promptToken() {
33
63
  console.log('\n📝 Setup required: Telegram Bot Token');
34
64
  console.log('\nHow to get a token:');
@@ -47,59 +77,281 @@ async function promptToken() {
47
77
  });
48
78
  return token;
49
79
  }
80
+ async function promptFeishuConfig() {
81
+ console.log('\n📝 Step 1: Create Feishu App');
82
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
83
+ console.log('');
84
+ console.log(' 1. Go to https://open.feishu.cn/app');
85
+ console.log(' 2. Click "创建企业自建应用" (Create enterprise app)');
86
+ console.log(' 3. Fill in app name and description');
87
+ console.log(' 4. Go to "凭证与基础信息" (Credentials) page');
88
+ console.log('');
89
+ process.stdout.write('Enter your App ID: ');
90
+ const appId = await new Promise((resolve) => {
91
+ process.stdin.setEncoding('utf8');
92
+ process.stdin.once('data', (chunk) => {
93
+ resolve(chunk.toString().trim());
94
+ });
95
+ });
96
+ process.stdout.write('Enter your App Secret: ');
97
+ const appSecret = await new Promise((resolve) => {
98
+ process.stdin.setEncoding('utf8');
99
+ process.stdin.once('data', (chunk) => {
100
+ resolve(chunk.toString().trim());
101
+ });
102
+ });
103
+ return { appId, appSecret };
104
+ }
105
+ function showFeishuSetupGuide() {
106
+ console.log('');
107
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
108
+ console.log(' 📋 Step 2: Configure App Permissions');
109
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
110
+ console.log('');
111
+ console.log(' Go to "权限管理" (Permission Management) page');
112
+ console.log(' Search and enable these permissions:');
113
+ console.log('');
114
+ console.log(' ┌────────────────────────────────────────────────────┐');
115
+ console.log(' │ Permission │ Scope │');
116
+ console.log(' ├────────────────────────────────────────────────────┤');
117
+ console.log(' │ im:message 获取与发送消息 │');
118
+ console.log(' │ im:message:send_as_bot 以应用身份发消息 │');
119
+ console.log(' │ im:message:receive_as_bot 接收机器人消息 │');
120
+ console.log(' └────────────────────────────────────────────────────┘');
121
+ console.log('');
122
+ console.log(' 💡 TIP: Copy the JSON below and use "批量添加" feature:');
123
+ console.log('');
124
+ console.log(' ┌────────────────────────────────────────────────────┐');
125
+ console.log(' │ [');
126
+ console.log(' │ "im:message",');
127
+ console.log(' │ "im:message:send_as_bot",');
128
+ console.log(' │ "im:message:receive_as_bot"');
129
+ console.log(' │ ]');
130
+ console.log(' └────────────────────────────────────────────────────┘');
131
+ console.log('');
132
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
133
+ console.log(' 🤖 Step 3: Enable Robot Capability');
134
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
135
+ console.log('');
136
+ console.log(' 1. Go to "应用能力" (App Capabilities) → "机器人" (Robot)');
137
+ console.log(' 2. Click "启用机器人" (Enable Robot)');
138
+ console.log(' 3. Set robot name and description');
139
+ console.log('');
140
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
141
+ console.log(' 🔗 Step 4: Configure Event Subscription');
142
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
143
+ console.log('');
144
+ console.log(' 1. Start the bot locally:');
145
+ console.log(' $ opencode-remote feishu');
146
+ console.log('');
147
+ console.log(' 2. Expose webhook with ngrok/cloudflared:');
148
+ console.log(' $ ngrok http 3001');
149
+ console.log('');
150
+ console.log(' 3. Go to "事件订阅" (Event Subscription) page');
151
+ console.log(' 4. Set Request URL: https://your-ngrok-url/feishu/webhook');
152
+ console.log(' 5. Add event: im.message.receive_v1');
153
+ console.log('');
154
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
155
+ console.log(' 📤 Step 5: Publish App');
156
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
157
+ console.log('');
158
+ console.log(' 1. Go to "版本管理与发布" (Version & Publish)');
159
+ console.log(' 2. Click "创建版本" (Create Version)');
160
+ console.log(' 3. Fill in version info and submit for review');
161
+ console.log(' 4. After approval, click "发布" (Publish)');
162
+ console.log(' 5. Search your bot in Feishu and start chatting!');
163
+ console.log('');
164
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
165
+ }
50
166
  async function getConfig() {
51
- // Check environment variable first
52
- if (process.env.TELEGRAM_BOT_TOKEN) {
53
- return process.env.TELEGRAM_BOT_TOKEN;
54
- }
167
+ const config = {
168
+ opencodeServerUrl: process.env.OPENCODE_SERVER_URL || 'http://localhost:3000',
169
+ tunnelUrl: process.env.TUNNEL_URL || '',
170
+ sessionIdleTimeoutMs: parseInt(process.env.SESSION_IDLE_TIMEOUT_MS || '1800000', 10),
171
+ cleanupIntervalMs: parseInt(process.env.CLEANUP_INTERVAL_MS || '300000', 10),
172
+ approvalTimeoutMs: parseInt(process.env.APPROVAL_TIMEOUT_MS || '300000', 10),
173
+ };
55
174
  // Check config file
56
175
  if (existsSync(CONFIG_FILE)) {
57
176
  const content = readFileSync(CONFIG_FILE, 'utf-8');
58
- const match = content.match(/TELEGRAM_BOT_TOKEN=(.+)/);
59
- if (match) {
60
- return match[1].trim();
177
+ // Parse Telegram token
178
+ const telegramMatch = content.match(/TELEGRAM_BOT_TOKEN=(.+)/);
179
+ if (telegramMatch) {
180
+ const token = telegramMatch[1].trim();
181
+ if (token && token !== 'your_bot_token_here') {
182
+ config.telegramBotToken = token;
183
+ }
184
+ }
185
+ // Parse Feishu config
186
+ const feishuAppIdMatch = content.match(/FEISHU_APP_ID=(.+)/);
187
+ if (feishuAppIdMatch) {
188
+ config.feishuAppId = feishuAppIdMatch[1].trim();
189
+ }
190
+ const feishuSecretMatch = content.match(/FEISHU_APP_SECRET=(.+)/);
191
+ if (feishuSecretMatch) {
192
+ config.feishuAppSecret = feishuSecretMatch[1].trim();
61
193
  }
62
194
  }
195
+ // Check environment variables
196
+ if (process.env.TELEGRAM_BOT_TOKEN?.trim()) {
197
+ config.telegramBotToken = process.env.TELEGRAM_BOT_TOKEN.trim();
198
+ }
199
+ if (process.env.FEISHU_APP_ID?.trim()) {
200
+ config.feishuAppId = process.env.FEISHU_APP_ID.trim();
201
+ }
202
+ if (process.env.FEISHU_APP_SECRET?.trim()) {
203
+ config.feishuAppSecret = process.env.FEISHU_APP_SECRET.trim();
204
+ }
63
205
  // Check local .env
64
206
  const localEnv = join(process.cwd(), '.env');
65
207
  if (existsSync(localEnv)) {
66
208
  const content = readFileSync(localEnv, 'utf-8');
67
- const match = content.match(/TELEGRAM_BOT_TOKEN=(.+)/);
68
- if (match) {
69
- return match[1].trim();
209
+ const telegramMatch = content.match(/TELEGRAM_BOT_TOKEN=(.+)/);
210
+ if (telegramMatch) {
211
+ const token = telegramMatch[1].trim();
212
+ if (token && token !== 'your_bot_token_here' && !config.telegramBotToken) {
213
+ config.telegramBotToken = token;
214
+ }
215
+ }
216
+ const feishuAppIdMatch = content.match(/FEISHU_APP_ID=(.+)/);
217
+ if (feishuAppIdMatch) {
218
+ config.feishuAppId = feishuAppIdMatch[1].trim();
219
+ }
220
+ const feishuSecretMatch = content.match(/FEISHU_APP_SECRET=(.+)/);
221
+ if (feishuSecretMatch) {
222
+ config.feishuAppSecret = feishuSecretMatch[1].trim();
70
223
  }
71
224
  }
72
- return null;
225
+ return config;
73
226
  }
74
227
  async function saveConfig(token) {
75
228
  // Create config directory if needed
76
229
  if (!existsSync(CONFIG_DIR)) {
77
230
  mkdirSync(CONFIG_DIR, { recursive: true });
78
231
  }
79
- writeFileSync(CONFIG_FILE, `TELEGRAM_BOT_TOKEN=${token}\n`);
232
+ // Read existing config
233
+ let existing = '';
234
+ if (existsSync(CONFIG_FILE)) {
235
+ existing = readFileSync(CONFIG_FILE, 'utf-8');
236
+ }
237
+ // Add or update Telegram token
238
+ const lines = existing.split('\n').filter(line => !line.startsWith('TELEGRAM_BOT_TOKEN='));
239
+ lines.push(`TELEGRAM_BOT_TOKEN=${token}`);
240
+ writeFileSync(CONFIG_FILE, lines.join('\n'));
80
241
  console.log(`\n✅ Token saved to ${CONFIG_FILE}`);
81
242
  }
243
+ async function saveFeishuConfig(appId, appSecret) {
244
+ // Create config directory if needed
245
+ if (!existsSync(CONFIG_DIR)) {
246
+ mkdirSync(CONFIG_DIR, { recursive: true });
247
+ }
248
+ // Read existing config
249
+ let existing = '';
250
+ if (existsSync(CONFIG_FILE)) {
251
+ existing = readFileSync(CONFIG_FILE, 'utf-8');
252
+ }
253
+ // Filter out old Feishu config
254
+ const lines = existing.split('\n').filter(line => !line.startsWith('FEISHU_APP_ID=') &&
255
+ !line.startsWith('FEISHU_APP_SECRET='));
256
+ // Add new Feishu config
257
+ lines.push(`FEISHU_APP_ID=${appId}`);
258
+ lines.push(`FEISHU_APP_SECRET=${appSecret}`);
259
+ writeFileSync(CONFIG_FILE, lines.join('\n'));
260
+ console.log(`\n✅ Feishu config saved to ${CONFIG_FILE}`);
261
+ // Show setup guide
262
+ showFeishuSetupGuide();
263
+ }
82
264
  async function runConfig() {
83
265
  printBanner();
84
- const token = await promptToken();
85
- if (!token || token === 'your_bot_token_here') {
86
- console.log('\n❌ Invalid token. Please try again.');
266
+ // Let user select channel
267
+ const channel = await promptChannel();
268
+ if (channel === 'telegram') {
269
+ const token = await promptToken();
270
+ if (!token || token === 'your_bot_token_here') {
271
+ console.log('\n❌ Invalid token. Please try again.');
272
+ process.exit(1);
273
+ }
274
+ await saveConfig(token);
275
+ console.log('\n🚀 Ready! Run `opencode-remote` to start the bot.');
276
+ }
277
+ else {
278
+ const { appId, appSecret } = await promptFeishuConfig();
279
+ if (!appId || !appSecret) {
280
+ console.log('\n❌ Invalid credentials. Please try again.');
281
+ process.exit(1);
282
+ }
283
+ await saveFeishuConfig(appId, appSecret);
284
+ console.log('\n🚀 Ready! Run `opencode-remote feishu` to start the Feishu bot.');
285
+ }
286
+ process.exit(0);
287
+ }
288
+ async function runConfigFeishu() {
289
+ printBanner();
290
+ const { appId, appSecret } = await promptFeishuConfig();
291
+ if (!appId || !appSecret) {
292
+ console.log('\n❌ Invalid credentials. Please try again.');
87
293
  process.exit(1);
88
294
  }
89
- await saveConfig(token);
295
+ await saveFeishuConfig(appId, appSecret);
90
296
  console.log('\n🚀 Ready! Run `opencode-remote` to start the bot.');
297
+ process.exit(0);
298
+ }
299
+ function hasTelegramConfig(config) {
300
+ return !!(config.telegramBotToken?.trim());
301
+ }
302
+ function hasFeishuConfig(config) {
303
+ return !!(config.feishuAppId?.trim() &&
304
+ config.feishuAppSecret?.trim());
91
305
  }
92
306
  async function runStart() {
307
+ const config = await getConfig();
93
308
  printBanner();
94
- const token = await getConfig();
95
- if (!token) {
96
- console.log('⚠️ No bot token configured.\n');
97
- await runConfig();
98
- return;
99
- }
100
- // Set token in environment
101
- process.env.TELEGRAM_BOT_TOKEN = token;
102
- console.log('🚀 Starting bot...\n');
309
+ // Check what's configured
310
+ const hasTelegram = hasTelegramConfig(config);
311
+ const hasFeishu = hasFeishuConfig(config);
312
+ if (!hasTelegram && !hasFeishu) {
313
+ console.log('❌ No bots configured!');
314
+ console.log('\nRun one of:');
315
+ console.log(' opencode-remote config # Configure Telegram');
316
+ console.log(' opencode-remote config-feishu # Configure Feishu');
317
+ process.exit(1);
318
+ }
319
+ // Start bots
320
+ const promises = [];
321
+ if (hasTelegram) {
322
+ console.log('🤖 Starting Telegram bot...');
323
+ process.env.TELEGRAM_BOT_TOKEN = config.telegramBotToken;
324
+ promises.push(startBot().catch((err) => {
325
+ console.error('Telegram bot failed:', err);
326
+ return { status: 'rejected', reason: err };
327
+ }));
328
+ }
329
+ if (hasFeishu) {
330
+ console.log('🤖 Starting Feishu bot...');
331
+ promises.push(startFeishuBot(config).catch((err) => {
332
+ console.error('Feishu bot failed:', err);
333
+ return { status: 'rejected', reason: err };
334
+ }));
335
+ }
336
+ // Wait for all bots
337
+ const results = await Promise.allSettled(promises);
338
+ const failed = results.filter((r) => r.status === 'rejected');
339
+ if (failed.length > 0) {
340
+ console.log(`\n⚠️ ${failed.length} bot(s) failed to start`);
341
+ process.exit(1);
342
+ }
343
+ console.log('\n✅ All bots started!');
344
+ }
345
+ async function runTelegramOnly() {
346
+ const config = await getConfig();
347
+ if (!hasTelegramConfig(config)) {
348
+ console.log('❌ Telegram bot not configured!');
349
+ console.log('\nRun: opencode-remote config');
350
+ process.exit(1);
351
+ }
352
+ printBanner();
353
+ console.log('🤖 Starting Telegram bot...');
354
+ process.env.TELEGRAM_BOT_TOKEN = config.telegramBotToken;
103
355
  try {
104
356
  await startBot();
105
357
  }
@@ -108,6 +360,23 @@ async function runStart() {
108
360
  process.exit(1);
109
361
  }
110
362
  }
363
+ async function runFeishuOnly() {
364
+ const config = await getConfig();
365
+ if (!hasFeishuConfig(config)) {
366
+ console.log('❌ Feishu bot not configured!');
367
+ console.log('\nRun: opencode-remote config-feishu');
368
+ process.exit(1);
369
+ }
370
+ printBanner();
371
+ console.log('🤖 Starting Feishu bot...');
372
+ try {
373
+ await startFeishuBot(config);
374
+ }
375
+ catch (error) {
376
+ console.error('Failed to start:', error);
377
+ process.exit(1);
378
+ }
379
+ }
111
380
  // Main CLI
112
381
  const args = process.argv.slice(2);
113
382
  const command = args[0] || 'start';
@@ -115,9 +384,18 @@ switch (command) {
115
384
  case 'start':
116
385
  runStart();
117
386
  break;
387
+ case 'telegram':
388
+ runTelegramOnly();
389
+ break;
390
+ case 'feishu':
391
+ runFeishuOnly();
392
+ break;
118
393
  case 'config':
119
394
  runConfig();
120
395
  break;
396
+ case 'config-feishu':
397
+ runConfigFeishu();
398
+ break;
121
399
  case 'help':
122
400
  case '--help':
123
401
  case '-h':
@@ -13,13 +13,13 @@ export function createHandler(deps) {
13
13
  return;
14
14
  }
15
15
  // It's a prompt - send to OpenCode
16
- await deps.sendTyping(ctx.threadId);
16
+ await deps.sendTypingIndicator(ctx.threadId);
17
17
  // TODO: Actually send to OpenCode SDK
18
18
  // For now, echo back
19
- await deps.sendMessage(ctx.threadId, TEMPLATES.thinking());
19
+ await deps.reply(ctx.threadId, TEMPLATES.thinking());
20
20
  // Simulate response
21
21
  setTimeout(async () => {
22
- await deps.sendMessage(ctx.threadId, TEMPLATES.taskCompleted([
22
+ await deps.reply(ctx.threadId, TEMPLATES.taskCompleted([
23
23
  { path: 'src/example.ts', additions: 10, deletions: 2 }
24
24
  ]));
25
25
  }, 1000);
@@ -31,7 +31,7 @@ export function createHandler(deps) {
31
31
  switch (command) {
32
32
  case '/start':
33
33
  case '/help':
34
- await deps.sendMessage(ctx.threadId, TEMPLATES.botStarted());
34
+ await deps.reply(ctx.threadId, TEMPLATES.botStarted());
35
35
  break;
36
36
  case '/approve':
37
37
  await this.handleApprove(ctx, session);
@@ -46,67 +46,67 @@ export function createHandler(deps) {
46
46
  await this.handleFiles(ctx, session);
47
47
  break;
48
48
  case '/status':
49
- await deps.sendMessage(ctx.threadId, `✅ Connected\n\n💬 Session: ${session.id.slice(0, 8)}\n⏰ Idle: ${Math.round((Date.now() - session.lastActivity) / 1000)}s`);
49
+ await deps.reply(ctx.threadId, `✅ Connected\n\n💬 Session: ${session.id.slice(0, 8)}\n⏰ Idle: ${Math.round((Date.now() - session.lastActivity) / 1000)}s`);
50
50
  break;
51
51
  case '/reset':
52
52
  session.pendingApprovals = [];
53
53
  session.opencodeSessionId = undefined;
54
- await deps.sendMessage(ctx.threadId, '🔄 Session reset. Start fresh!');
54
+ await deps.reply(ctx.threadId, '🔄 Session reset. Start fresh!');
55
55
  break;
56
56
  default:
57
- await deps.sendMessage(ctx.threadId, `${EMOJI.WARNING} Unknown command: ${command}\n\nTry /help`);
57
+ await deps.reply(ctx.threadId, `${EMOJI.WARNING} Unknown command: ${command}\n\nTry /help`);
58
58
  }
59
59
  },
60
60
  // Handle /approve
61
61
  async handleApprove(ctx, session) {
62
62
  const pending = session.pendingApprovals[0];
63
63
  if (!pending) {
64
- await deps.sendMessage(ctx.threadId, '🤷 Nothing to approve right now');
64
+ await deps.reply(ctx.threadId, '🤷 Nothing to approve right now');
65
65
  return;
66
66
  }
67
67
  // Resolve the approval
68
68
  // TODO: Actually apply changes via OpenCode SDK
69
- await deps.sendMessage(ctx.threadId, TEMPLATES.approved());
69
+ await deps.reply(ctx.threadId, TEMPLATES.approved());
70
70
  },
71
71
  // Handle /reject
72
72
  async handleReject(ctx, session) {
73
73
  const pending = session.pendingApprovals[0];
74
74
  if (!pending) {
75
- await deps.sendMessage(ctx.threadId, '🤷 Nothing to reject right now');
75
+ await deps.reply(ctx.threadId, '🤷 Nothing to reject right now');
76
76
  return;
77
77
  }
78
78
  session.pendingApprovals.shift();
79
- await deps.sendMessage(ctx.threadId, TEMPLATES.rejected());
79
+ await deps.reply(ctx.threadId, TEMPLATES.rejected());
80
80
  },
81
81
  // Handle /diff
82
82
  async handleDiff(ctx, session) {
83
83
  const pending = session.pendingApprovals[0];
84
84
  if (!pending || !pending.files?.length) {
85
- await deps.sendMessage(ctx.threadId, '📄 No pending changes to show');
85
+ await deps.reply(ctx.threadId, '📄 No pending changes to show');
86
86
  return;
87
87
  }
88
88
  // TODO: Get actual diff from OpenCode SDK
89
89
  const diffPreview = pending.files.map(f => `--- a/${f.path}\n+++ b/${f.path}\n@@ changes +${f.additions} +${f.deletions} @@`).join('\n');
90
90
  const messages = splitMessage(`\`\`\`diff\n${diffPreview}\n\`\`\``);
91
91
  for (const msg of messages) {
92
- await deps.sendMessage(ctx.threadId, msg);
92
+ await deps.reply(ctx.threadId, msg);
93
93
  }
94
94
  },
95
95
  // Handle /files
96
96
  async handleFiles(ctx, session) {
97
97
  const pending = session.pendingApprovals[0];
98
98
  if (!pending || !pending.files?.length) {
99
- await deps.sendMessage(ctx.threadId, '📄 No files changed in this session');
99
+ await deps.reply(ctx.threadId, '📄 No files changed in this session');
100
100
  return;
101
101
  }
102
102
  const fileList = pending.files.map(f => `• ${f.path} (+${f.additions}, -${f.deletions})`).join('\n');
103
- await deps.sendMessage(ctx.threadId, `📄 Changed files:\n${fileList}`);
103
+ await deps.reply(ctx.threadId, `📄 Changed files:\n${fileList}`);
104
104
  },
105
105
  // Request approval from user
106
106
  async requestApproval(ctx, session, type, data) {
107
107
  const request = createApprovalRequest(session, type, data);
108
108
  const message = formatApprovalMessage(request);
109
- await deps.sendMessage(ctx.threadId, message);
109
+ await deps.reply(ctx.threadId, message);
110
110
  // Wait for user response
111
111
  return waitForApproval(request);
112
112
  }