opencode-telegram-bot 1.1.1 → 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 +21 -0
- package/dist/app.d.ts +4 -0
- package/dist/index.js +127 -0
- 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
|
|
@@ -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 {
|
|
@@ -21545,6 +21583,36 @@ async function startTelegram(options) {
|
|
|
21545
21583
|
await answerAndEdit(ctx, "Failed to dismiss question.");
|
|
21546
21584
|
}
|
|
21547
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
|
+
});
|
|
21548
21616
|
// Handle /usage command - show token and cost usage for current session
|
|
21549
21617
|
bot.command("usage", async (ctx) => {
|
|
21550
21618
|
const chatId = ctx.chat.id.toString();
|
|
@@ -21976,6 +22044,65 @@ async function startTelegram(options) {
|
|
|
21976
22044
|
catch (err) {
|
|
21977
22045
|
console.warn("[Telegram] Failed to check for pending questions:", err);
|
|
21978
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
|
+
}
|
|
21979
22106
|
if (options.launch !== false) {
|
|
21980
22107
|
try {
|
|
21981
22108
|
// Start the bot — launch() returns a promise that resolves only
|