opencode-remote-control 0.1.0 → 0.1.3

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.
16
+ </p>
6
17
 
7
18
  ## Installation
8
19
 
@@ -13,19 +24,6 @@ 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
- ```
20
-
21
- ### Install from Source
22
-
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
27
  ```
30
28
 
31
29
  ## Setup
@@ -38,16 +36,26 @@ On first run, you'll be prompted for a Telegram bot token:
38
36
 
39
37
  Token is saved to `~/.opencode-remote/.env`
40
38
 
41
- ## Commands
39
+ ## Usage
42
40
 
43
- **CLI:**
44
- ```
41
+ ```bash
45
42
  opencode-remote # Start the bot
46
43
  opencode-remote config # Reconfigure token
47
44
  opencode-remote help # Show help
48
45
  ```
49
46
 
50
- **Telegram:**
47
+ ## Install from Source
48
+
49
+ ```bash
50
+ git clone https://github.com/ceociocto/opencode-remote-control.git
51
+ cd opencode-remote-control
52
+ bun install
53
+ bun run build
54
+ node dist/cli.js
55
+ ```
56
+
57
+ ## Telegram Commands
58
+
51
59
  | Command | Description |
52
60
  |--------|-------------|
53
61
  | `/start` | Start the bot |
package/dist/cli.js CHANGED
@@ -48,16 +48,20 @@ async function promptToken() {
48
48
  return token;
49
49
  }
50
50
  async function getConfig() {
51
- // Check environment variable first
52
- if (process.env.TELEGRAM_BOT_TOKEN) {
53
- return process.env.TELEGRAM_BOT_TOKEN;
51
+ // Check environment variable first (must be non-empty)
52
+ const envToken = process.env.TELEGRAM_BOT_TOKEN;
53
+ if (envToken && envToken.trim()) {
54
+ return envToken.trim();
54
55
  }
55
56
  // Check config file
56
57
  if (existsSync(CONFIG_FILE)) {
57
58
  const content = readFileSync(CONFIG_FILE, 'utf-8');
58
59
  const match = content.match(/TELEGRAM_BOT_TOKEN=(.+)/);
59
60
  if (match) {
60
- return match[1].trim();
61
+ const token = match[1].trim();
62
+ if (token && token !== 'your_bot_token_here') {
63
+ return token;
64
+ }
61
65
  }
62
66
  }
63
67
  // Check local .env
@@ -66,7 +70,10 @@ async function getConfig() {
66
70
  const content = readFileSync(localEnv, 'utf-8');
67
71
  const match = content.match(/TELEGRAM_BOT_TOKEN=(.+)/);
68
72
  if (match) {
69
- return match[1].trim();
73
+ const token = match[1].trim();
74
+ if (token && token !== 'your_bot_token_here') {
75
+ return token;
76
+ }
70
77
  }
71
78
  }
72
79
  return null;
@@ -88,18 +95,14 @@ async function runConfig() {
88
95
  }
89
96
  await saveConfig(token);
90
97
  console.log('\n🚀 Ready! Run `opencode-remote` to start the bot.');
98
+ process.exit(0);
91
99
  }
92
100
  async function runStart() {
93
- printBanner();
94
101
  const token = await getConfig();
95
- if (!token) {
96
- console.log('⚠️ No bot token configured.\n');
97
- await runConfig();
98
- return;
102
+ // Set token in environment for startBot to use
103
+ if (token) {
104
+ process.env.TELEGRAM_BOT_TOKEN = token;
99
105
  }
100
- // Set token in environment
101
- process.env.TELEGRAM_BOT_TOKEN = token;
102
- console.log('🚀 Starting bot...\n');
103
106
  try {
104
107
  await startBot();
105
108
  }
@@ -4,24 +4,29 @@ import { loadConfig } from '../core/types.js';
4
4
  import { initSessionManager, getOrCreateSession } from '../core/session.js';
5
5
  import { splitMessage } from '../core/notifications.js';
6
6
  import { initOpenCode, createSession, sendMessage, checkConnection } from '../opencode/client.js';
7
- const config = loadConfig();
8
- // Create bot instance
9
- const bot = new Bot(config.telegramBotToken);
10
- // Initialize session manager
11
- initSessionManager(config);
12
- // Store OpenCode sessions by thread ID
13
- const openCodeSessions = new Map();
14
- // Start command
15
- bot.command('start', async (ctx) => {
16
- await ctx.reply(`🚀 OpenCode Remote Control ready
7
+ // Lazy initialization - bot is only created when startBot() is called
8
+ let config = null;
9
+ let bot = null;
10
+ let openCodeSessions = null;
11
+ // Helper to get thread ID
12
+ function getThreadId(ctx) {
13
+ const chatId = ctx.chat?.id;
14
+ const threadId = ctx.message?.message_thread_id || ctx.message?.message_id;
15
+ return `${chatId}:${threadId}`;
16
+ }
17
+ // Setup bot commands
18
+ function setupBotCommands(bot, openCodeSessions) {
19
+ // Start command
20
+ bot.command('start', async (ctx) => {
21
+ await ctx.reply(`🚀 OpenCode Remote Control ready
17
22
 
18
23
  💬 Send me a prompt to start coding
19
24
  /help — see all commands
20
25
  /status — check OpenCode connection`);
21
- });
22
- // Help command
23
- bot.command('help', async (ctx) => {
24
- await ctx.reply(`📖 Commands
26
+ });
27
+ // Help command
28
+ bot.command('help', async (ctx) => {
29
+ await ctx.reply(`📖 Commands
25
30
 
26
31
  /start — Start bot
27
32
  /status — Check connection
@@ -32,169 +37,194 @@ bot.command('help', async (ctx) => {
32
37
  /files — List changed files
33
38
 
34
39
  💬 Anything else is treated as a prompt for OpenCode!`);
35
- });
36
- // Status command
37
- bot.command('status', async (ctx) => {
38
- const threadId = getThreadId(ctx);
39
- const session = getOrCreateSession(threadId, 'telegram');
40
- const openCodeSession = openCodeSessions.get(threadId);
41
- // Check OpenCode connection
42
- const connected = await checkConnection();
43
- if (!connected) {
44
- await ctx.reply(`❌ OpenCode is offline
40
+ });
41
+ // Status command
42
+ bot.command('status', async (ctx) => {
43
+ const threadId = getThreadId(ctx);
44
+ const session = getOrCreateSession(threadId, 'telegram');
45
+ const openCodeSession = openCodeSessions.get(threadId);
46
+ // Check OpenCode connection
47
+ const connected = await checkConnection();
48
+ if (!connected) {
49
+ await ctx.reply(`❌ OpenCode is offline
45
50
 
46
51
  Cannot connect to OpenCode server.
47
52
 
48
53
  🔄 /retry — check again`);
49
- return;
50
- }
51
- const idleSeconds = Math.round((Date.now() - session.lastActivity) / 1000);
52
- const pendingCount = session.pendingApprovals.length;
53
- await ctx.reply(`✅ Connected
54
+ return;
55
+ }
56
+ const idleSeconds = Math.round((Date.now() - session.lastActivity) / 1000);
57
+ const pendingCount = session.pendingApprovals.length;
58
+ await ctx.reply(`✅ Connected
54
59
 
55
60
  💬 Session: ${openCodeSession?.sessionId?.slice(0, 8) || 'none'}
56
61
  ⏰ Idle: ${idleSeconds}s
57
62
  📝 Pending approvals: ${pendingCount}`);
58
- });
59
- // Approve command
60
- bot.command('approve', async (ctx) => {
61
- const threadId = getThreadId(ctx);
62
- const session = getOrCreateSession(threadId, 'telegram');
63
- if (session.pendingApprovals.length === 0) {
64
- await ctx.reply('🤷 Nothing to approve right now');
65
- return;
66
- }
67
- // Remove first pending approval
68
- session.pendingApprovals.shift();
69
- await ctx.reply('✅ Approved — changes applied');
70
- });
71
- // Reject command
72
- bot.command('reject', async (ctx) => {
73
- const threadId = getThreadId(ctx);
74
- const session = getOrCreateSession(threadId, 'telegram');
75
- if (session.pendingApprovals.length === 0) {
76
- await ctx.reply('🤷 Nothing to reject right now');
77
- return;
78
- }
79
- session.pendingApprovals.shift();
80
- await ctx.reply('❌ Rejected — changes discarded');
81
- });
82
- // Reset command
83
- bot.command('reset', async (ctx) => {
84
- const threadId = getThreadId(ctx);
85
- const session = getOrCreateSession(threadId, 'telegram');
86
- session.pendingApprovals = [];
87
- session.opencodeSessionId = undefined;
88
- // Clear OpenCode session
89
- openCodeSessions.delete(threadId);
90
- await ctx.reply('🔄 Session reset. Start fresh!');
91
- });
92
- // Diff command
93
- bot.command('diff', async (ctx) => {
94
- const threadId = getThreadId(ctx);
95
- const session = getOrCreateSession(threadId, 'telegram');
96
- const pending = session.pendingApprovals[0];
97
- if (!pending?.files?.length) {
98
- await ctx.reply('📄 No pending changes to show');
99
- return;
100
- }
101
- // Show file list with changes
102
- const fileList = pending.files.map(f => `• ${f.path} (+${f.additions}, -${f.deletions})`).join('\n');
103
- await ctx.reply(`📄 Pending changes:\n\n${fileList}\n\n💬 /approve or /reject`);
104
- });
105
- // Files command
106
- bot.command('files', async (ctx) => {
107
- const threadId = getThreadId(ctx);
108
- const session = getOrCreateSession(threadId, 'telegram');
109
- const pending = session.pendingApprovals[0];
110
- if (!pending?.files?.length) {
111
- await ctx.reply('📄 No files changed in this session');
112
- return;
113
- }
114
- const fileList = pending.files.map(f => `• ${f.path} (+${f.additions}, -${f.deletions})`).join('\n');
115
- await ctx.reply(`📄 Changed files:\n\n${fileList}`);
116
- });
117
- // Retry command
118
- bot.command('retry', async (ctx) => {
119
- const connected = await checkConnection();
120
- if (connected) {
121
- await ctx.reply('✅ OpenCode is now online!');
122
- }
123
- else {
124
- await ctx.reply('❌ Still offline. Is OpenCode running?');
125
- }
126
- });
127
- // Handle all other messages as prompts
128
- bot.on('message:text', async (ctx) => {
129
- const text = ctx.message.text;
130
- // Skip if it's a command (already handled)
131
- if (text.startsWith('/'))
132
- return;
133
- const threadId = getThreadId(ctx);
134
- // Send typing indicator
135
- await ctx.api.sendChatAction(ctx.chat.id, 'typing');
136
- // Get or create session
137
- const session = getOrCreateSession(threadId, 'telegram');
138
- // Check OpenCode connection
139
- const connected = await checkConnection();
140
- if (!connected) {
141
- await ctx.reply(`❌ OpenCode is offline
63
+ });
64
+ // Approve command
65
+ bot.command('approve', async (ctx) => {
66
+ const threadId = getThreadId(ctx);
67
+ const session = getOrCreateSession(threadId, 'telegram');
68
+ if (session.pendingApprovals.length === 0) {
69
+ await ctx.reply('🤷 Nothing to approve right now');
70
+ return;
71
+ }
72
+ // Remove first pending approval
73
+ session.pendingApprovals.shift();
74
+ await ctx.reply('✅ Approved — changes applied');
75
+ });
76
+ // Reject command
77
+ bot.command('reject', async (ctx) => {
78
+ const threadId = getThreadId(ctx);
79
+ const session = getOrCreateSession(threadId, 'telegram');
80
+ if (session.pendingApprovals.length === 0) {
81
+ await ctx.reply('🤷 Nothing to reject right now');
82
+ return;
83
+ }
84
+ session.pendingApprovals.shift();
85
+ await ctx.reply('❌ Rejected — changes discarded');
86
+ });
87
+ // Reset command
88
+ bot.command('reset', async (ctx) => {
89
+ const threadId = getThreadId(ctx);
90
+ const session = getOrCreateSession(threadId, 'telegram');
91
+ session.pendingApprovals = [];
92
+ session.opencodeSessionId = undefined;
93
+ // Clear OpenCode session
94
+ openCodeSessions.delete(threadId);
95
+ await ctx.reply('🔄 Session reset. Start fresh!');
96
+ });
97
+ // Diff command
98
+ bot.command('diff', async (ctx) => {
99
+ const threadId = getThreadId(ctx);
100
+ const session = getOrCreateSession(threadId, 'telegram');
101
+ const pending = session.pendingApprovals[0];
102
+ if (!pending?.files?.length) {
103
+ await ctx.reply('📄 No pending changes to show');
104
+ return;
105
+ }
106
+ // Show file list with changes
107
+ const fileList = pending.files.map(f => `• ${f.path} (+${f.additions}, -${f.deletions})`).join('\n');
108
+ await ctx.reply(`📄 Pending changes:\n\n${fileList}\n\n💬 /approve or /reject`);
109
+ });
110
+ // Files command
111
+ bot.command('files', async (ctx) => {
112
+ const threadId = getThreadId(ctx);
113
+ const session = getOrCreateSession(threadId, 'telegram');
114
+ const pending = session.pendingApprovals[0];
115
+ if (!pending?.files?.length) {
116
+ await ctx.reply('📄 No files changed in this session');
117
+ return;
118
+ }
119
+ const fileList = pending.files.map(f => `• ${f.path} (+${f.additions}, -${f.deletions})`).join('\n');
120
+ await ctx.reply(`📄 Changed files:\n\n${fileList}`);
121
+ });
122
+ // Retry command
123
+ bot.command('retry', async (ctx) => {
124
+ const connected = await checkConnection();
125
+ if (connected) {
126
+ await ctx.reply('✅ OpenCode is now online!');
127
+ }
128
+ else {
129
+ await ctx.reply('❌ Still offline. Is OpenCode running?');
130
+ }
131
+ });
132
+ // Handle all other messages as prompts
133
+ bot.on('message:text', async (ctx) => {
134
+ const text = ctx.message.text;
135
+ // Skip if it's a command (already handled)
136
+ if (text.startsWith('/'))
137
+ return;
138
+ const threadId = getThreadId(ctx);
139
+ // Send typing indicator
140
+ await ctx.api.sendChatAction(ctx.chat.id, 'typing');
141
+ // Get or create session
142
+ const session = getOrCreateSession(threadId, 'telegram');
143
+ // Check OpenCode connection
144
+ const connected = await checkConnection();
145
+ if (!connected) {
146
+ await ctx.reply(`❌ OpenCode is offline
142
147
 
143
148
  Cannot connect to OpenCode server.
144
149
 
145
150
  🔄 /retry — check again`);
146
- return;
147
- }
148
- // Get or create OpenCode session
149
- let openCodeSession = openCodeSessions.get(threadId);
150
- if (!openCodeSession) {
151
- await ctx.reply('⏳ Creating session...');
152
- const newSession = await createSession(threadId, `Telegram thread ${threadId}`);
153
- if (!newSession) {
154
- await ctx.reply('❌ Failed to create OpenCode session');
155
151
  return;
156
152
  }
157
- openCodeSession = newSession;
158
- openCodeSessions.set(threadId, openCodeSession);
159
- session.opencodeSessionId = openCodeSession.sessionId;
160
- // Share the session URL
161
- if (openCodeSession.shareUrl) {
162
- await ctx.reply(`🔗 Session: ${openCodeSession.shareUrl}`);
153
+ // Get or create OpenCode session
154
+ let openCodeSession = openCodeSessions.get(threadId);
155
+ if (!openCodeSession) {
156
+ await ctx.reply('⏳ Creating session...');
157
+ const newSession = await createSession(threadId, `Telegram thread ${threadId}`);
158
+ if (!newSession) {
159
+ await ctx.reply('❌ Failed to create OpenCode session');
160
+ return;
161
+ }
162
+ openCodeSession = newSession;
163
+ openCodeSessions.set(threadId, openCodeSession);
164
+ session.opencodeSessionId = openCodeSession.sessionId;
165
+ // Share the session URL
166
+ if (openCodeSession.shareUrl) {
167
+ await ctx.reply(`🔗 Session: ${openCodeSession.shareUrl}`);
168
+ }
163
169
  }
164
- }
165
- // Send prompt to OpenCode
166
- await ctx.reply('⏳ Thinking...');
167
- try {
168
- const response = await sendMessage(openCodeSession, text);
169
- // Split long messages
170
- const messages = splitMessage(response);
171
- for (const msg of messages) {
172
- await ctx.reply(msg);
170
+ // Send prompt to OpenCode
171
+ await ctx.reply('⏳ Thinking...');
172
+ try {
173
+ const response = await sendMessage(openCodeSession, text);
174
+ // Split long messages
175
+ const messages = splitMessage(response);
176
+ for (const msg of messages) {
177
+ await ctx.reply(msg);
178
+ }
173
179
  }
174
- }
175
- catch (error) {
176
- console.error('Error sending message:', error);
177
- await ctx.reply(`❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
178
- }
179
- });
180
- // Error handling
181
- bot.catch((err) => {
182
- console.error('Bot error:', err);
183
- });
184
- // Helper to get thread ID
185
- function getThreadId(ctx) {
186
- const chatId = ctx.chat?.id;
187
- const threadId = ctx.message?.message_thread_id || ctx.message?.message_id;
188
- return `${chatId}:${threadId}`;
189
- }
190
- export { bot };
191
- // Start bot function
180
+ catch (error) {
181
+ console.error('Error sending message:', error);
182
+ await ctx.reply(`❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
183
+ }
184
+ });
185
+ // Error handling
186
+ bot.catch((err) => {
187
+ console.error('Bot error:', err);
188
+ });
189
+ } // End of setupBotCommands
190
+ // Start bot function - initializes everything lazily
192
191
  export async function startBot() {
193
- if (!config.telegramBotToken) {
194
- console.error('ERROR: TELEGRAM_BOT_TOKEN not set');
195
- console.log('Get a token from @BotFather on Telegram');
192
+ // Load config
193
+ config = loadConfig();
194
+ if (!config.telegramBotToken || config.telegramBotToken === 'your_bot_token_here') {
195
+ console.log('');
196
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
197
+ console.log(' ❌ Telegram Bot Token not configured');
198
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
199
+ console.log('');
200
+ console.log(' To get your bot token:');
201
+ console.log('');
202
+ console.log(' 1. Open Telegram app');
203
+ console.log(' 2. Search for @BotFather');
204
+ console.log(' 3. Send: /newbot');
205
+ console.log(' 4. Follow the instructions to create your bot');
206
+ console.log(' 5. Copy the token (looks like: 123456789:ABCdef...)');
207
+ console.log('');
208
+ console.log(' Then run: opencode-remote config');
209
+ console.log('');
210
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
196
211
  process.exit(1);
197
212
  }
213
+ // Show banner
214
+ console.log('');
215
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
216
+ console.log(' OpenCode Remote Control');
217
+ console.log(' Control OpenCode from Telegram');
218
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
219
+ console.log('');
220
+ // Create bot instance
221
+ bot = new Bot(config.telegramBotToken);
222
+ // Initialize session manager
223
+ initSessionManager(config);
224
+ // Initialize OpenCode sessions map
225
+ openCodeSessions = new Map();
226
+ // Setup bot commands
227
+ setupBotCommands(bot, openCodeSessions);
198
228
  // Initialize OpenCode
199
229
  console.log('🔧 Initializing OpenCode...');
200
230
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-remote-control",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Control OpenCode from anywhere via Telegram",
5
5
  "type": "module",
6
6
  "bin": {