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 +77 -23
- package/dist/cli.js +310 -32
- package/dist/core/handler-common.js +16 -16
- package/dist/core/types.js +6 -1
- package/dist/feishu/bot.js +350 -0
- package/dist/opencode/client.js +11 -8
- package/dist/telegram/bot.js +188 -158
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
# OpenCode Remote Control
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
+
## Quick Start
|
|
22
30
|
|
|
23
|
-
|
|
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
|
-
|
|
41
|
+
### Feishu Setup
|
|
42
|
+
|
|
43
|
+
Run the config command for Feishu:
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
```bash
|
|
46
|
+
opencode-remote config-feishu
|
|
44
47
|
```
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
28
|
-
opencode-remote start
|
|
29
|
-
opencode-remote
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
68
|
-
if (
|
|
69
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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.
|
|
16
|
+
await deps.sendTypingIndicator(ctx.threadId);
|
|
17
17
|
// TODO: Actually send to OpenCode SDK
|
|
18
18
|
// For now, echo back
|
|
19
|
-
await deps.
|
|
19
|
+
await deps.reply(ctx.threadId, TEMPLATES.thinking());
|
|
20
20
|
// Simulate response
|
|
21
21
|
setTimeout(async () => {
|
|
22
|
-
await deps.
|
|
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.
|
|
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.
|
|
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.
|
|
54
|
+
await deps.reply(ctx.threadId, '🔄 Session reset. Start fresh!');
|
|
55
55
|
break;
|
|
56
56
|
default:
|
|
57
|
-
await deps.
|
|
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.
|
|
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.
|
|
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.
|
|
75
|
+
await deps.reply(ctx.threadId, '🤷 Nothing to reject right now');
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
78
|
session.pendingApprovals.shift();
|
|
79
|
-
await deps.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
109
|
+
await deps.reply(ctx.threadId, message);
|
|
110
110
|
// Wait for user response
|
|
111
111
|
return waitForApproval(request);
|
|
112
112
|
}
|