opencode-telegram-bot 1.1.0 → 1.1.2
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 +22 -1
- package/dist/app.d.ts +4 -0
- package/dist/index.js +236 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -131,6 +131,26 @@ You: [tap "Bug fix"]
|
|
|
131
131
|
Bot: Answered: Bug fix
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
+
### Permission Requests
|
|
135
|
+
|
|
136
|
+
When the OpenCode agent needs permission to perform an action (e.g. edit a file, run a bash command, access an external directory), the bot forwards the request to Telegram with inline buttons:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
Bot: Permission: edit
|
|
140
|
+
`src/app.ts`
|
|
141
|
+
[Allow once] [Always allow]
|
|
142
|
+
[Reject]
|
|
143
|
+
|
|
144
|
+
You: [tap "Allow once"]
|
|
145
|
+
Bot: Allowed (once): edit
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- **Allow once** -- approve this single request
|
|
149
|
+
- **Always allow** -- approve and create a permanent allow rule in OpenCode's config
|
|
150
|
+
- **Reject** -- deny the request
|
|
151
|
+
|
|
152
|
+
If the bot restarts while a permission request is pending, it automatically re-sends the buttons on startup. Orphan permission requests (from sessions the bot doesn't know about) are automatically rejected to unblock the session.
|
|
153
|
+
|
|
134
154
|
The bot also registers these commands in Telegram's command menu (the `/` button), plus any OpenCode server commands discovered at startup.
|
|
135
155
|
|
|
136
156
|
### Verbose Mode
|
|
@@ -280,7 +300,7 @@ You: [tap "Auth refactoring"]
|
|
|
280
300
|
Bot: Switched to session: Auth refactoring
|
|
281
301
|
```
|
|
282
302
|
|
|
283
|
-
The `/sessions` list
|
|
303
|
+
The `/sessions` list shows sessions created by the Telegram bot. At the bottom of the list, a **"Show all sessions"** button lets you discover sessions created by other OpenCode clients (like the TUI via `opencode attach`). Tapping an external session adopts it and switches to it. You cannot delete the currently active session -- use `/new` first.
|
|
284
304
|
|
|
285
305
|
## Session Persistence
|
|
286
306
|
|
|
@@ -299,6 +319,7 @@ The bot is designed to survive restarts and failures:
|
|
|
299
319
|
|
|
300
320
|
- **Drop pending updates** -- On startup, stale Telegram updates are discarded so old button clicks and messages from a previous run are not replayed.
|
|
301
321
|
- **Pending question recovery** -- If the bot restarts while the agent is waiting for a question answer, the question is re-sent to Telegram on startup so the session can be unblocked.
|
|
322
|
+
- **Pending permission recovery** -- If the bot restarts while the agent is waiting for a permission approval, the permission request is re-sent to Telegram. Orphan requests (from unknown sessions) are automatically rejected.
|
|
302
323
|
- **Global error handler** -- Unhandled errors in Telegram update handlers are logged instead of crashing the process.
|
|
303
324
|
- **Handler timeout** -- Telegraf's handler timeout is set to 10 minutes (up from 90 seconds) to accommodate long-running LLM responses.
|
|
304
325
|
|
package/dist/app.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ interface OpencodeClientLike {
|
|
|
26
26
|
reply: (params: any, options?: any) => Promise<any>;
|
|
27
27
|
reject: (params: any, options?: any) => Promise<any>;
|
|
28
28
|
};
|
|
29
|
+
permission: {
|
|
30
|
+
list: (params?: any, options?: any) => Promise<any>;
|
|
31
|
+
reply: (params: any, options?: any) => Promise<any>;
|
|
32
|
+
};
|
|
29
33
|
app: {
|
|
30
34
|
agents: (params?: any, options?: any) => Promise<any>;
|
|
31
35
|
};
|
package/dist/index.js
CHANGED
|
@@ -20054,6 +20054,8 @@ async function startTelegram(options) {
|
|
|
20054
20054
|
const chatModelSearchResults = new Map();
|
|
20055
20055
|
// Map of questionId -> pending question context (for forwarding OpenCode questions to Telegram)
|
|
20056
20056
|
const pendingQuestions = new Map();
|
|
20057
|
+
// Map of requestId -> pending permission context (for forwarding OpenCode permission requests to Telegram)
|
|
20058
|
+
const pendingPermissions = new Map();
|
|
20057
20059
|
/**
|
|
20058
20060
|
* Save the chat-to-session mapping and known session IDs to disk.
|
|
20059
20061
|
*/
|
|
@@ -20686,6 +20688,42 @@ async function startTelegram(options) {
|
|
|
20686
20688
|
}
|
|
20687
20689
|
}
|
|
20688
20690
|
}
|
|
20691
|
+
else if (ev.type === "permission.asked" && ev.properties) {
|
|
20692
|
+
const permSessionId = ev.properties.sessionID;
|
|
20693
|
+
if (permSessionId === sessionId) {
|
|
20694
|
+
const requestId = ev.properties.id;
|
|
20695
|
+
const permission = ev.properties.permission;
|
|
20696
|
+
const patterns = (ev.properties.patterns || []);
|
|
20697
|
+
if (requestId) {
|
|
20698
|
+
pendingPermissions.set(requestId, {
|
|
20699
|
+
chatId: chatId.toString(),
|
|
20700
|
+
sessionId,
|
|
20701
|
+
permission,
|
|
20702
|
+
patterns,
|
|
20703
|
+
});
|
|
20704
|
+
console.log(`[Telegram] Permission request ${requestId} (${permission}) for chat ${chatId}`);
|
|
20705
|
+
let msg = `Permission: *${permission}*`;
|
|
20706
|
+
if (patterns.length > 0) {
|
|
20707
|
+
msg += "\n`" + patterns.join("`, `") + "`";
|
|
20708
|
+
}
|
|
20709
|
+
const keyboard = [
|
|
20710
|
+
[
|
|
20711
|
+
{ text: "Allow once", callback_data: `perm_once:${requestId}` },
|
|
20712
|
+
{ text: "Always allow", callback_data: `perm_always:${requestId}` },
|
|
20713
|
+
],
|
|
20714
|
+
[
|
|
20715
|
+
{ text: "Reject", callback_data: `perm_reject:${requestId}` },
|
|
20716
|
+
],
|
|
20717
|
+
];
|
|
20718
|
+
await bot.telegram.sendMessage(chatId, msg, {
|
|
20719
|
+
parse_mode: "Markdown",
|
|
20720
|
+
reply_markup: {
|
|
20721
|
+
inline_keyboard: keyboard,
|
|
20722
|
+
},
|
|
20723
|
+
});
|
|
20724
|
+
}
|
|
20725
|
+
}
|
|
20726
|
+
}
|
|
20689
20727
|
}
|
|
20690
20728
|
}
|
|
20691
20729
|
finally {
|
|
@@ -20923,7 +20961,13 @@ async function startTelegram(options) {
|
|
|
20923
20961
|
try {
|
|
20924
20962
|
const sessions = await getKnownSessions();
|
|
20925
20963
|
if (sessions.length === 0) {
|
|
20926
|
-
await ctx.reply("No sessions found
|
|
20964
|
+
await ctx.reply("No sessions found.\n\nYou can discover sessions created outside this bot:", {
|
|
20965
|
+
reply_markup: {
|
|
20966
|
+
inline_keyboard: [
|
|
20967
|
+
[{ text: "Show all sessions", callback_data: "sessions:all" }],
|
|
20968
|
+
],
|
|
20969
|
+
},
|
|
20970
|
+
});
|
|
20927
20971
|
return;
|
|
20928
20972
|
}
|
|
20929
20973
|
const activeSession = sessions.find((session) => session.id === activeSessionId);
|
|
@@ -20932,25 +20976,33 @@ async function startTelegram(options) {
|
|
|
20932
20976
|
msgLines.push(`Current session: ${activeSession.title}`);
|
|
20933
20977
|
}
|
|
20934
20978
|
const otherSessions = sessions.filter((session) => session.id !== activeSessionId);
|
|
20935
|
-
|
|
20979
|
+
const keyboard = [];
|
|
20980
|
+
if (otherSessions.length > 0) {
|
|
20981
|
+
msgLines.push("Tap a session to switch or delete.");
|
|
20982
|
+
otherSessions.forEach((session) => {
|
|
20983
|
+
keyboard.push([
|
|
20984
|
+
{
|
|
20985
|
+
text: session.title,
|
|
20986
|
+
callback_data: `switch:${session.id}`,
|
|
20987
|
+
},
|
|
20988
|
+
{
|
|
20989
|
+
text: "Delete",
|
|
20990
|
+
callback_data: `delete:${session.id}`,
|
|
20991
|
+
},
|
|
20992
|
+
]);
|
|
20993
|
+
});
|
|
20994
|
+
}
|
|
20995
|
+
else {
|
|
20936
20996
|
msgLines.push("This is your only session.");
|
|
20937
|
-
await ctx.reply(msgLines.join("\n"));
|
|
20938
|
-
return;
|
|
20939
20997
|
}
|
|
20940
|
-
|
|
20941
|
-
|
|
20942
|
-
|
|
20943
|
-
|
|
20944
|
-
|
|
20945
|
-
|
|
20946
|
-
|
|
20947
|
-
|
|
20948
|
-
{
|
|
20949
|
-
text: "Delete",
|
|
20950
|
-
callback_data: `delete:${session.id}`,
|
|
20951
|
-
},
|
|
20952
|
-
]);
|
|
20953
|
-
});
|
|
20998
|
+
// Always show "Show all sessions" button to discover sessions
|
|
20999
|
+
// created outside the Telegram bot (e.g. via opencode TUI)
|
|
21000
|
+
keyboard.push([
|
|
21001
|
+
{
|
|
21002
|
+
text: "Show all sessions",
|
|
21003
|
+
callback_data: "sessions:all",
|
|
21004
|
+
},
|
|
21005
|
+
]);
|
|
20954
21006
|
await ctx.reply(msgLines.join("\n"), {
|
|
20955
21007
|
reply_markup: {
|
|
20956
21008
|
inline_keyboard: keyboard,
|
|
@@ -21110,6 +21162,83 @@ async function startTelegram(options) {
|
|
|
21110
21162
|
await answerAndEdit(ctx, "Failed to delete session.");
|
|
21111
21163
|
}
|
|
21112
21164
|
});
|
|
21165
|
+
// Handle "Show all sessions" button - list sessions not tracked by the bot
|
|
21166
|
+
bot.action("sessions:all", async (ctx) => {
|
|
21167
|
+
const chatId = getChatIdFromContext(ctx);
|
|
21168
|
+
if (!chatId)
|
|
21169
|
+
return;
|
|
21170
|
+
try {
|
|
21171
|
+
// Delete the button message
|
|
21172
|
+
try {
|
|
21173
|
+
await ctx.deleteMessage();
|
|
21174
|
+
}
|
|
21175
|
+
catch {
|
|
21176
|
+
// Ignore if already deleted
|
|
21177
|
+
}
|
|
21178
|
+
await ctx.answerCbQuery();
|
|
21179
|
+
const list = await client.session.list({});
|
|
21180
|
+
const allSessions = (list.data || [])
|
|
21181
|
+
.filter((s) => !knownSessionIds.has(s.id) && !s.parentID)
|
|
21182
|
+
.sort((a, b) => b.time.updated - a.time.updated);
|
|
21183
|
+
if (allSessions.length === 0) {
|
|
21184
|
+
await ctx.reply("No other sessions found.");
|
|
21185
|
+
return;
|
|
21186
|
+
}
|
|
21187
|
+
const keyboard = [];
|
|
21188
|
+
allSessions.forEach((session) => {
|
|
21189
|
+
keyboard.push([
|
|
21190
|
+
{
|
|
21191
|
+
text: session.title,
|
|
21192
|
+
callback_data: `adopt:${session.id}`,
|
|
21193
|
+
},
|
|
21194
|
+
]);
|
|
21195
|
+
});
|
|
21196
|
+
await ctx.reply("Sessions created outside this bot.\nTap to adopt and switch:", {
|
|
21197
|
+
reply_markup: {
|
|
21198
|
+
inline_keyboard: keyboard,
|
|
21199
|
+
},
|
|
21200
|
+
});
|
|
21201
|
+
}
|
|
21202
|
+
catch (err) {
|
|
21203
|
+
console.error("[Telegram] Error listing all sessions:", err);
|
|
21204
|
+
await ctx.reply("Failed to list sessions.");
|
|
21205
|
+
}
|
|
21206
|
+
});
|
|
21207
|
+
// Handle adopt button - adopt an external session and switch to it
|
|
21208
|
+
bot.action(/^adopt:(.+)$/, async (ctx) => {
|
|
21209
|
+
const chatId = getChatIdFromContext(ctx);
|
|
21210
|
+
const sessionId = ctx.match?.[1];
|
|
21211
|
+
if (!chatId || !sessionId)
|
|
21212
|
+
return;
|
|
21213
|
+
try {
|
|
21214
|
+
// Verify the session still exists on the server
|
|
21215
|
+
const list = await client.session.list({});
|
|
21216
|
+
const allSessions = (list.data || []);
|
|
21217
|
+
const match = allSessions.find((s) => s.id === sessionId);
|
|
21218
|
+
if (!match) {
|
|
21219
|
+
await answerAndEdit(ctx, "Session not found. It may have been deleted.");
|
|
21220
|
+
return;
|
|
21221
|
+
}
|
|
21222
|
+
// Delete the button message
|
|
21223
|
+
try {
|
|
21224
|
+
await ctx.deleteMessage();
|
|
21225
|
+
}
|
|
21226
|
+
catch {
|
|
21227
|
+
// Ignore if already deleted
|
|
21228
|
+
}
|
|
21229
|
+
await ctx.answerCbQuery();
|
|
21230
|
+
// Adopt: add to known sessions and switch to it
|
|
21231
|
+
knownSessionIds.add(match.id);
|
|
21232
|
+
chatSessions.set(chatId, match.id);
|
|
21233
|
+
saveSessions();
|
|
21234
|
+
console.log(`[Telegram] Adopted and switched chat ${chatId} to session ${match.id}`);
|
|
21235
|
+
await ctx.reply(`Adopted and switched to session: ${match.title}`);
|
|
21236
|
+
}
|
|
21237
|
+
catch (err) {
|
|
21238
|
+
console.error("[Telegram] Error adopting session:", err);
|
|
21239
|
+
await answerAndEdit(ctx, "Failed to adopt session.");
|
|
21240
|
+
}
|
|
21241
|
+
});
|
|
21113
21242
|
// Handle /verbose command - toggle verbose mode for this chat
|
|
21114
21243
|
// Usage: /verbose, /verbose on, /verbose off
|
|
21115
21244
|
bot.command("verbose", (ctx) => {
|
|
@@ -21454,6 +21583,36 @@ async function startTelegram(options) {
|
|
|
21454
21583
|
await answerAndEdit(ctx, "Failed to dismiss question.");
|
|
21455
21584
|
}
|
|
21456
21585
|
});
|
|
21586
|
+
// Handle permission reply callbacks (from OpenCode permission.asked events)
|
|
21587
|
+
bot.action(/^perm_(once|always|reject):(.+)$/, async (ctx) => {
|
|
21588
|
+
const action = ctx.match?.[1];
|
|
21589
|
+
const requestId = ctx.match?.[2];
|
|
21590
|
+
if (!action || !requestId)
|
|
21591
|
+
return;
|
|
21592
|
+
console.log(`[Telegram] Permission ${action} callback for ${requestId}`);
|
|
21593
|
+
const pending = pendingPermissions.get(requestId);
|
|
21594
|
+
if (!pending) {
|
|
21595
|
+
await answerAndEdit(ctx, "This permission request has expired or was already answered.");
|
|
21596
|
+
return;
|
|
21597
|
+
}
|
|
21598
|
+
try {
|
|
21599
|
+
await client.permission.reply({
|
|
21600
|
+
requestID: requestId,
|
|
21601
|
+
reply: action,
|
|
21602
|
+
});
|
|
21603
|
+
pendingPermissions.delete(requestId);
|
|
21604
|
+
const labels = {
|
|
21605
|
+
once: "Allowed (once)",
|
|
21606
|
+
always: "Always allowed",
|
|
21607
|
+
reject: "Rejected",
|
|
21608
|
+
};
|
|
21609
|
+
await answerAndEdit(ctx, `${labels[action]}: ${pending.permission}`);
|
|
21610
|
+
}
|
|
21611
|
+
catch (err) {
|
|
21612
|
+
console.error("[Telegram] Error replying to permission:", err);
|
|
21613
|
+
await answerAndEdit(ctx, "Failed to respond to permission request.");
|
|
21614
|
+
}
|
|
21615
|
+
});
|
|
21457
21616
|
// Handle /usage command - show token and cost usage for current session
|
|
21458
21617
|
bot.command("usage", async (ctx) => {
|
|
21459
21618
|
const chatId = ctx.chat.id.toString();
|
|
@@ -21885,6 +22044,65 @@ async function startTelegram(options) {
|
|
|
21885
22044
|
catch (err) {
|
|
21886
22045
|
console.warn("[Telegram] Failed to check for pending questions:", err);
|
|
21887
22046
|
}
|
|
22047
|
+
// Check for pending permission requests left over from previous bot runs.
|
|
22048
|
+
try {
|
|
22049
|
+
const pendingPermResult = await client.permission.list({});
|
|
22050
|
+
if (pendingPermResult.data && pendingPermResult.data.length > 0) {
|
|
22051
|
+
console.log(`[Telegram] Found ${pendingPermResult.data.length} pending permission(s) from previous run`);
|
|
22052
|
+
for (const pp of pendingPermResult.data) {
|
|
22053
|
+
const requestId = pp.id;
|
|
22054
|
+
const permSessionId = pp.sessionID;
|
|
22055
|
+
const permission = pp.permission;
|
|
22056
|
+
const patterns = (pp.patterns || []);
|
|
22057
|
+
const chatId = sessionToChatId(permSessionId);
|
|
22058
|
+
if (!chatId || !requestId) {
|
|
22059
|
+
// No matching chat — reject the stale permission to unblock the session
|
|
22060
|
+
console.log(`[Telegram] Rejecting orphan pending permission ${requestId} (no matching chat)`);
|
|
22061
|
+
try {
|
|
22062
|
+
await client.permission.reply({ requestID: requestId, reply: "reject" });
|
|
22063
|
+
}
|
|
22064
|
+
catch (err) {
|
|
22065
|
+
console.warn(`[Telegram] Failed to reject orphan permission ${requestId}:`, err);
|
|
22066
|
+
}
|
|
22067
|
+
continue;
|
|
22068
|
+
}
|
|
22069
|
+
pendingPermissions.set(requestId, {
|
|
22070
|
+
chatId,
|
|
22071
|
+
sessionId: permSessionId,
|
|
22072
|
+
permission,
|
|
22073
|
+
patterns,
|
|
22074
|
+
});
|
|
22075
|
+
const numericChatId = Number(chatId);
|
|
22076
|
+
let msg = `Permission: *${permission}*`;
|
|
22077
|
+
if (patterns.length > 0) {
|
|
22078
|
+
msg += "\n`" + patterns.join("`, `") + "`";
|
|
22079
|
+
}
|
|
22080
|
+
msg += "\n_(resumed from previous session)_";
|
|
22081
|
+
const keyboard = [
|
|
22082
|
+
[
|
|
22083
|
+
{ text: "Allow once", callback_data: `perm_once:${requestId}` },
|
|
22084
|
+
{ text: "Always allow", callback_data: `perm_always:${requestId}` },
|
|
22085
|
+
],
|
|
22086
|
+
[
|
|
22087
|
+
{ text: "Reject", callback_data: `perm_reject:${requestId}` },
|
|
22088
|
+
],
|
|
22089
|
+
];
|
|
22090
|
+
try {
|
|
22091
|
+
await bot.telegram.sendMessage(numericChatId, msg, {
|
|
22092
|
+
parse_mode: "Markdown",
|
|
22093
|
+
reply_markup: { inline_keyboard: keyboard },
|
|
22094
|
+
});
|
|
22095
|
+
}
|
|
22096
|
+
catch (err) {
|
|
22097
|
+
console.warn(`[Telegram] Failed to re-send permission to chat ${chatId}:`, err);
|
|
22098
|
+
}
|
|
22099
|
+
console.log(`[Telegram] Re-forwarded pending permission ${requestId} to chat ${chatId}`);
|
|
22100
|
+
}
|
|
22101
|
+
}
|
|
22102
|
+
}
|
|
22103
|
+
catch (err) {
|
|
22104
|
+
console.warn("[Telegram] Failed to check for pending permissions:", err);
|
|
22105
|
+
}
|
|
21888
22106
|
if (options.launch !== false) {
|
|
21889
22107
|
try {
|
|
21890
22108
|
// Start the bot — launch() returns a promise that resolves only
|