@coinseeker/opencode-telegram-plugin 1.0.8 → 1.0.10

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.8"]
18
+ "plugin": ["@coinseeker/opencode-telegram-plugin@1.0.10"]
19
19
  }
20
20
  ```
21
21
 
22
- Current stable version: `@coinseeker/opencode-telegram-plugin@1.0.8`.
22
+ Current stable version: `@coinseeker/opencode-telegram-plugin@1.0.10`.
23
23
 
24
24
  Restart OpenCode after editing the config. OpenCode resolves npm package plugins on startup.
25
25
 
@@ -45,7 +45,6 @@ function createTelegramBot(opts) {
45
45
  let activeChatId = opts.initialChatId;
46
46
  let questionDispatcher;
47
47
  let permissionDispatcher;
48
- let startWorkDispatcher;
49
48
  if (polling) {
50
49
  bot.use(async (ctx, next) => {
51
50
  const userId = ctx.from?.id;
@@ -98,13 +97,6 @@ This chat is now active for OpenCode notifications.`
98
97
  if (!permissionDispatcher || messageId === void 0) return;
99
98
  await permissionDispatcher.handleCallbackQuery(data, messageId);
100
99
  });
101
- bot.callbackQuery(/^sw:(.+)$/, async (ctx) => {
102
- await ctx.answerCallbackQuery();
103
- const data = ctx.callbackQuery.data;
104
- const messageId = ctx.callbackQuery.message?.message_id;
105
- if (!startWorkDispatcher || messageId === void 0) return;
106
- await startWorkDispatcher.handleCallbackQuery(data, messageId);
107
- });
108
100
  bot.on("message:text", async (ctx) => {
109
101
  const replyToMessageId = ctx.message.reply_to_message?.message_id;
110
102
  const chatId = ctx.chat.id;
@@ -198,9 +190,6 @@ This chat is now active for OpenCode notifications.`
198
190
  },
199
191
  setPermissionDispatcher(dispatcher) {
200
192
  permissionDispatcher = dispatcher;
201
- },
202
- setStartWorkDispatcher(dispatcher) {
203
- startWorkDispatcher = dispatcher;
204
193
  }
205
194
  };
206
195
  }
@@ -482,6 +471,7 @@ async function handleNormalizedPermission(permission, ctx) {
482
471
  const pending = {
483
472
  requestID: permission.requestID,
484
473
  sessionID: permission.sessionID,
474
+ serverUrl: ctx.serverUrl.href,
485
475
  title: permission.title,
486
476
  permission: permission.permission,
487
477
  patterns: permission.patterns,
@@ -525,7 +515,13 @@ function createPermissionDispatcher(ctx) {
525
515
  return;
526
516
  }
527
517
  try {
528
- await ctx.replyToPermission(pending.requestID, pending.sessionID, reply, pending.endpoint);
518
+ await ctx.replyToPermission(
519
+ pending.requestID,
520
+ pending.sessionID,
521
+ reply,
522
+ pending.endpoint,
523
+ pending.serverUrl
524
+ );
529
525
  await ctx.bot.editMessageRemoveKeyboard(messageId, `\u2705 Permission ${replyLabel(reply)}
530
526
 
531
527
  ${pending.permission}: ${pending.title}`);
@@ -730,7 +726,7 @@ async function completeIfReady(ctx, pending, shortHash) {
730
726
  const answers = pending.answersInProgress.map((answer) => answer ?? []);
731
727
  const messageId = pending.telegramMessageIds[0];
732
728
  try {
733
- await ctx.replyToQuestion(pending.requestID, answers);
729
+ await ctx.replyToQuestion(pending.requestID, answers, pending.serverUrl);
734
730
  await ctx.bot.editMessageRemoveKeyboard(
735
731
  messageId,
736
732
  `\u2705 Answered:
@@ -770,6 +766,7 @@ async function handleQuestionAsked(event, ctx) {
770
766
  const pending = {
771
767
  requestID: request.id,
772
768
  sessionID: request.sessionID,
769
+ serverUrl: ctx.serverUrl.href,
773
770
  questions: request.questions,
774
771
  sentAt,
775
772
  expiresAt: sentAt + QUESTION_EXPIRY_MS,
@@ -953,84 +950,6 @@ async function handleSessionError(event, ctx) {
953
950
  ctx.logger.info("session abort recorded", { sessionId: event.properties.sessionID ?? "global" });
954
951
  }
955
952
 
956
- // src/events/start-work.ts
957
- var CALLBACK_RE3 = /^sw:(.+)$/;
958
- var START_WORK_COMMAND = "start-work";
959
- var START_WORK_RE = /(?:^|[\s`])\/start-work(?:\s+([^\n`]+))?/g;
960
- var StartWorkCommandStore = class {
961
- commands = /* @__PURE__ */ new Map();
962
- updateFromText(sessionID, text) {
963
- const command = extractStartWorkCommand(sessionID, text);
964
- if (!command) return void 0;
965
- this.commands.set(sessionID, command.arguments);
966
- return command;
967
- }
968
- get(sessionID) {
969
- const args = this.commands.get(sessionID);
970
- if (args === void 0) return void 0;
971
- return { sessionID, arguments: args };
972
- }
973
- delete(sessionID) {
974
- this.commands.delete(sessionID);
975
- }
976
- };
977
- function extractStartWorkCommand(sessionID, text) {
978
- let latestArgs;
979
- for (const match of text.matchAll(START_WORK_RE)) {
980
- const args = (match[1] ?? "").trim();
981
- if (args) latestArgs = args;
982
- }
983
- if (latestArgs === void 0) return void 0;
984
- return { sessionID, arguments: latestArgs };
985
- }
986
- function startWorkCallbackData(sessionID) {
987
- const data = `sw:${encodeURIComponent(sessionID)}`;
988
- return Buffer.byteLength(data, "utf8") <= 64 ? data : void 0;
989
- }
990
- function startWorkKeyboard(sessionID) {
991
- const callbackData = startWorkCallbackData(sessionID);
992
- if (!callbackData) return void 0;
993
- return [[{ text: "\u25B6\uFE0F Run /start-work", callback_data: callbackData }]];
994
- }
995
- function createStartWorkDispatcher(ctx) {
996
- return {
997
- async handleCallbackQuery(data, messageId) {
998
- const match = CALLBACK_RE3.exec(data);
999
- if (!match) return;
1000
- const sessionID = decodeURIComponent(match[1]);
1001
- const command = ctx.startWorkCommands.get(sessionID);
1002
- if (!command) {
1003
- await ctx.bot.editMessageRemoveKeyboard(
1004
- messageId,
1005
- `\u26A0\uFE0F No /start-work command was detected for this session.
1006
-
1007
- Session: ${sessionID}`
1008
- );
1009
- return;
1010
- }
1011
- try {
1012
- await ctx.runSessionCommand(sessionID, START_WORK_COMMAND, command.arguments);
1013
- await ctx.bot.editMessageRemoveKeyboard(
1014
- messageId,
1015
- `\u25B6\uFE0F Sent /start-work ${command.arguments} to opencode.
1016
-
1017
- Session: ${sessionID}`
1018
- );
1019
- ctx.startWorkCommands.delete(sessionID);
1020
- ctx.logger.info("start-work command sent", { sessionID });
1021
- } catch (err) {
1022
- await ctx.bot.editMessageRemoveKeyboard(
1023
- messageId,
1024
- `\u26A0\uFE0F Failed to send /start-work to opencode.
1025
-
1026
- Session: ${sessionID}`
1027
- );
1028
- ctx.logger.error("failed to send start-work command", { sessionID, error: String(err) });
1029
- }
1030
- }
1031
- };
1032
- }
1033
-
1034
953
  // src/events/session-idle.ts
1035
954
  var ROOT_IDLE_RECHECK_DELAY_MS = 2500;
1036
955
  function sleep(ms) {
@@ -1077,17 +996,9 @@ async function sendIdleNotification(sessionId, ctx) {
1077
996
  });
1078
997
  if (!claimed) return;
1079
998
  const title = ctx.sessionTitleService.getSessionTitle(sessionId);
1080
- const startWorkCommand = ctx.startWorkCommands.get(sessionId);
1081
- const message = title ? `Agent has finished: ${title}` : "Agent has finished.";
1082
- const keyboard = startWorkCommand ? startWorkKeyboard(sessionId) : void 0;
1083
- const text = startWorkCommand ? `${message}
1084
-
1085
- Plan is ready. Tap below to run /start-work ${startWorkCommand.arguments}.` : message;
999
+ const text = title ? `Agent has finished: ${title}` : "Agent has finished.";
1086
1000
  try {
1087
- await ctx.bot.sendMessage(
1088
- text,
1089
- keyboard ? { reply_markup: { inline_keyboard: keyboard } } : void 0
1090
- );
1001
+ await ctx.bot.sendMessage(text);
1091
1002
  ctx.sessionTitleService.clearDeferredIdleNotification(sessionId);
1092
1003
  ctx.logger.info("idle notification sent", { sessionId, title });
1093
1004
  } catch (err) {
@@ -1097,15 +1008,19 @@ Plan is ready. Tap below to run /start-work ${startWorkCommand.arguments}.` : me
1097
1008
  async function flushDeferredParentIfReady(parentID, ctx) {
1098
1009
  if (!ctx.sessionTitleService.hasDeferredIdleNotification(parentID)) return;
1099
1010
  if (ctx.sessionTitleService.hasUnfinishedDescendants(parentID)) return;
1100
- if (ctx.sessionTitleService.getSessionStatus(parentID) !== "idle") {
1011
+ const parentStatus = ctx.sessionTitleService.getSessionStatus(parentID);
1012
+ if (parentStatus === "idle") {
1013
+ ctx.logger.info("keeping deferred parent idle notification - waiting for parent to resume", {
1014
+ sessionId: parentID
1015
+ });
1016
+ return;
1017
+ }
1018
+ if (parentStatus !== void 0) {
1101
1019
  ctx.sessionTitleService.clearDeferredIdleNotification(parentID);
1102
1020
  ctx.logger.info("clearing deferred parent idle notification - parent resumed", {
1103
1021
  sessionId: parentID
1104
1022
  });
1105
- return;
1106
1023
  }
1107
- ctx.logger.info("sending deferred parent idle notification", { sessionId: parentID });
1108
- await sendIdleNotification(parentID, ctx);
1109
1024
  }
1110
1025
  async function deferParentIdleIfDescendantsRunning(sessionId, ctx) {
1111
1026
  await hydrateDescendants(sessionId, ctx);
@@ -1118,8 +1033,8 @@ async function deferParentIdleIfDescendantsRunning(sessionId, ctx) {
1118
1033
  }
1119
1034
  async function handleSessionIdle(event, ctx) {
1120
1035
  const sessionId = event.properties.sessionID;
1121
- ctx.sessionTitleService.setSessionStatus(sessionId, "idle");
1122
1036
  const parentID = await resolveParentID(sessionId, ctx);
1037
+ ctx.sessionTitleService.setSessionStatus(sessionId, "idle");
1123
1038
  if (typeof parentID === "string") {
1124
1039
  ctx.logger.info("suppressing child session idle notification", { sessionId, parentID });
1125
1040
  await flushDeferredParentIfReady(parentID, ctx);
@@ -1412,7 +1327,7 @@ var SessionTitleService = class {
1412
1327
  const existing = this.sessions.get(sessionId);
1413
1328
  this.sessions.set(sessionId, {
1414
1329
  title,
1415
- parentID: existing?.parentID ?? null,
1330
+ parentID: existing?.parentID,
1416
1331
  status: existing?.status,
1417
1332
  idleNotificationPending: existing?.idleNotificationPending ?? false
1418
1333
  });
@@ -1421,7 +1336,7 @@ var SessionTitleService = class {
1421
1336
  const existing = this.sessions.get(sessionId);
1422
1337
  this.sessions.set(sessionId, {
1423
1338
  title: existing?.title ?? null,
1424
- parentID: existing?.parentID ?? null,
1339
+ parentID: existing?.parentID,
1425
1340
  status,
1426
1341
  idleNotificationPending: status === "idle" ? existing?.idleNotificationPending ?? false : false
1427
1342
  });
@@ -1447,7 +1362,7 @@ var SessionTitleService = class {
1447
1362
  const existing = this.sessions.get(sessionId);
1448
1363
  this.sessions.set(sessionId, {
1449
1364
  title: existing?.title ?? null,
1450
- parentID: existing?.parentID ?? null,
1365
+ parentID: existing?.parentID,
1451
1366
  status: existing?.status ?? "idle",
1452
1367
  idleNotificationPending: true
1453
1368
  });
@@ -1467,15 +1382,16 @@ var SessionTitleService = class {
1467
1382
 
1468
1383
  // src/telegram-remote.ts
1469
1384
  var pluginDir = dirname4(fileURLToPath(import.meta.url));
1470
- function getTextPartFromMessagePartUpdated(event) {
1471
- if (event.type !== "message.part.updated") return void 0;
1472
- const part = event.properties?.part;
1473
- if (!part || typeof part !== "object") return void 0;
1474
- const candidate = part;
1475
- if (candidate.type !== "text" || typeof candidate.sessionID !== "string" || typeof candidate.text !== "string") {
1476
- return void 0;
1477
- }
1478
- return { type: "text", sessionID: candidate.sessionID, text: candidate.text };
1385
+ async function postToServer(serverUrl, path, body) {
1386
+ const url = new URL(path, serverUrl);
1387
+ const response = await fetch(url, {
1388
+ method: "POST",
1389
+ headers: { "Content-Type": "application/json" },
1390
+ body: JSON.stringify(body)
1391
+ });
1392
+ if (response.ok) return;
1393
+ const text = await response.text();
1394
+ throw new Error(`OpenCode request failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`);
1479
1395
  }
1480
1396
  var TelegramRemote = async (input) => {
1481
1397
  const logger = createLogger({ namespace: "telegram" });
@@ -1490,7 +1406,6 @@ var TelegramRemote = async (input) => {
1490
1406
  const claimsDir = join6(tmpdir4(), `opencoder-telegram-claims-${tokenHash}`);
1491
1407
  const pendingQuestions = createPendingQuestionStore({ tokenHash });
1492
1408
  const pendingPermissions = createPendingPermissionStore({ tokenHash });
1493
- const startWorkCommands = new StartWorkCommandStore();
1494
1409
  const lockResult = await acquireLock({ lockPath });
1495
1410
  const isLeader = lockResult.acquired;
1496
1411
  logger.info(
@@ -1504,38 +1419,46 @@ var TelegramRemote = async (input) => {
1504
1419
  });
1505
1420
  const sessionTitleService = new SessionTitleService();
1506
1421
  const client = input.client;
1507
- const replyToQuestion = async (requestID, answers) => {
1422
+ const replyToQuestion = async (requestID, answers, serverUrl = input.serverUrl.href) => {
1423
+ const path = `/question/${encodeURIComponent(requestID)}/reply`;
1424
+ if (serverUrl !== input.serverUrl.href) {
1425
+ await postToServer(serverUrl, path, { answers });
1426
+ return;
1427
+ }
1508
1428
  await client._client.post({
1509
- url: `/question/${encodeURIComponent(requestID)}/reply`,
1429
+ url: path,
1510
1430
  headers: { "Content-Type": "application/json" },
1511
1431
  body: { answers },
1512
1432
  throwOnError: true
1513
1433
  });
1514
1434
  };
1515
- const replyToPermission = async (requestID, sessionID, reply, endpoint) => {
1435
+ const replyToPermission = async (requestID, sessionID, reply, endpoint, serverUrl = input.serverUrl.href) => {
1516
1436
  if (endpoint === "request") {
1437
+ const path2 = `/permission/${encodeURIComponent(requestID)}/reply`;
1438
+ if (serverUrl !== input.serverUrl.href) {
1439
+ await postToServer(serverUrl, path2, { reply });
1440
+ return;
1441
+ }
1517
1442
  await client._client.post({
1518
- url: `/permission/${encodeURIComponent(requestID)}/reply`,
1443
+ url: path2,
1519
1444
  headers: { "Content-Type": "application/json" },
1520
1445
  body: { reply },
1521
1446
  throwOnError: true
1522
1447
  });
1523
1448
  return;
1524
1449
  }
1450
+ const path = `/session/${encodeURIComponent(sessionID)}/permissions/${encodeURIComponent(requestID)}`;
1451
+ if (serverUrl !== input.serverUrl.href) {
1452
+ await postToServer(serverUrl, path, { response: reply });
1453
+ return;
1454
+ }
1525
1455
  await client._client.post({
1526
- url: `/session/${encodeURIComponent(sessionID)}/permissions/${encodeURIComponent(requestID)}`,
1456
+ url: path,
1527
1457
  headers: { "Content-Type": "application/json" },
1528
1458
  body: { response: reply },
1529
1459
  throwOnError: true
1530
1460
  });
1531
1461
  };
1532
- const runSessionCommand = async (sessionID, command, args) => {
1533
- await input.client.session.command({
1534
- path: { id: sessionID },
1535
- body: { command, arguments: args },
1536
- throwOnError: true
1537
- });
1538
- };
1539
1462
  const bot = createTelegramBot({
1540
1463
  config,
1541
1464
  stateStore,
@@ -1580,15 +1503,12 @@ var TelegramRemote = async (input) => {
1580
1503
  tokenHash,
1581
1504
  pendingQuestions,
1582
1505
  pendingPermissions,
1583
- startWorkCommands,
1584
1506
  replyToQuestion,
1585
- replyToPermission,
1586
- runSessionCommand
1507
+ replyToPermission
1587
1508
  };
1588
1509
  if (isLeader) {
1589
1510
  bot.setQuestionDispatcher(createQuestionDispatcher(ctx));
1590
1511
  bot.setPermissionDispatcher(createPermissionDispatcher(ctx));
1591
- bot.setStartWorkDispatcher(createStartWorkDispatcher(ctx));
1592
1512
  }
1593
1513
  return {
1594
1514
  event: async ({ event }) => {
@@ -1606,17 +1526,6 @@ var TelegramRemote = async (input) => {
1606
1526
  case "permission.updated":
1607
1527
  return handlePermissionUpdated(event, ctx);
1608
1528
  default: {
1609
- const textPart = getTextPartFromMessagePartUpdated(extEvent);
1610
- if (textPart) {
1611
- const command = startWorkCommands.updateFromText(textPart.sessionID, textPart.text);
1612
- if (command) {
1613
- logger.info("start-work command detected", {
1614
- sessionID: command.sessionID,
1615
- arguments: command.arguments
1616
- });
1617
- }
1618
- return;
1619
- }
1620
1529
  if (isEventPermissionAsked(extEvent)) {
1621
1530
  if (!isLeader) return;
1622
1531
  return handlePermissionAsked(extEvent, ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinseeker/opencode-telegram-plugin",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
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",