@coinseeker/opencode-telegram-plugin 1.0.5 → 1.0.6
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 +2 -2
- package/dist/telegram-remote.js +137 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,11 +15,11 @@ Configure the npm package in `~/.config/opencode/opencode.json`:
|
|
|
15
15
|
|
|
16
16
|
```json
|
|
17
17
|
{
|
|
18
|
-
"plugin": ["@coinseeker/opencode-telegram-plugin@1.0.
|
|
18
|
+
"plugin": ["@coinseeker/opencode-telegram-plugin@1.0.6"]
|
|
19
19
|
}
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Current stable version: `@coinseeker/opencode-telegram-plugin@1.0.
|
|
22
|
+
Current stable version: `@coinseeker/opencode-telegram-plugin@1.0.6`.
|
|
23
23
|
|
|
24
24
|
Restart OpenCode after editing the config. OpenCode resolves npm package plugins on startup.
|
|
25
25
|
|
package/dist/telegram-remote.js
CHANGED
|
@@ -465,6 +465,37 @@ function loadConfig(opts) {
|
|
|
465
465
|
|
|
466
466
|
// src/bot.ts
|
|
467
467
|
import { Bot, GrammyError } from "grammy";
|
|
468
|
+
|
|
469
|
+
// src/lib/question-format.ts
|
|
470
|
+
function optionDescriptionText(question) {
|
|
471
|
+
const options = question.options.map((option, index) => {
|
|
472
|
+
const description = option.description.trim();
|
|
473
|
+
return description ? `${index + 1}. ${option.label}
|
|
474
|
+
> ${description}` : `${index + 1}. ${option.label}`;
|
|
475
|
+
});
|
|
476
|
+
return options.length > 0 ? `
|
|
477
|
+
|
|
478
|
+
${options.join("\n")}` : "";
|
|
479
|
+
}
|
|
480
|
+
function questionText(question) {
|
|
481
|
+
const header = question.header ? `\u2753 ${question.header}` : "\u2753 Question";
|
|
482
|
+
return `${header}
|
|
483
|
+
|
|
484
|
+
${question.question}${optionDescriptionText(question)}`;
|
|
485
|
+
}
|
|
486
|
+
function pendingQuestionText(questions, questionIndex) {
|
|
487
|
+
const question = questions[questionIndex];
|
|
488
|
+
const prefix = questions.length > 1 ? `Question ${questionIndex + 1}/${questions.length}
|
|
489
|
+
|
|
490
|
+
` : "";
|
|
491
|
+
const allQuestions = questions.length > 1 ? `All questions:
|
|
492
|
+
${questions.map((q, i) => `${i + 1}. ${q.header}: ${q.question}`).join("\n")}
|
|
493
|
+
|
|
494
|
+
` : "";
|
|
495
|
+
return `${allQuestions}${prefix}${questionText(question)}`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/bot.ts
|
|
468
499
|
function createTelegramBot(opts) {
|
|
469
500
|
const { config, stateStore, logger, polling } = opts;
|
|
470
501
|
const bot = new Bot(config.botToken);
|
|
@@ -485,11 +516,13 @@ function createTelegramBot(opts) {
|
|
|
485
516
|
activeChatId = newChatId;
|
|
486
517
|
await stateStore.write({ chatId: newChatId, discoveredBy: process.pid });
|
|
487
518
|
logger.info("chat_id discovered", { chatId: newChatId });
|
|
488
|
-
await ctx.reply(
|
|
519
|
+
await ctx.reply(
|
|
520
|
+
`\u2705 Chat connected!
|
|
489
521
|
|
|
490
522
|
Your chat_id: ${newChatId}
|
|
491
523
|
|
|
492
|
-
This chat is now active for OpenCode notifications.`
|
|
524
|
+
This chat is now active for OpenCode notifications.`
|
|
525
|
+
);
|
|
493
526
|
}
|
|
494
527
|
}
|
|
495
528
|
await next();
|
|
@@ -497,7 +530,9 @@ This chat is now active for OpenCode notifications.`);
|
|
|
497
530
|
bot.catch((err) => {
|
|
498
531
|
const e = err.error;
|
|
499
532
|
if (e instanceof GrammyError && e.error_code === 409) {
|
|
500
|
-
logger.info("polling conflict (409) - another process took over", {
|
|
533
|
+
logger.info("polling conflict (409) - another process took over", {
|
|
534
|
+
description: e.description
|
|
535
|
+
});
|
|
501
536
|
} else {
|
|
502
537
|
logger.error("bot error", { error: String(e) });
|
|
503
538
|
}
|
|
@@ -508,7 +543,8 @@ This chat is now active for OpenCode notifications.`);
|
|
|
508
543
|
const messageId = ctx.callbackQuery.message?.message_id;
|
|
509
544
|
const chatId = ctx.chat?.id;
|
|
510
545
|
const userId = ctx.from?.id;
|
|
511
|
-
if (!questionDispatcher || messageId === void 0 || chatId === void 0 || userId === void 0)
|
|
546
|
+
if (!questionDispatcher || messageId === void 0 || chatId === void 0 || userId === void 0)
|
|
547
|
+
return;
|
|
512
548
|
await questionDispatcher.handleCallbackQuery(data, messageId, chatId, userId);
|
|
513
549
|
});
|
|
514
550
|
bot.callbackQuery(/^p:([^:]+):(o|a|r)$/, async (ctx) => {
|
|
@@ -563,17 +599,20 @@ This chat is now active for OpenCode notifications.`);
|
|
|
563
599
|
return { message_id: result.message_id };
|
|
564
600
|
},
|
|
565
601
|
async sendQuestionWithKeyboard(question, callbackData) {
|
|
566
|
-
const inlineKeyboard = question.options.map((option, index) => [
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
602
|
+
const inlineKeyboard = question.options.map((option, index) => [
|
|
603
|
+
{
|
|
604
|
+
text: option.label,
|
|
605
|
+
callback_data: callbackData[index] ?? ""
|
|
606
|
+
}
|
|
607
|
+
]);
|
|
570
608
|
if (callbackData[question.options.length]) {
|
|
571
|
-
inlineKeyboard.push([
|
|
609
|
+
inlineKeyboard.push([
|
|
610
|
+
{ text: "\u270F\uFE0F Custom answer", callback_data: callbackData[question.options.length] }
|
|
611
|
+
]);
|
|
572
612
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
${question.question}`, { reply_markup: { inline_keyboard: inlineKeyboard } });
|
|
613
|
+
return this.sendMessage(questionText(question), {
|
|
614
|
+
reply_markup: { inline_keyboard: inlineKeyboard }
|
|
615
|
+
});
|
|
577
616
|
},
|
|
578
617
|
async editMessage(messageId, text) {
|
|
579
618
|
const chatId = await requireChatId("editMessage");
|
|
@@ -1055,7 +1094,9 @@ function isQuestionInfo(value) {
|
|
|
1055
1094
|
if (typeof value.question !== "string") return false;
|
|
1056
1095
|
if (typeof value.header !== "string") return false;
|
|
1057
1096
|
if (!Array.isArray(value.options)) return false;
|
|
1058
|
-
return value.options.every(
|
|
1097
|
+
return value.options.every(
|
|
1098
|
+
(option) => typeof option === "object" && option !== null && isQuestionOption(option)
|
|
1099
|
+
);
|
|
1059
1100
|
}
|
|
1060
1101
|
function isEventQuestionAsked(event) {
|
|
1061
1102
|
if (event.type !== "question.asked") return false;
|
|
@@ -1064,15 +1105,20 @@ function isEventQuestionAsked(event) {
|
|
|
1064
1105
|
if (typeof props.id !== "string") return false;
|
|
1065
1106
|
if (typeof props.sessionID !== "string") return false;
|
|
1066
1107
|
if (!Array.isArray(props.questions)) return false;
|
|
1067
|
-
return props.questions.every(
|
|
1108
|
+
return props.questions.every(
|
|
1109
|
+
(question) => typeof question === "object" && question !== null && isQuestionInfo(question)
|
|
1110
|
+
);
|
|
1068
1111
|
}
|
|
1069
1112
|
function buildCallbackData2(shortHash, questionIndex, optionIndex) {
|
|
1070
1113
|
const data = `q:${shortHash}:${questionIndex}:${optionIndex}`;
|
|
1071
|
-
if (Buffer.byteLength(data, "utf8") > 64)
|
|
1114
|
+
if (Buffer.byteLength(data, "utf8") > 64)
|
|
1115
|
+
throw new Error("Telegram callback_data exceeds 64 bytes");
|
|
1072
1116
|
return data;
|
|
1073
1117
|
}
|
|
1074
1118
|
function callbackDataForQuestion(shortHash, questionIndex, question) {
|
|
1075
|
-
const data = question.options.map(
|
|
1119
|
+
const data = question.options.map(
|
|
1120
|
+
(_, optionIndex) => buildCallbackData2(shortHash, questionIndex, optionIndex)
|
|
1121
|
+
);
|
|
1076
1122
|
if (question.custom !== false) data.push(buildCallbackData2(shortHash, questionIndex, "c"));
|
|
1077
1123
|
return data;
|
|
1078
1124
|
}
|
|
@@ -1084,39 +1130,44 @@ function selectedAnswers(pending, questionIndex) {
|
|
|
1084
1130
|
}
|
|
1085
1131
|
function questionInlineKeyboard(shortHash, questionIndex, question, selected) {
|
|
1086
1132
|
const multiple = question.multiple === true;
|
|
1087
|
-
const inlineKeyboard = question.options.map((option, optionIndex) => [
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1133
|
+
const inlineKeyboard = question.options.map((option, optionIndex) => [
|
|
1134
|
+
{
|
|
1135
|
+
text: multiple && selected.includes(option.label) ? `\u2705 ${option.label}` : option.label,
|
|
1136
|
+
callback_data: buildCallbackData2(shortHash, questionIndex, optionIndex)
|
|
1137
|
+
}
|
|
1138
|
+
]);
|
|
1091
1139
|
if (question.custom !== false) {
|
|
1092
|
-
inlineKeyboard.push([
|
|
1140
|
+
inlineKeyboard.push([
|
|
1141
|
+
{ text: "\u270F\uFE0F Custom answer", callback_data: buildCallbackData2(shortHash, questionIndex, "c") }
|
|
1142
|
+
]);
|
|
1093
1143
|
}
|
|
1094
1144
|
if (multiple) {
|
|
1095
|
-
inlineKeyboard.push([
|
|
1145
|
+
inlineKeyboard.push([
|
|
1146
|
+
{ text: "\u2705 Done", callback_data: buildCallbackData2(shortHash, questionIndex, "d") }
|
|
1147
|
+
]);
|
|
1096
1148
|
}
|
|
1097
1149
|
return inlineKeyboard;
|
|
1098
1150
|
}
|
|
1099
1151
|
function questionPromptText(pending, questionIndex) {
|
|
1100
|
-
|
|
1101
|
-
const prefix = pending.questions.length > 1 ? `Question ${questionIndex + 1}/${pending.questions.length}
|
|
1102
|
-
|
|
1103
|
-
` : "";
|
|
1104
|
-
const allQuestions = pending.questions.length > 1 ? `All questions:
|
|
1105
|
-
${pending.questions.map((q, i) => `${i + 1}. ${q.header}: ${q.question}`).join("\n")}
|
|
1106
|
-
|
|
1107
|
-
` : "";
|
|
1108
|
-
return `${allQuestions}${prefix}\u2753 ${question.header}
|
|
1109
|
-
|
|
1110
|
-
${question.question}`;
|
|
1152
|
+
return pendingQuestionText(pending.questions, questionIndex);
|
|
1111
1153
|
}
|
|
1112
1154
|
function answerSummary(questions, answers) {
|
|
1113
|
-
return answers.map(
|
|
1155
|
+
return answers.map(
|
|
1156
|
+
(answer, index) => `${index + 1}. ${questions[index]?.header ?? "Question"}: ${answer.join(", ") || "(empty)"}`
|
|
1157
|
+
).join("\n");
|
|
1114
1158
|
}
|
|
1115
1159
|
async function editPromptForQuestion(ctx, pending, shortHash, questionIndex) {
|
|
1116
1160
|
const messageId = pending.telegramMessageIds[0];
|
|
1117
1161
|
const question = pending.questions[questionIndex];
|
|
1118
|
-
const inlineKeyboard = questionInlineKeyboard(
|
|
1119
|
-
|
|
1162
|
+
const inlineKeyboard = questionInlineKeyboard(
|
|
1163
|
+
shortHash,
|
|
1164
|
+
questionIndex,
|
|
1165
|
+
question,
|
|
1166
|
+
selectedAnswers(pending, questionIndex)
|
|
1167
|
+
);
|
|
1168
|
+
await ctx.bot.editMessageText(messageId, questionPromptText(pending, questionIndex), {
|
|
1169
|
+
reply_markup: { inline_keyboard: inlineKeyboard }
|
|
1170
|
+
});
|
|
1120
1171
|
}
|
|
1121
1172
|
async function completeIfReady(ctx, pending, shortHash) {
|
|
1122
1173
|
const nextIndex = pending.answersInProgress.findIndex((answer) => answer === null);
|
|
@@ -1130,12 +1181,21 @@ async function completeIfReady(ctx, pending, shortHash) {
|
|
|
1130
1181
|
const messageId = pending.telegramMessageIds[0];
|
|
1131
1182
|
try {
|
|
1132
1183
|
await ctx.replyToQuestion(pending.requestID, answers);
|
|
1133
|
-
await ctx.bot.editMessageRemoveKeyboard(
|
|
1134
|
-
|
|
1135
|
-
|
|
1184
|
+
await ctx.bot.editMessageRemoveKeyboard(
|
|
1185
|
+
messageId,
|
|
1186
|
+
`\u2705 Answered:
|
|
1187
|
+
${answerSummary(pending.questions, answers)}`
|
|
1188
|
+
);
|
|
1189
|
+
ctx.logger.info("question reply sent", {
|
|
1190
|
+
requestID: pending.requestID,
|
|
1191
|
+
sessionID: pending.sessionID
|
|
1192
|
+
});
|
|
1136
1193
|
} catch (err) {
|
|
1137
1194
|
await ctx.bot.editMessageRemoveKeyboard(messageId, "\u26A0\uFE0F Failed to send answer to opencode");
|
|
1138
|
-
ctx.logger.error("failed to send question reply", {
|
|
1195
|
+
ctx.logger.error("failed to send question reply", {
|
|
1196
|
+
error: String(err),
|
|
1197
|
+
requestID: pending.requestID
|
|
1198
|
+
});
|
|
1139
1199
|
} finally {
|
|
1140
1200
|
await ctx.pendingQuestions.deletePending(shortHash);
|
|
1141
1201
|
}
|
|
@@ -1148,7 +1208,11 @@ async function expirePending2(ctx, shortHash, pending, messageId) {
|
|
|
1148
1208
|
async function handleQuestionAsked(event, ctx) {
|
|
1149
1209
|
const request = event.properties;
|
|
1150
1210
|
if (request.questions.length === 0) return;
|
|
1151
|
-
const claimed = await claimOnce({
|
|
1211
|
+
const claimed = await claimOnce({
|
|
1212
|
+
claimsDir: ctx.claimsDir,
|
|
1213
|
+
key: `question.asked:${request.id}`,
|
|
1214
|
+
ttlMs: 5e3
|
|
1215
|
+
});
|
|
1152
1216
|
if (!claimed) return;
|
|
1153
1217
|
const shortHash = createQuestionShortHash(request.id);
|
|
1154
1218
|
const firstQuestion = request.questions[0];
|
|
@@ -1164,16 +1228,26 @@ async function handleQuestionAsked(event, ctx) {
|
|
|
1164
1228
|
answersInProgress: request.questions.map(() => null)
|
|
1165
1229
|
};
|
|
1166
1230
|
try {
|
|
1167
|
-
const message = request.questions.length === 1 && useSimpleQuestionKeyboard(firstQuestion) ? await ctx.bot.sendQuestionWithKeyboard(
|
|
1231
|
+
const message = request.questions.length === 1 && useSimpleQuestionKeyboard(firstQuestion) ? await ctx.bot.sendQuestionWithKeyboard(
|
|
1232
|
+
firstQuestion,
|
|
1233
|
+
callbackDataForQuestion(shortHash, 0, firstQuestion)
|
|
1234
|
+
) : await ctx.bot.sendMessage(questionPromptText(pending, 0), {
|
|
1168
1235
|
reply_markup: {
|
|
1169
1236
|
inline_keyboard: questionInlineKeyboard(shortHash, 0, firstQuestion, [])
|
|
1170
1237
|
}
|
|
1171
1238
|
});
|
|
1172
1239
|
pending.telegramMessageIds = [message.message_id];
|
|
1173
1240
|
await ctx.pendingQuestions.savePending(shortHash, pending);
|
|
1174
|
-
ctx.logger.info("question prompt sent", {
|
|
1241
|
+
ctx.logger.info("question prompt sent", {
|
|
1242
|
+
requestID: request.id,
|
|
1243
|
+
sessionID: request.sessionID,
|
|
1244
|
+
count: request.questions.length
|
|
1245
|
+
});
|
|
1175
1246
|
} catch (err) {
|
|
1176
|
-
ctx.logger.error("failed to send question prompt", {
|
|
1247
|
+
ctx.logger.error("failed to send question prompt", {
|
|
1248
|
+
error: String(err),
|
|
1249
|
+
requestID: request.id
|
|
1250
|
+
});
|
|
1177
1251
|
}
|
|
1178
1252
|
}
|
|
1179
1253
|
function createQuestionDispatcher(ctx) {
|
|
@@ -1197,12 +1271,26 @@ function createQuestionDispatcher(ctx) {
|
|
|
1197
1271
|
if (!question) return;
|
|
1198
1272
|
if (selection === "c") {
|
|
1199
1273
|
if (question.multiple === true) {
|
|
1200
|
-
await ctx.bot.editMessageText(messageId, questionPromptText(pending, questionIndex), {
|
|
1274
|
+
await ctx.bot.editMessageText(messageId, questionPromptText(pending, questionIndex), {
|
|
1275
|
+
reply_markup: { inline_keyboard: [] }
|
|
1276
|
+
});
|
|
1201
1277
|
} else {
|
|
1202
|
-
await ctx.bot.editMessageRemoveKeyboard(
|
|
1278
|
+
await ctx.bot.editMessageRemoveKeyboard(
|
|
1279
|
+
messageId,
|
|
1280
|
+
"\u270F\uFE0F Reply to the next message with your custom answer."
|
|
1281
|
+
);
|
|
1203
1282
|
}
|
|
1204
|
-
const prompt = await ctx.bot.replyWithForceReply(
|
|
1205
|
-
|
|
1283
|
+
const prompt = await ctx.bot.replyWithForceReply(
|
|
1284
|
+
"Type your custom answer",
|
|
1285
|
+
"Type your answer"
|
|
1286
|
+
);
|
|
1287
|
+
pending.awaitingCustomFor = {
|
|
1288
|
+
shortHash,
|
|
1289
|
+
questionIndex,
|
|
1290
|
+
chatId,
|
|
1291
|
+
userId,
|
|
1292
|
+
promptMessageId: prompt.message_id
|
|
1293
|
+
};
|
|
1206
1294
|
await ctx.pendingQuestions.savePending(shortHash, pending);
|
|
1207
1295
|
return;
|
|
1208
1296
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coinseeker/opencode-telegram-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Control and monitor OpenCode from Telegram with notifications, question replies, and subagent-aware completion.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/telegram-remote.js",
|