@coinseeker/opencode-telegram-plugin 1.0.3 → 1.0.4

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
@@ -54,7 +54,7 @@ Keep this file private. Never commit or share your Telegram bot token.
54
54
  - OpenCode question prompts via Telegram inline buttons.
55
55
  - Multi-select question prompts with toggle buttons and **Done** submission.
56
56
  - Custom free-text answers from Telegram.
57
- - Permission alerts.
57
+ - Permission approve/reject buttons from Telegram.
58
58
  - Multi-session-safe Telegram polling through a file-lock leader model.
59
59
  - Log file output instead of stdout terminal spam.
60
60
 
@@ -5,9 +5,9 @@
5
5
 
6
6
  // src/telegram-remote.ts
7
7
  import { fileURLToPath } from "url";
8
- import { dirname as dirname3, join as join5 } from "path";
9
- import { tmpdir as tmpdir3 } from "os";
10
- import { createHash as createHash3 } from "crypto";
8
+ import { dirname as dirname4, join as join6 } from "path";
9
+ import { tmpdir as tmpdir4 } from "os";
10
+ import { createHash as createHash4 } from "crypto";
11
11
 
12
12
  // src/lib/logger.ts
13
13
  import { appendFile } from "fs/promises";
@@ -314,17 +314,103 @@ function createQuestionShortHash(requestID) {
314
314
  return createHash("sha256").update(requestID).digest("base64url").slice(0, 10);
315
315
  }
316
316
 
317
+ // src/lib/pending-permissions.ts
318
+ import { createHash as createHash2 } from "crypto";
319
+ import { mkdir as mkdir3, readFile as readFile4, readdir as readdir2, rename as rename3, unlink as unlink3, writeFile as writeFile3 } from "fs/promises";
320
+ import { tmpdir as tmpdir3 } from "os";
321
+ import { dirname as dirname3, join as join3 } from "path";
322
+ function hasCode4(err, code) {
323
+ return "code" in err && err.code === code;
324
+ }
325
+ function pendingFilePath2(dir, shortHash) {
326
+ return join3(dir, `${shortHash}.json`);
327
+ }
328
+ function parsePending2(text) {
329
+ const parsed = JSON.parse(text);
330
+ if (typeof parsed.requestID !== "string") throw new Error("Invalid pending permission: requestID");
331
+ if (typeof parsed.sessionID !== "string") throw new Error("Invalid pending permission: sessionID");
332
+ if (typeof parsed.title !== "string") throw new Error("Invalid pending permission: title");
333
+ if (typeof parsed.permission !== "string") throw new Error("Invalid pending permission: permission");
334
+ if (!Array.isArray(parsed.patterns)) throw new Error("Invalid pending permission: patterns");
335
+ if (!Array.isArray(parsed.always)) throw new Error("Invalid pending permission: always");
336
+ if (parsed.endpoint !== "request" && parsed.endpoint !== "session") throw new Error("Invalid pending permission: endpoint");
337
+ return parsed;
338
+ }
339
+ async function listPendingFiles2(dir) {
340
+ try {
341
+ const entries = await readdir2(dir, { withFileTypes: true });
342
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name);
343
+ } catch (err) {
344
+ if (err instanceof Error && hasCode4(err, "ENOENT")) return [];
345
+ throw err;
346
+ }
347
+ }
348
+ function shortHashFromFileName2(fileName) {
349
+ return fileName.slice(0, -".json".length);
350
+ }
351
+ function createPendingPermissionStore(opts) {
352
+ const dir = opts.baseDir ?? join3(tmpdir3(), `opencoder-telegram-pending-permissions-${opts.tokenHash}`);
353
+ return {
354
+ dir,
355
+ async savePending(shortHash, data) {
356
+ const filePath = pendingFilePath2(dir, shortHash);
357
+ await mkdir3(dirname3(filePath), { recursive: true });
358
+ const tmpPath = `${filePath}.tmp.${process.pid}`;
359
+ await writeFile3(tmpPath, JSON.stringify(data, null, 2), "utf8");
360
+ await rename3(tmpPath, filePath);
361
+ },
362
+ async loadPending(shortHash) {
363
+ try {
364
+ return parsePending2(await readFile4(pendingFilePath2(dir, shortHash), "utf8"));
365
+ } catch (err) {
366
+ if (err instanceof Error && hasCode4(err, "ENOENT")) return void 0;
367
+ throw err;
368
+ }
369
+ },
370
+ async deletePending(shortHash) {
371
+ try {
372
+ await unlink3(pendingFilePath2(dir, shortHash));
373
+ } catch (err) {
374
+ if (!(err instanceof Error) || !hasCode4(err, "ENOENT")) throw err;
375
+ }
376
+ },
377
+ async findByRequestID(requestID) {
378
+ for (const fileName of await listPendingFiles2(dir)) {
379
+ const shortHash = shortHashFromFileName2(fileName);
380
+ const data = await this.loadPending(shortHash);
381
+ if (data?.requestID === requestID) return { shortHash, data };
382
+ }
383
+ return void 0;
384
+ },
385
+ async sweepExpired() {
386
+ const expired = [];
387
+ for (const fileName of await listPendingFiles2(dir)) {
388
+ const shortHash = shortHashFromFileName2(fileName);
389
+ const data = await this.loadPending(shortHash);
390
+ if (data && data.expiresAt < Date.now()) {
391
+ expired.push(data);
392
+ await this.deletePending(shortHash);
393
+ }
394
+ }
395
+ return expired;
396
+ }
397
+ };
398
+ }
399
+ function createPermissionShortHash(requestID) {
400
+ return createHash2("sha256").update(requestID).digest("base64url").slice(0, 10);
401
+ }
402
+
317
403
  // src/lib/env-loader.ts
318
404
  import { existsSync } from "fs";
319
405
  import { homedir as homedir2 } from "os";
320
- import { join as join3 } from "path";
406
+ import { join as join4 } from "path";
321
407
  import dotenv from "dotenv";
322
408
  function loadPluginEnv(opts) {
323
409
  const paths = [
324
- join3(opts.pluginDir, "../../.env"),
325
- join3(opts.pluginDir, "..", ".env"),
326
- join3(opts.pluginDir, ".env"),
327
- join3(opts.homeDir ?? homedir2(), ".config/opencode/telegram-remote/.env")
410
+ join4(opts.pluginDir, "../../.env"),
411
+ join4(opts.pluginDir, "..", ".env"),
412
+ join4(opts.pluginDir, ".env"),
413
+ join4(opts.homeDir ?? homedir2(), ".config/opencode/telegram-remote/.env")
328
414
  ];
329
415
  const loadedFrom = [];
330
416
  const values = {};
@@ -384,6 +470,7 @@ function createTelegramBot(opts) {
384
470
  const bot = new Bot(config.botToken);
385
471
  let activeChatId = opts.initialChatId;
386
472
  let questionDispatcher;
473
+ let permissionDispatcher;
387
474
  if (polling) {
388
475
  bot.use(async (ctx, next) => {
389
476
  const userId = ctx.from?.id;
@@ -424,6 +511,13 @@ This chat is now active for OpenCode notifications.`);
424
511
  if (!questionDispatcher || messageId === void 0 || chatId === void 0 || userId === void 0) return;
425
512
  await questionDispatcher.handleCallbackQuery(data, messageId, chatId, userId);
426
513
  });
514
+ bot.callbackQuery(/^p:([^:]+):(o|a|r)$/, async (ctx) => {
515
+ await ctx.answerCallbackQuery();
516
+ const data = ctx.callbackQuery.data;
517
+ const messageId = ctx.callbackQuery.message?.message_id;
518
+ if (!permissionDispatcher || messageId === void 0) return;
519
+ await permissionDispatcher.handleCallbackQuery(data, messageId);
520
+ });
427
521
  bot.on("message:text", async (ctx) => {
428
522
  const replyToMessageId = ctx.message.reply_to_message?.message_id;
429
523
  const chatId = ctx.chat.id;
@@ -511,6 +605,9 @@ ${question.question}`, { reply_markup: { inline_keyboard: inlineKeyboard } });
511
605
  },
512
606
  setQuestionDispatcher(dispatcher) {
513
607
  questionDispatcher = dispatcher;
608
+ },
609
+ setPermissionDispatcher(dispatcher) {
610
+ permissionDispatcher = dispatcher;
514
611
  }
515
612
  };
516
613
  }
@@ -585,29 +682,29 @@ var SessionTitleService = class {
585
682
  };
586
683
 
587
684
  // src/lib/claim.ts
588
- import { mkdir as mkdir3, open as open2, readdir as readdir2, stat as stat2, unlink as unlink3 } from "fs/promises";
589
- import { join as join4 } from "path";
590
- import { createHash as createHash2 } from "crypto";
685
+ import { mkdir as mkdir4, open as open2, readdir as readdir3, stat as stat2, unlink as unlink4 } from "fs/promises";
686
+ import { join as join5 } from "path";
687
+ import { createHash as createHash3 } from "crypto";
591
688
  var DEFAULT_TTL_MS2 = 6e4;
592
689
  var sweptDirs = /* @__PURE__ */ new Set();
593
- function hasCode4(err, code) {
690
+ function hasCode5(err, code) {
594
691
  return "code" in err && err.code === code;
595
692
  }
596
693
  function claimPath(claimsDir, key) {
597
- const hash = createHash2("sha256").update(key).digest("hex").slice(0, 16);
598
- return join4(claimsDir, `${hash}.claim`);
694
+ const hash = createHash3("sha256").update(key).digest("hex").slice(0, 16);
695
+ return join5(claimsDir, `${hash}.claim`);
599
696
  }
600
697
  async function sweep(claimsDir, ttlMs) {
601
698
  if (sweptDirs.has(claimsDir)) return;
602
699
  sweptDirs.add(claimsDir);
603
700
  try {
604
- const entries = await readdir2(claimsDir, { withFileTypes: true });
701
+ const entries = await readdir3(claimsDir, { withFileTypes: true });
605
702
  await Promise.all(entries.filter((entry) => entry.isFile() && entry.name.endsWith(".claim")).map(async (entry) => {
606
- const filePath = join4(claimsDir, entry.name);
703
+ const filePath = join5(claimsDir, entry.name);
607
704
  try {
608
705
  const fileStat = await stat2(filePath);
609
706
  if (Date.now() - fileStat.mtimeMs > ttlMs * 2) {
610
- await unlink3(filePath);
707
+ await unlink4(filePath);
611
708
  }
612
709
  } catch {
613
710
  }
@@ -626,20 +723,20 @@ async function createClaim(filePath) {
626
723
  }
627
724
  async function claimOnce(opts) {
628
725
  const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS2;
629
- await mkdir3(opts.claimsDir, { recursive: true });
726
+ await mkdir4(opts.claimsDir, { recursive: true });
630
727
  await sweep(opts.claimsDir, ttlMs);
631
728
  const filePath = claimPath(opts.claimsDir, opts.key);
632
729
  for (let attempt = 0; attempt < 2; attempt += 1) {
633
730
  try {
634
731
  return await createClaim(filePath);
635
732
  } catch (err) {
636
- if (!(err instanceof Error) || !hasCode4(err, "EEXIST")) throw err;
733
+ if (!(err instanceof Error) || !hasCode5(err, "EEXIST")) throw err;
637
734
  try {
638
735
  const fileStat = await stat2(filePath);
639
736
  if (Date.now() - fileStat.mtimeMs <= ttlMs || attempt === 1) return false;
640
- await unlink3(filePath);
737
+ await unlink4(filePath);
641
738
  } catch (statErr) {
642
- if (statErr instanceof Error && hasCode4(statErr, "ENOENT")) continue;
739
+ if (statErr instanceof Error && hasCode5(statErr, "ENOENT")) continue;
643
740
  return false;
644
741
  }
645
742
  }
@@ -764,28 +861,158 @@ async function handleSessionUpdated(event, ctx) {
764
861
  }
765
862
 
766
863
  // src/events/permission-updated.ts
767
- async function handlePermissionUpdated(event, ctx) {
768
- const permission = event.properties;
769
- const claimed = await claimOnce({ claimsDir: ctx.claimsDir, key: `permission.updated:${permission.id}` });
770
- if (!claimed) return;
771
- const sessionTitle = ctx.sessionTitleService.getSessionTitle(permission.sessionID);
864
+ var PERMISSION_EXPIRY_MS = 5 * 6e4;
865
+ var CALLBACK_RE = /^p:([^:]+):(o|a|r)$/;
866
+ function isStringArray(value) {
867
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
868
+ }
869
+ function isEventPermissionAsked(event) {
870
+ if (event.type !== "permission.asked") return false;
871
+ const props = event.properties;
872
+ if (!props) return false;
873
+ if (typeof props.id !== "string") return false;
874
+ if (typeof props.sessionID !== "string") return false;
875
+ if (typeof props.permission !== "string") return false;
876
+ if (!isStringArray(props.patterns)) return false;
877
+ if (!isStringArray(props.always)) return false;
878
+ return true;
879
+ }
880
+ function buildCallbackData(shortHash, reply) {
881
+ const data = `p:${shortHash}:${reply}`;
882
+ if (Buffer.byteLength(data, "utf8") > 64) throw new Error("Telegram callback_data exceeds 64 bytes");
883
+ return data;
884
+ }
885
+ function normalizeUpdated(permission) {
886
+ const pattern = permission.pattern === void 0 ? [] : Array.isArray(permission.pattern) ? permission.pattern : [permission.pattern];
887
+ return {
888
+ requestID: permission.id,
889
+ sessionID: permission.sessionID,
890
+ title: permission.title,
891
+ permission: permission.type,
892
+ patterns: pattern,
893
+ always: [],
894
+ endpoint: "session",
895
+ claimKey: `permission.updated:${permission.id}`
896
+ };
897
+ }
898
+ function normalizeAsked(permission) {
899
+ return {
900
+ requestID: permission.id,
901
+ sessionID: permission.sessionID,
902
+ title: permission.patterns.join(", ") || permission.permission,
903
+ permission: permission.permission,
904
+ patterns: permission.patterns,
905
+ always: permission.always,
906
+ endpoint: "request",
907
+ claimKey: `permission.asked:${permission.id}`
908
+ };
909
+ }
910
+ function permissionMessage(permission, sessionTitle) {
772
911
  const titleLine = sessionTitle ? `\u{1F4CB} ${sessionTitle}` : `Session: ${permission.sessionID}`;
773
- const message = `\u2753 Permission requested
912
+ const patterns = permission.patterns.length > 0 ? `
913
+ Patterns: ${permission.patterns.join(", ")}` : "";
914
+ const always = permission.always.length > 0 ? `
915
+ Always options: ${permission.always.join(", ")}` : "";
916
+ return `\u2753 Permission requested
774
917
 
775
918
  ${titleLine}
776
919
 
777
- Type: ${permission.type}
778
- Detail: ${permission.title}`;
920
+ Permission: ${permission.permission}
921
+ Detail: ${permission.title}${patterns}${always}`;
922
+ }
923
+ function permissionKeyboard(shortHash) {
924
+ return [
925
+ [{ text: "\u2705 Allow once", callback_data: buildCallbackData(shortHash, "o") }],
926
+ [{ text: "\u267B\uFE0F Always allow", callback_data: buildCallbackData(shortHash, "a") }],
927
+ [{ text: "\u274C Reject", callback_data: buildCallbackData(shortHash, "r") }]
928
+ ];
929
+ }
930
+ function replyFromSelection(selection) {
931
+ if (selection === "o") return "once";
932
+ if (selection === "a") return "always";
933
+ if (selection === "r") return "reject";
934
+ return void 0;
935
+ }
936
+ function replyLabel(reply) {
937
+ if (reply === "once") return "Allowed once";
938
+ if (reply === "always") return "Always allowed";
939
+ return "Rejected";
940
+ }
941
+ async function handleNormalizedPermission(permission, ctx) {
942
+ const claimed = await claimOnce({ claimsDir: ctx.claimsDir, key: permission.claimKey });
943
+ if (!claimed) return;
944
+ const shortHash = createPermissionShortHash(permission.requestID);
945
+ const sentAt = Date.now();
946
+ const rawSessionTitle = ctx.sessionTitleService.getSessionTitle(permission.sessionID);
947
+ const sessionTitle = rawSessionTitle === null ? void 0 : rawSessionTitle;
779
948
  try {
780
- await ctx.bot.sendMessage(message);
949
+ const message = await ctx.bot.sendMessage(permissionMessage(permission, sessionTitle), {
950
+ reply_markup: { inline_keyboard: permissionKeyboard(shortHash) }
951
+ });
952
+ const pending = {
953
+ requestID: permission.requestID,
954
+ sessionID: permission.sessionID,
955
+ title: permission.title,
956
+ permission: permission.permission,
957
+ patterns: permission.patterns,
958
+ always: permission.always,
959
+ sentAt,
960
+ expiresAt: sentAt + PERMISSION_EXPIRY_MS,
961
+ telegramMessageId: message.message_id,
962
+ endpoint: permission.endpoint
963
+ };
964
+ await ctx.pendingPermissions.savePending(shortHash, pending);
781
965
  } catch (err) {
782
966
  ctx.logger.error("failed to send permission notification", { error: String(err) });
783
967
  }
784
968
  }
969
+ async function expirePending(ctx, shortHash, pending, messageId) {
970
+ await ctx.bot.editMessageRemoveKeyboard(messageId, "\u23F1 Permission request expired");
971
+ await ctx.pendingPermissions.deletePending(shortHash);
972
+ ctx.logger.info("pending permission expired", { requestID: pending.requestID });
973
+ }
974
+ async function handlePermissionUpdated(event, ctx) {
975
+ await handleNormalizedPermission(normalizeUpdated(event.properties), ctx);
976
+ }
977
+ async function handlePermissionAsked(event, ctx) {
978
+ await handleNormalizedPermission(normalizeAsked(event.properties), ctx);
979
+ }
980
+ function createPermissionDispatcher(ctx) {
981
+ return {
982
+ async handleCallbackQuery(data, messageId) {
983
+ const match = CALLBACK_RE.exec(data);
984
+ if (!match) return;
985
+ const shortHash = match[1];
986
+ const reply = replyFromSelection(match[2]);
987
+ if (!reply) return;
988
+ const pending = await ctx.pendingPermissions.loadPending(shortHash);
989
+ if (!pending) {
990
+ await ctx.bot.editMessageRemoveKeyboard(messageId, "This permission request has expired.");
991
+ return;
992
+ }
993
+ if (pending.expiresAt < Date.now()) {
994
+ await expirePending(ctx, shortHash, pending, messageId);
995
+ return;
996
+ }
997
+ try {
998
+ await ctx.replyToPermission(pending.requestID, pending.sessionID, reply, pending.endpoint);
999
+ await ctx.bot.editMessageRemoveKeyboard(messageId, `\u2705 Permission ${replyLabel(reply)}
1000
+
1001
+ ${pending.permission}: ${pending.title}`);
1002
+ ctx.logger.info("permission reply sent", { requestID: pending.requestID, sessionID: pending.sessionID, reply });
1003
+ } catch (err) {
1004
+ await ctx.bot.editMessageRemoveKeyboard(messageId, "\u26A0\uFE0F Failed to send permission reply to opencode");
1005
+ ctx.logger.error("failed to send permission reply", { error: String(err), requestID: pending.requestID });
1006
+ } finally {
1007
+ await ctx.pendingPermissions.deletePending(shortHash);
1008
+ }
1009
+ }
1010
+ };
1011
+ }
785
1012
 
786
1013
  // src/events/question-asked.ts
787
1014
  var QUESTION_EXPIRY_MS = 5 * 6e4;
788
- var CALLBACK_RE = /^q:([^:]+):(\d+):(\d+|c|d)$/;
1015
+ var CALLBACK_RE2 = /^q:([^:]+):(\d+):(\d+|c|d)$/;
789
1016
  function isQuestionOption(value) {
790
1017
  return typeof value.label === "string" && typeof value.description === "string";
791
1018
  }
@@ -804,14 +1031,14 @@ function isEventQuestionAsked(event) {
804
1031
  if (!Array.isArray(props.questions)) return false;
805
1032
  return props.questions.every((question) => typeof question === "object" && question !== null && isQuestionInfo(question));
806
1033
  }
807
- function buildCallbackData(shortHash, questionIndex, optionIndex) {
1034
+ function buildCallbackData2(shortHash, questionIndex, optionIndex) {
808
1035
  const data = `q:${shortHash}:${questionIndex}:${optionIndex}`;
809
1036
  if (Buffer.byteLength(data, "utf8") > 64) throw new Error("Telegram callback_data exceeds 64 bytes");
810
1037
  return data;
811
1038
  }
812
1039
  function callbackDataForQuestion(shortHash, questionIndex, question) {
813
- const data = question.options.map((_, optionIndex) => buildCallbackData(shortHash, questionIndex, optionIndex));
814
- if (question.custom !== false) data.push(buildCallbackData(shortHash, questionIndex, "c"));
1040
+ const data = question.options.map((_, optionIndex) => buildCallbackData2(shortHash, questionIndex, optionIndex));
1041
+ if (question.custom !== false) data.push(buildCallbackData2(shortHash, questionIndex, "c"));
815
1042
  return data;
816
1043
  }
817
1044
  function useSimpleQuestionKeyboard(question) {
@@ -824,13 +1051,13 @@ function questionInlineKeyboard(shortHash, questionIndex, question, selected) {
824
1051
  const multiple = question.multiple === true;
825
1052
  const inlineKeyboard = question.options.map((option, optionIndex) => [{
826
1053
  text: multiple && selected.includes(option.label) ? `\u2705 ${option.label}` : option.label,
827
- callback_data: buildCallbackData(shortHash, questionIndex, optionIndex)
1054
+ callback_data: buildCallbackData2(shortHash, questionIndex, optionIndex)
828
1055
  }]);
829
1056
  if (question.custom !== false) {
830
- inlineKeyboard.push([{ text: "\u270F\uFE0F Custom answer", callback_data: buildCallbackData(shortHash, questionIndex, "c") }]);
1057
+ inlineKeyboard.push([{ text: "\u270F\uFE0F Custom answer", callback_data: buildCallbackData2(shortHash, questionIndex, "c") }]);
831
1058
  }
832
1059
  if (multiple) {
833
- inlineKeyboard.push([{ text: "\u2705 Done", callback_data: buildCallbackData(shortHash, questionIndex, "d") }]);
1060
+ inlineKeyboard.push([{ text: "\u2705 Done", callback_data: buildCallbackData2(shortHash, questionIndex, "d") }]);
834
1061
  }
835
1062
  return inlineKeyboard;
836
1063
  }
@@ -878,7 +1105,7 @@ ${answerSummary(pending.questions, answers)}`);
878
1105
  await ctx.pendingQuestions.deletePending(shortHash);
879
1106
  }
880
1107
  }
881
- async function expirePending(ctx, shortHash, pending, messageId) {
1108
+ async function expirePending2(ctx, shortHash, pending, messageId) {
882
1109
  await ctx.bot.editMessageRemoveKeyboard(messageId, "\u23F1 Question expired");
883
1110
  await ctx.pendingQuestions.deletePending(shortHash);
884
1111
  ctx.logger.info("pending question expired", { requestID: pending.requestID });
@@ -917,7 +1144,7 @@ async function handleQuestionAsked(event, ctx) {
917
1144
  function createQuestionDispatcher(ctx) {
918
1145
  return {
919
1146
  async handleCallbackQuery(data, messageId, chatId, userId) {
920
- const match = CALLBACK_RE.exec(data);
1147
+ const match = CALLBACK_RE2.exec(data);
921
1148
  if (!match) return;
922
1149
  const shortHash = match[1];
923
1150
  const questionIndex = Number(match[2]);
@@ -928,7 +1155,7 @@ function createQuestionDispatcher(ctx) {
928
1155
  return;
929
1156
  }
930
1157
  if (pending.expiresAt < Date.now()) {
931
- await expirePending(ctx, shortHash, pending, messageId);
1158
+ await expirePending2(ctx, shortHash, pending, messageId);
932
1159
  return;
933
1160
  }
934
1161
  const question = pending.questions[questionIndex];
@@ -971,7 +1198,7 @@ function createQuestionDispatcher(ctx) {
971
1198
  const awaiting = match.data.awaitingCustomFor;
972
1199
  if (!awaiting || awaiting.promptMessageId !== replyToMessageId) return;
973
1200
  if (match.data.expiresAt < Date.now()) {
974
- await expirePending(ctx, match.shortHash, match.data, match.data.telegramMessageIds[0]);
1201
+ await expirePending2(ctx, match.shortHash, match.data, match.data.telegramMessageIds[0]);
975
1202
  return;
976
1203
  }
977
1204
  const question = match.data.questions[awaiting.questionIndex];
@@ -1012,7 +1239,7 @@ async function handleQuestionReplied(event, ctx) {
1012
1239
  }
1013
1240
 
1014
1241
  // src/telegram-remote.ts
1015
- var pluginDir = dirname3(fileURLToPath(import.meta.url));
1242
+ var pluginDir = dirname4(fileURLToPath(import.meta.url));
1016
1243
  var TelegramRemote = async (input) => {
1017
1244
  const logger = createLogger({ namespace: "telegram" });
1018
1245
  try {
@@ -1021,10 +1248,11 @@ var TelegramRemote = async (input) => {
1021
1248
  const config = loadConfig({ logger, env: process.env });
1022
1249
  const stateStore = createStateStore();
1023
1250
  const initialState = await stateStore.read();
1024
- const tokenHash = createHash3("sha256").update(config.botToken).digest("hex").slice(0, 16);
1025
- const lockPath = join5(tmpdir3(), `opencoder-telegram-${tokenHash}.lock`);
1026
- const claimsDir = join5(tmpdir3(), `opencoder-telegram-claims-${tokenHash}`);
1251
+ const tokenHash = createHash4("sha256").update(config.botToken).digest("hex").slice(0, 16);
1252
+ const lockPath = join6(tmpdir4(), `opencoder-telegram-${tokenHash}.lock`);
1253
+ const claimsDir = join6(tmpdir4(), `opencoder-telegram-claims-${tokenHash}`);
1027
1254
  const pendingQuestions = createPendingQuestionStore({ tokenHash });
1255
+ const pendingPermissions = createPendingPermissionStore({ tokenHash });
1028
1256
  const lockResult = await acquireLock({ lockPath });
1029
1257
  const isLeader = lockResult.acquired;
1030
1258
  logger.info(
@@ -1042,6 +1270,23 @@ var TelegramRemote = async (input) => {
1042
1270
  throwOnError: true
1043
1271
  });
1044
1272
  };
1273
+ const replyToPermission = async (requestID, sessionID, reply, endpoint) => {
1274
+ if (endpoint === "request") {
1275
+ await client._client.post({
1276
+ url: `/permission/${encodeURIComponent(requestID)}/reply`,
1277
+ headers: { "Content-Type": "application/json" },
1278
+ body: { reply },
1279
+ throwOnError: true
1280
+ });
1281
+ return;
1282
+ }
1283
+ await client._client.post({
1284
+ url: `/session/${encodeURIComponent(sessionID)}/permissions/${encodeURIComponent(requestID)}`,
1285
+ headers: { "Content-Type": "application/json" },
1286
+ body: { response: reply },
1287
+ throwOnError: true
1288
+ });
1289
+ };
1045
1290
  const bot = createTelegramBot({
1046
1291
  config,
1047
1292
  stateStore,
@@ -1085,10 +1330,13 @@ var TelegramRemote = async (input) => {
1085
1330
  serverUrl: input.serverUrl,
1086
1331
  tokenHash,
1087
1332
  pendingQuestions,
1088
- replyToQuestion
1333
+ pendingPermissions,
1334
+ replyToQuestion,
1335
+ replyToPermission
1089
1336
  };
1090
1337
  if (isLeader) {
1091
1338
  bot.setQuestionDispatcher(createQuestionDispatcher(ctx));
1339
+ bot.setPermissionDispatcher(createPermissionDispatcher(ctx));
1092
1340
  }
1093
1341
  return {
1094
1342
  event: async ({ event }) => {
@@ -1106,6 +1354,10 @@ var TelegramRemote = async (input) => {
1106
1354
  case "permission.updated":
1107
1355
  return handlePermissionUpdated(event, ctx);
1108
1356
  default: {
1357
+ if (isEventPermissionAsked(extEvent)) {
1358
+ if (!isLeader) return;
1359
+ return handlePermissionAsked(extEvent, ctx);
1360
+ }
1109
1361
  if (isEventSessionError(extEvent)) {
1110
1362
  return handleSessionError(extEvent, ctx);
1111
1363
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinseeker/opencode-telegram-plugin",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
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",