@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 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.5"]
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.5`.
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
 
@@ -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(`\u2705 Chat connected!
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", { description: e.description });
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) return;
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
- text: option.label,
568
- callback_data: callbackData[index] ?? ""
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([{ text: "\u270F\uFE0F Custom answer", callback_data: callbackData[question.options.length] }]);
609
+ inlineKeyboard.push([
610
+ { text: "\u270F\uFE0F Custom answer", callback_data: callbackData[question.options.length] }
611
+ ]);
572
612
  }
573
- const header = question.header ? `\u2753 ${question.header}` : "\u2753 Question";
574
- return this.sendMessage(`${header}
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((option) => typeof option === "object" && option !== null && isQuestionOption(option));
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((question) => typeof question === "object" && question !== null && isQuestionInfo(question));
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) throw new Error("Telegram callback_data exceeds 64 bytes");
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((_, optionIndex) => buildCallbackData2(shortHash, questionIndex, optionIndex));
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
- text: multiple && selected.includes(option.label) ? `\u2705 ${option.label}` : option.label,
1089
- callback_data: buildCallbackData2(shortHash, questionIndex, optionIndex)
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([{ text: "\u270F\uFE0F Custom answer", callback_data: buildCallbackData2(shortHash, questionIndex, "c") }]);
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([{ text: "\u2705 Done", callback_data: buildCallbackData2(shortHash, questionIndex, "d") }]);
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
- const question = pending.questions[questionIndex];
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((answer, index) => `${index + 1}. ${questions[index]?.header ?? "Question"}: ${answer.join(", ") || "(empty)"}`).join("\n");
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(shortHash, questionIndex, question, selectedAnswers(pending, questionIndex));
1119
- await ctx.bot.editMessageText(messageId, questionPromptText(pending, questionIndex), { reply_markup: { inline_keyboard: inlineKeyboard } });
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(messageId, `\u2705 Answered:
1134
- ${answerSummary(pending.questions, answers)}`);
1135
- ctx.logger.info("question reply sent", { requestID: pending.requestID, sessionID: pending.sessionID });
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", { error: String(err), requestID: pending.requestID });
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({ claimsDir: ctx.claimsDir, key: `question.asked:${request.id}`, ttlMs: 5e3 });
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(firstQuestion, callbackDataForQuestion(shortHash, 0, firstQuestion)) : await ctx.bot.sendMessage(questionPromptText(pending, 0), {
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", { requestID: request.id, sessionID: request.sessionID, count: request.questions.length });
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", { error: String(err), requestID: request.id });
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), { reply_markup: { inline_keyboard: [] } });
1274
+ await ctx.bot.editMessageText(messageId, questionPromptText(pending, questionIndex), {
1275
+ reply_markup: { inline_keyboard: [] }
1276
+ });
1201
1277
  } else {
1202
- await ctx.bot.editMessageRemoveKeyboard(messageId, "\u270F\uFE0F Reply to the next message with your custom answer.");
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("Type your custom answer", "Type your answer");
1205
- pending.awaitingCustomFor = { shortHash, questionIndex, chatId, userId, promptMessageId: prompt.message_id };
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.5",
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",