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/dist/telegram/bot.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
});
|
|
71
|
-
// Reject command
|
|
72
|
-
bot.command('reject', async (ctx) => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
});
|
|
82
|
-
// Reset command
|
|
83
|
-
bot.command('reset', async (ctx) => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
});
|
|
92
|
-
// Diff command
|
|
93
|
-
bot.command('diff', async (ctx) => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
});
|
|
105
|
-
// Files command
|
|
106
|
-
bot.command('files', async (ctx) => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
});
|
|
117
|
-
// Retry command
|
|
118
|
-
bot.command('retry', async (ctx) => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
});
|
|
127
|
-
// Handle all other messages as prompts
|
|
128
|
-
bot.on('message:text', async (ctx) => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
158
|
-
openCodeSessions.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
153
|
+
// Get or create OpenCode session
|
|
154
|
+
let openCodeSession = openCodeSessions.get(threadId);
|
|
155
|
+
if (!openCodeSession) {
|
|
156
|
+
// Keep typing indicator while 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 (only if sharing is enabled)
|
|
166
|
+
if (openCodeSession.shareUrl) {
|
|
167
|
+
await ctx.reply(`š Session: ${openCodeSession.shareUrl}`);
|
|
168
|
+
}
|
|
163
169
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
// Refresh typing indicator before sending prompt
|
|
171
|
+
await ctx.api.sendChatAction(ctx.chat.id, 'typing');
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
//
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-remote-control",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Control OpenCode from anywhere via Telegram",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Control OpenCode from anywhere via Telegram or Feishu",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"opencode-remote": "./dist/cli.js"
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
23
24
|
"@opencode-ai/sdk": "^1.2.27",
|
|
25
|
+
"@types/express": "^5.0.6",
|
|
26
|
+
"express": "^5.2.1",
|
|
24
27
|
"grammy": "^1.30.0"
|
|
25
28
|
},
|
|
26
29
|
"devDependencies": {
|
|
@@ -38,6 +41,8 @@
|
|
|
38
41
|
"keywords": [
|
|
39
42
|
"opencode",
|
|
40
43
|
"telegram",
|
|
44
|
+
"feishu",
|
|
45
|
+
"lark",
|
|
41
46
|
"remote-control",
|
|
42
47
|
"ai",
|
|
43
48
|
"coding"
|