@coinseeker/opencode-telegram-plugin 1.0.1 → 1.0.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
@@ -52,6 +52,7 @@ Keep this file private. Never commit or share your Telegram bot token.
52
52
  - Root session completion notifications.
53
53
  - Background subagent-aware completion: child session messages are suppressed and parent completion waits until children finish.
54
54
  - OpenCode question prompts via Telegram inline buttons.
55
+ - Multi-select question prompts with toggle buttons and **Done** submission.
55
56
  - Custom free-text answers from Telegram.
56
57
  - Permission alerts.
57
58
  - Multi-session-safe Telegram polling through a file-lock leader model.
@@ -238,6 +238,7 @@ function parsePending(text) {
238
238
  if (!Array.isArray(parsed.questions)) throw new Error("Invalid pending question: questions");
239
239
  if (!Array.isArray(parsed.telegramMessageIds)) throw new Error("Invalid pending question: telegramMessageIds");
240
240
  if (!Array.isArray(parsed.answersInProgress)) throw new Error("Invalid pending question: answersInProgress");
241
+ parsed.answersInProgress = parsed.answersInProgress.map((answer) => answer ?? null);
241
242
  return parsed;
242
243
  }
243
244
  async function listPendingFiles(dir) {
@@ -323,7 +324,7 @@ function loadPluginEnv(opts) {
323
324
  join3(opts.pluginDir, "../../.env"),
324
325
  join3(opts.pluginDir, "..", ".env"),
325
326
  join3(opts.pluginDir, ".env"),
326
- join3(homedir2(), ".config/opencode/telegram-remote/.env")
327
+ join3(opts.homeDir ?? homedir2(), ".config/opencode/telegram-remote/.env")
327
328
  ];
328
329
  const loadedFrom = [];
329
330
  const values = {};
@@ -414,7 +415,7 @@ This chat is now active for OpenCode notifications.`);
414
415
  logger.error("bot error", { error: String(e) });
415
416
  }
416
417
  });
417
- bot.callbackQuery(/^q:([^:]+):(\d+):(\d+|c)$/, async (ctx) => {
418
+ bot.callbackQuery(/^q:([^:]+):(\d+):(\d+|c|d)$/, async (ctx) => {
418
419
  await ctx.answerCallbackQuery();
419
420
  const data = ctx.callbackQuery.data;
420
421
  const messageId = ctx.callbackQuery.message?.message_id;
@@ -784,7 +785,7 @@ Detail: ${permission.title}`;
784
785
 
785
786
  // src/events/question-asked.ts
786
787
  var QUESTION_EXPIRY_MS = 5 * 6e4;
787
- var CALLBACK_RE = /^q:([^:]+):(\d+):(\d+|c)$/;
788
+ var CALLBACK_RE = /^q:([^:]+):(\d+):(\d+|c|d)$/;
788
789
  function isQuestionOption(value) {
789
790
  return typeof value.label === "string" && typeof value.description === "string";
790
791
  }
@@ -813,6 +814,26 @@ function callbackDataForQuestion(shortHash, questionIndex, question) {
813
814
  if (question.custom !== false) data.push(buildCallbackData(shortHash, questionIndex, "c"));
814
815
  return data;
815
816
  }
817
+ function useSimpleQuestionKeyboard(question) {
818
+ return question.multiple !== true;
819
+ }
820
+ function selectedAnswers(pending, questionIndex) {
821
+ return pending.answersInProgress[questionIndex] ?? [];
822
+ }
823
+ function questionInlineKeyboard(shortHash, questionIndex, question, selected) {
824
+ const multiple = question.multiple === true;
825
+ const inlineKeyboard = question.options.map((option, optionIndex) => [{
826
+ text: multiple && selected.includes(option.label) ? `\u2705 ${option.label}` : option.label,
827
+ callback_data: buildCallbackData(shortHash, questionIndex, optionIndex)
828
+ }]);
829
+ if (question.custom !== false) {
830
+ inlineKeyboard.push([{ text: "\u270F\uFE0F Custom answer", callback_data: buildCallbackData(shortHash, questionIndex, "c") }]);
831
+ }
832
+ if (multiple) {
833
+ inlineKeyboard.push([{ text: "\u2705 Done", callback_data: buildCallbackData(shortHash, questionIndex, "d") }]);
834
+ }
835
+ return inlineKeyboard;
836
+ }
816
837
  function questionPromptText(pending, questionIndex) {
817
838
  const question = pending.questions[questionIndex];
818
839
  const prefix = pending.questions.length > 1 ? `Question ${questionIndex + 1}/${pending.questions.length}
@@ -832,17 +853,11 @@ function answerSummary(questions, answers) {
832
853
  async function editPromptForQuestion(ctx, pending, shortHash, questionIndex) {
833
854
  const messageId = pending.telegramMessageIds[0];
834
855
  const question = pending.questions[questionIndex];
835
- const inlineKeyboard = question.options.map((option, optionIndex) => [{
836
- text: option.label,
837
- callback_data: buildCallbackData(shortHash, questionIndex, optionIndex)
838
- }]);
839
- if (question.custom !== false) {
840
- inlineKeyboard.push([{ text: "\u270F\uFE0F Custom answer", callback_data: buildCallbackData(shortHash, questionIndex, "c") }]);
841
- }
856
+ const inlineKeyboard = questionInlineKeyboard(shortHash, questionIndex, question, selectedAnswers(pending, questionIndex));
842
857
  await ctx.bot.editMessageText(messageId, questionPromptText(pending, questionIndex), { reply_markup: { inline_keyboard: inlineKeyboard } });
843
858
  }
844
859
  async function completeIfReady(ctx, pending, shortHash) {
845
- const nextIndex = pending.answersInProgress.findIndex((answer) => answer === void 0);
860
+ const nextIndex = pending.answersInProgress.findIndex((answer) => answer === null);
846
861
  if (nextIndex >= 0) {
847
862
  pending.currentQuestionIndex = nextIndex;
848
863
  await ctx.pendingQuestions.savePending(shortHash, pending);
@@ -884,15 +899,12 @@ async function handleQuestionAsked(event, ctx) {
884
899
  expiresAt: sentAt + QUESTION_EXPIRY_MS,
885
900
  telegramMessageIds: [],
886
901
  currentQuestionIndex: 0,
887
- answersInProgress: request.questions.map(() => void 0)
902
+ answersInProgress: request.questions.map(() => null)
888
903
  };
889
904
  try {
890
- const message = request.questions.length === 1 ? await ctx.bot.sendQuestionWithKeyboard(firstQuestion, callbackDataForQuestion(shortHash, 0, firstQuestion)) : await ctx.bot.sendMessage(questionPromptText(pending, 0), {
905
+ const message = request.questions.length === 1 && useSimpleQuestionKeyboard(firstQuestion) ? await ctx.bot.sendQuestionWithKeyboard(firstQuestion, callbackDataForQuestion(shortHash, 0, firstQuestion)) : await ctx.bot.sendMessage(questionPromptText(pending, 0), {
891
906
  reply_markup: {
892
- inline_keyboard: firstQuestion.options.map((option, optionIndex) => [{
893
- text: option.label,
894
- callback_data: buildCallbackData(shortHash, 0, optionIndex)
895
- }]).concat(firstQuestion.custom !== false ? [[{ text: "\u270F\uFE0F Custom answer", callback_data: buildCallbackData(shortHash, 0, "c") }]] : [])
907
+ inline_keyboard: questionInlineKeyboard(shortHash, 0, firstQuestion, [])
896
908
  }
897
909
  });
898
910
  pending.telegramMessageIds = [message.message_id];
@@ -922,16 +934,32 @@ function createQuestionDispatcher(ctx) {
922
934
  const question = pending.questions[questionIndex];
923
935
  if (!question) return;
924
936
  if (selection === "c") {
925
- await ctx.bot.editMessageRemoveKeyboard(messageId, "\u270F\uFE0F Reply to the next message with your custom answer.");
937
+ if (question.multiple === true) {
938
+ await ctx.bot.editMessageText(messageId, questionPromptText(pending, questionIndex), { reply_markup: { inline_keyboard: [] } });
939
+ } else {
940
+ await ctx.bot.editMessageRemoveKeyboard(messageId, "\u270F\uFE0F Reply to the next message with your custom answer.");
941
+ }
926
942
  const prompt = await ctx.bot.replyWithForceReply("Type your custom answer", "Type your answer");
927
943
  pending.awaitingCustomFor = { shortHash, questionIndex, chatId, userId, promptMessageId: prompt.message_id };
928
944
  await ctx.pendingQuestions.savePending(shortHash, pending);
929
945
  return;
930
946
  }
947
+ if (selection === "d") {
948
+ if (question.multiple !== true) return;
949
+ pending.answersInProgress[questionIndex] = selectedAnswers(pending, questionIndex);
950
+ pending.awaitingCustomFor = void 0;
951
+ await completeIfReady(ctx, pending, shortHash);
952
+ return;
953
+ }
931
954
  const option = question.options[Number(selection)];
932
955
  if (!option) return;
933
956
  if (question.multiple === true) {
934
- ctx.logger.info("multiple-choice question handled as single-select", { requestID: pending.requestID, questionIndex });
957
+ const current = selectedAnswers(pending, questionIndex);
958
+ pending.answersInProgress[questionIndex] = current.includes(option.label) ? current.filter((answer) => answer !== option.label) : [...current, option.label];
959
+ pending.awaitingCustomFor = void 0;
960
+ await ctx.pendingQuestions.savePending(shortHash, pending);
961
+ await editPromptForQuestion(ctx, pending, shortHash, questionIndex);
962
+ return;
935
963
  }
936
964
  pending.answersInProgress[questionIndex] = [option.label];
937
965
  pending.awaitingCustomFor = void 0;
@@ -946,6 +974,16 @@ function createQuestionDispatcher(ctx) {
946
974
  await expirePending(ctx, match.shortHash, match.data, match.data.telegramMessageIds[0]);
947
975
  return;
948
976
  }
977
+ const question = match.data.questions[awaiting.questionIndex];
978
+ if (question?.multiple === true) {
979
+ const current = selectedAnswers(match.data, awaiting.questionIndex);
980
+ match.data.answersInProgress[awaiting.questionIndex] = current.includes(text) ? current : [...current, text];
981
+ match.data.awaitingCustomFor = void 0;
982
+ await ctx.bot.sendMessage("\u2705 Custom answer added. Tap Done when finished.");
983
+ await ctx.pendingQuestions.savePending(match.shortHash, match.data);
984
+ await editPromptForQuestion(ctx, match.data, match.shortHash, awaiting.questionIndex);
985
+ return;
986
+ }
949
987
  match.data.answersInProgress[awaiting.questionIndex] = [text];
950
988
  match.data.awaitingCustomFor = void 0;
951
989
  await ctx.bot.sendMessage("\u2705 Custom answer sent.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinseeker/opencode-telegram-plugin",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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",