@coinseeker/opencode-telegram-plugin 1.0.9 → 1.0.11

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.9"]
18
+ "plugin": ["@coinseeker/opencode-telegram-plugin@1.0.11"]
19
19
  }
20
20
  ```
21
21
 
22
- Current stable version: `@coinseeker/opencode-telegram-plugin@1.0.9`.
22
+ Current stable version: `@coinseeker/opencode-telegram-plugin@1.0.11`.
23
23
 
24
24
  Restart OpenCode after editing the config. OpenCode resolves npm package plugins on startup.
25
25
 
@@ -4,9 +4,9 @@
4
4
  */
5
5
 
6
6
  // src/telegram-remote.ts
7
- import { createHash as createHash4 } from "crypto";
8
- import { tmpdir as tmpdir4 } from "os";
9
- import { dirname as dirname4, join as join6 } from "path";
7
+ import { createHash as createHash5 } from "crypto";
8
+ import { tmpdir as tmpdir5 } from "os";
9
+ import { dirname as dirname5, join as join7 } from "path";
10
10
  import { fileURLToPath } from "url";
11
11
 
12
12
  // src/bot.ts
@@ -45,6 +45,7 @@ function createTelegramBot(opts) {
45
45
  let activeChatId = opts.initialChatId;
46
46
  let questionDispatcher;
47
47
  let permissionDispatcher;
48
+ let startWorkDispatcher;
48
49
  if (polling) {
49
50
  bot.use(async (ctx, next) => {
50
51
  const userId = ctx.from?.id;
@@ -97,6 +98,13 @@ This chat is now active for OpenCode notifications.`
97
98
  if (!permissionDispatcher || messageId === void 0) return;
98
99
  await permissionDispatcher.handleCallbackQuery(data, messageId);
99
100
  });
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
+ });
100
108
  bot.on("message:text", async (ctx) => {
101
109
  const replyToMessageId = ctx.message.reply_to_message?.message_id;
102
110
  const chatId = ctx.chat.id;
@@ -190,6 +198,9 @@ This chat is now active for OpenCode notifications.`
190
198
  },
191
199
  setPermissionDispatcher(dispatcher) {
192
200
  permissionDispatcher = dispatcher;
201
+ },
202
+ setStartWorkDispatcher(dispatcher) {
203
+ startWorkDispatcher = dispatcher;
193
204
  }
194
205
  };
195
206
  }
@@ -471,6 +482,7 @@ async function handleNormalizedPermission(permission, ctx) {
471
482
  const pending = {
472
483
  requestID: permission.requestID,
473
484
  sessionID: permission.sessionID,
485
+ serverUrl: ctx.serverUrl.href,
474
486
  title: permission.title,
475
487
  permission: permission.permission,
476
488
  patterns: permission.patterns,
@@ -514,7 +526,13 @@ function createPermissionDispatcher(ctx) {
514
526
  return;
515
527
  }
516
528
  try {
517
- await ctx.replyToPermission(pending.requestID, pending.sessionID, reply, pending.endpoint);
529
+ await ctx.replyToPermission(
530
+ pending.requestID,
531
+ pending.sessionID,
532
+ reply,
533
+ pending.endpoint,
534
+ pending.serverUrl
535
+ );
518
536
  await ctx.bot.editMessageRemoveKeyboard(messageId, `\u2705 Permission ${replyLabel(reply)}
519
537
 
520
538
  ${pending.permission}: ${pending.title}`);
@@ -719,7 +737,7 @@ async function completeIfReady(ctx, pending, shortHash) {
719
737
  const answers = pending.answersInProgress.map((answer) => answer ?? []);
720
738
  const messageId = pending.telegramMessageIds[0];
721
739
  try {
722
- await ctx.replyToQuestion(pending.requestID, answers);
740
+ await ctx.replyToQuestion(pending.requestID, answers, pending.serverUrl);
723
741
  await ctx.bot.editMessageRemoveKeyboard(
724
742
  messageId,
725
743
  `\u2705 Answered:
@@ -759,6 +777,7 @@ async function handleQuestionAsked(event, ctx) {
759
777
  const pending = {
760
778
  requestID: request.id,
761
779
  sessionID: request.sessionID,
780
+ serverUrl: ctx.serverUrl.href,
762
781
  questions: request.questions,
763
782
  sentAt,
764
783
  expiresAt: sentAt + QUESTION_EXPIRY_MS,
@@ -942,6 +961,163 @@ async function handleSessionError(event, ctx) {
942
961
  ctx.logger.info("session abort recorded", { sessionId: event.properties.sessionID ?? "global" });
943
962
  }
944
963
 
964
+ // src/lib/pending-start-work.ts
965
+ import { createHash as createHash4 } from "crypto";
966
+ import { mkdir as mkdir4, readdir as readdir4, readFile as readFile3, rename as rename3, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
967
+ import { tmpdir as tmpdir3 } from "os";
968
+ import { dirname as dirname3, join as join4 } from "path";
969
+ function hasCode4(err, code) {
970
+ return "code" in err && err.code === code;
971
+ }
972
+ function pendingFilePath3(dir, shortHash) {
973
+ return join4(dir, `${shortHash}.json`);
974
+ }
975
+ function parsePending3(text) {
976
+ const parsed = JSON.parse(text);
977
+ if (typeof parsed.sessionID !== "string")
978
+ throw new Error("Invalid pending start-work: sessionID");
979
+ if (parsed.serverUrl !== void 0 && typeof parsed.serverUrl !== "string")
980
+ throw new Error("Invalid pending start-work: serverUrl");
981
+ if (parsed.title !== void 0 && typeof parsed.title !== "string")
982
+ throw new Error("Invalid pending start-work: title");
983
+ if (typeof parsed.sentAt !== "number") throw new Error("Invalid pending start-work: sentAt");
984
+ if (typeof parsed.expiresAt !== "number")
985
+ throw new Error("Invalid pending start-work: expiresAt");
986
+ if (typeof parsed.telegramMessageId !== "number")
987
+ throw new Error("Invalid pending start-work: telegramMessageId");
988
+ return parsed;
989
+ }
990
+ async function listPendingFiles3(dir) {
991
+ try {
992
+ const entries = await readdir4(dir, { withFileTypes: true });
993
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name);
994
+ } catch (err) {
995
+ if (err instanceof Error && hasCode4(err, "ENOENT")) return [];
996
+ throw err;
997
+ }
998
+ }
999
+ function shortHashFromFileName3(fileName) {
1000
+ return fileName.slice(0, -".json".length);
1001
+ }
1002
+ function createPendingStartWorkStore(opts) {
1003
+ const dir = opts.baseDir ?? join4(tmpdir3(), `opencoder-telegram-pending-start-work-${opts.tokenHash}`);
1004
+ return {
1005
+ dir,
1006
+ async savePending(shortHash, data) {
1007
+ const filePath = pendingFilePath3(dir, shortHash);
1008
+ await mkdir4(dirname3(filePath), { recursive: true });
1009
+ const tmpPath = `${filePath}.tmp.${process.pid}`;
1010
+ await writeFile3(tmpPath, JSON.stringify(data, null, 2), "utf8");
1011
+ await rename3(tmpPath, filePath);
1012
+ },
1013
+ async loadPending(shortHash) {
1014
+ try {
1015
+ return parsePending3(await readFile3(pendingFilePath3(dir, shortHash), "utf8"));
1016
+ } catch (err) {
1017
+ if (err instanceof Error && hasCode4(err, "ENOENT")) return void 0;
1018
+ throw err;
1019
+ }
1020
+ },
1021
+ async deletePending(shortHash) {
1022
+ try {
1023
+ await unlink4(pendingFilePath3(dir, shortHash));
1024
+ } catch (err) {
1025
+ if (!(err instanceof Error) || !hasCode4(err, "ENOENT")) throw err;
1026
+ }
1027
+ },
1028
+ async sweepExpired() {
1029
+ const expired = [];
1030
+ for (const fileName of await listPendingFiles3(dir)) {
1031
+ const shortHash = shortHashFromFileName3(fileName);
1032
+ const data = await this.loadPending(shortHash);
1033
+ if (data && data.expiresAt < Date.now()) {
1034
+ expired.push(data);
1035
+ await this.deletePending(shortHash);
1036
+ }
1037
+ }
1038
+ return expired;
1039
+ }
1040
+ };
1041
+ }
1042
+ function createStartWorkShortHash(sessionID) {
1043
+ return createHash4("sha256").update(sessionID).digest("base64url").slice(0, 10);
1044
+ }
1045
+
1046
+ // src/events/start-work.ts
1047
+ var CALLBACK_RE3 = /^sw:([^:]+)$/;
1048
+ var START_WORK_COMMAND = "start-work";
1049
+ var START_WORK_EXPIRY_MS = 24 * 60 * 6e4;
1050
+ function startWorkKeyboard(shortHash) {
1051
+ const callbackData = `sw:${shortHash}`;
1052
+ if (Buffer.byteLength(callbackData, "utf8") > 64)
1053
+ throw new Error("Telegram callback_data exceeds 64 bytes");
1054
+ return [[{ text: "\u25B6\uFE0F Run /start-work", callback_data: callbackData }]];
1055
+ }
1056
+ function planCompleteMessage(title) {
1057
+ return title ? `plan \uC791\uC131\uC774 \uB05D\uB0AC\uC5B4\uC694.
1058
+
1059
+ ${title}` : "plan \uC791\uC131\uC774 \uB05D\uB0AC\uC5B4\uC694.";
1060
+ }
1061
+ function createPendingStartWork(sessionID, title, serverUrl, telegramMessageId) {
1062
+ const sentAt = Date.now();
1063
+ return {
1064
+ sessionID,
1065
+ serverUrl,
1066
+ title: title ?? void 0,
1067
+ sentAt,
1068
+ expiresAt: sentAt + START_WORK_EXPIRY_MS,
1069
+ telegramMessageId
1070
+ };
1071
+ }
1072
+ function startWorkShortHash(sessionID) {
1073
+ return createStartWorkShortHash(sessionID);
1074
+ }
1075
+ async function expirePending3(ctx, shortHash, pending, messageId) {
1076
+ await ctx.bot.editMessageRemoveKeyboard(messageId, "\u23F1 /start-work request expired");
1077
+ await ctx.pendingStartWorks.deletePending(shortHash);
1078
+ ctx.logger.info("pending start-work expired", { sessionID: pending.sessionID });
1079
+ }
1080
+ function createStartWorkDispatcher(ctx) {
1081
+ return {
1082
+ async handleCallbackQuery(data, messageId) {
1083
+ const match = CALLBACK_RE3.exec(data);
1084
+ if (!match) return;
1085
+ const shortHash = match[1];
1086
+ const pending = await ctx.pendingStartWorks.loadPending(shortHash);
1087
+ if (!pending) {
1088
+ await ctx.bot.editMessageRemoveKeyboard(messageId, "This /start-work request has expired.");
1089
+ return;
1090
+ }
1091
+ if (pending.expiresAt < Date.now()) {
1092
+ await expirePending3(ctx, shortHash, pending, messageId);
1093
+ return;
1094
+ }
1095
+ try {
1096
+ await ctx.runSessionCommand(pending.sessionID, START_WORK_COMMAND, pending.serverUrl);
1097
+ const label = pending.title ?? pending.sessionID;
1098
+ await ctx.bot.editMessageRemoveKeyboard(
1099
+ messageId,
1100
+ `\u25B6\uFE0F Sent /start-work to opencode.
1101
+
1102
+ Session: ${label}`
1103
+ );
1104
+ ctx.logger.info("start-work command sent", { sessionID: pending.sessionID });
1105
+ } catch (err) {
1106
+ await ctx.bot.editMessageRemoveKeyboard(
1107
+ messageId,
1108
+ "\u26A0\uFE0F Failed to send /start-work to opencode"
1109
+ );
1110
+ ctx.logger.error("failed to send start-work command", {
1111
+ sessionID: pending.sessionID,
1112
+ error: String(err)
1113
+ });
1114
+ } finally {
1115
+ await ctx.pendingStartWorks.deletePending(shortHash);
1116
+ }
1117
+ }
1118
+ };
1119
+ }
1120
+
945
1121
  // src/events/session-idle.ts
946
1122
  var ROOT_IDLE_RECHECK_DELAY_MS = 2500;
947
1123
  function sleep(ms) {
@@ -988,9 +1164,21 @@ async function sendIdleNotification(sessionId, ctx) {
988
1164
  });
989
1165
  if (!claimed) return;
990
1166
  const title = ctx.sessionTitleService.getSessionTitle(sessionId);
991
- const text = title ? `Agent has finished: ${title}` : "Agent has finished.";
1167
+ const isPlanSession = ctx.sessionTitleService.getSessionAgent(sessionId) === "plan";
1168
+ const text = isPlanSession ? planCompleteMessage(title) : title ? `Agent has finished: ${title}` : "Agent has finished.";
992
1169
  try {
993
- await ctx.bot.sendMessage(text);
1170
+ if (isPlanSession) {
1171
+ const shortHash = startWorkShortHash(sessionId);
1172
+ const message = await ctx.bot.sendMessage(text, {
1173
+ reply_markup: { inline_keyboard: startWorkKeyboard(shortHash) }
1174
+ });
1175
+ await ctx.pendingStartWorks.savePending(
1176
+ shortHash,
1177
+ createPendingStartWork(sessionId, title, ctx.serverUrl.href, message.message_id)
1178
+ );
1179
+ } else {
1180
+ await ctx.bot.sendMessage(text);
1181
+ }
994
1182
  ctx.sessionTitleService.clearDeferredIdleNotification(sessionId);
995
1183
  ctx.logger.info("idle notification sent", { sessionId, title });
996
1184
  } catch (err) {
@@ -1000,15 +1188,19 @@ async function sendIdleNotification(sessionId, ctx) {
1000
1188
  async function flushDeferredParentIfReady(parentID, ctx) {
1001
1189
  if (!ctx.sessionTitleService.hasDeferredIdleNotification(parentID)) return;
1002
1190
  if (ctx.sessionTitleService.hasUnfinishedDescendants(parentID)) return;
1003
- if (ctx.sessionTitleService.getSessionStatus(parentID) !== "idle") {
1191
+ const parentStatus = ctx.sessionTitleService.getSessionStatus(parentID);
1192
+ if (parentStatus === "idle") {
1193
+ ctx.logger.info("keeping deferred parent idle notification - waiting for parent to resume", {
1194
+ sessionId: parentID
1195
+ });
1196
+ return;
1197
+ }
1198
+ if (parentStatus !== void 0) {
1004
1199
  ctx.sessionTitleService.clearDeferredIdleNotification(parentID);
1005
1200
  ctx.logger.info("clearing deferred parent idle notification - parent resumed", {
1006
1201
  sessionId: parentID
1007
1202
  });
1008
- return;
1009
1203
  }
1010
- ctx.logger.info("sending deferred parent idle notification", { sessionId: parentID });
1011
- await sendIdleNotification(parentID, ctx);
1012
1204
  }
1013
1205
  async function deferParentIdleIfDescendantsRunning(sessionId, ctx) {
1014
1206
  await hydrateDescendants(sessionId, ctx);
@@ -1021,8 +1213,8 @@ async function deferParentIdleIfDescendantsRunning(sessionId, ctx) {
1021
1213
  }
1022
1214
  async function handleSessionIdle(event, ctx) {
1023
1215
  const sessionId = event.properties.sessionID;
1024
- ctx.sessionTitleService.setSessionStatus(sessionId, "idle");
1025
1216
  const parentID = await resolveParentID(sessionId, ctx);
1217
+ ctx.sessionTitleService.setSessionStatus(sessionId, "idle");
1026
1218
  if (typeof parentID === "string") {
1027
1219
  ctx.logger.info("suppressing child session idle notification", { sessionId, parentID });
1028
1220
  await flushDeferredParentIfReady(parentID, ctx);
@@ -1064,14 +1256,14 @@ async function handleSessionUpdated(event, ctx) {
1064
1256
  // src/lib/env-loader.ts
1065
1257
  import { existsSync } from "fs";
1066
1258
  import { homedir } from "os";
1067
- import { join as join4 } from "path";
1259
+ import { join as join5 } from "path";
1068
1260
  import dotenv from "dotenv";
1069
1261
  function loadPluginEnv(opts) {
1070
1262
  const paths = [
1071
- join4(opts.pluginDir, "../../.env"),
1072
- join4(opts.pluginDir, "..", ".env"),
1073
- join4(opts.pluginDir, ".env"),
1074
- join4(opts.homeDir ?? homedir(), ".config/opencode/telegram-remote/.env")
1263
+ join5(opts.pluginDir, "../../.env"),
1264
+ join5(opts.pluginDir, "..", ".env"),
1265
+ join5(opts.pluginDir, ".env"),
1266
+ join5(opts.homeDir ?? homedir(), ".config/opencode/telegram-remote/.env")
1075
1267
  ];
1076
1268
  const loadedFrom = [];
1077
1269
  const values = {};
@@ -1089,10 +1281,10 @@ function loadPluginEnv(opts) {
1089
1281
  }
1090
1282
 
1091
1283
  // src/lib/lock.ts
1092
- import { open as open2, readFile as readFile3, stat as stat2, unlink as unlink4 } from "fs/promises";
1284
+ import { open as open2, readFile as readFile4, stat as stat2, unlink as unlink5 } from "fs/promises";
1093
1285
  import { hostname } from "os";
1094
1286
  var DEFAULT_TTL_MS2 = 5 * 60 * 1e3;
1095
- function hasCode4(err, code) {
1287
+ function hasCode5(err, code) {
1096
1288
  return "code" in err && err.code === code;
1097
1289
  }
1098
1290
  function parseLockData(text) {
@@ -1111,7 +1303,7 @@ function isPidAlive(pid) {
1111
1303
  process.kill(pid, 0);
1112
1304
  return true;
1113
1305
  } catch (err) {
1114
- if (err instanceof Error && hasCode4(err, "ESRCH")) return false;
1306
+ if (err instanceof Error && hasCode5(err, "ESRCH")) return false;
1115
1307
  return true;
1116
1308
  }
1117
1309
  }
@@ -1132,7 +1324,7 @@ async function createLock(lockPath, pid) {
1132
1324
  if (released) return;
1133
1325
  released = true;
1134
1326
  try {
1135
- await unlink4(lockPath);
1327
+ await unlink5(lockPath);
1136
1328
  } catch {
1137
1329
  }
1138
1330
  }
@@ -1142,7 +1334,7 @@ async function inspectExisting(lockPath, ttlMs) {
1142
1334
  let ownerPid;
1143
1335
  let dead = false;
1144
1336
  try {
1145
- const text = await readFile3(lockPath, "utf8");
1337
+ const text = await readFile4(lockPath, "utf8");
1146
1338
  const data = parseLockData(text);
1147
1339
  if (data) {
1148
1340
  ownerPid = data.pid;
@@ -1168,7 +1360,7 @@ async function acquireLock(opts) {
1168
1360
  try {
1169
1361
  return { acquired: true, handle: await createLock(opts.lockPath, pid) };
1170
1362
  } catch (err) {
1171
- if (!(err instanceof Error) || !hasCode4(err, "EEXIST")) {
1363
+ if (!(err instanceof Error) || !hasCode5(err, "EEXIST")) {
1172
1364
  return { acquired: false, reason: err instanceof Error ? err.message : String(err) };
1173
1365
  }
1174
1366
  const existing = await inspectExisting(opts.lockPath, ttlMs);
@@ -1176,7 +1368,7 @@ async function acquireLock(opts) {
1176
1368
  return { acquired: false, reason: existing.reason, ownerPid: existing.ownerPid };
1177
1369
  }
1178
1370
  try {
1179
- await unlink4(opts.lockPath);
1371
+ await unlink5(opts.lockPath);
1180
1372
  } catch {
1181
1373
  return { acquired: false, reason: "failed to remove stale lock", ownerPid: existing.ownerPid };
1182
1374
  }
@@ -1187,7 +1379,7 @@ async function acquireLock(opts) {
1187
1379
 
1188
1380
  // src/lib/logger.ts
1189
1381
  import { appendFile } from "fs/promises";
1190
- import { tmpdir as tmpdir3 } from "os";
1382
+ import { tmpdir as tmpdir4 } from "os";
1191
1383
  var DEFAULT_BUFFER_LIMIT = 4096;
1192
1384
  var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
1193
1385
  function safeJson(data) {
@@ -1198,7 +1390,7 @@ function safeJson(data) {
1198
1390
  }
1199
1391
  }
1200
1392
  function createLogger(opts = {}) {
1201
- const filePath = opts.filePath ?? `${tmpdir3()}/opencoder-telegram.log`;
1393
+ const filePath = opts.filePath ?? `${tmpdir4()}/opencoder-telegram.log`;
1202
1394
  const namespace = opts.namespace ?? "default";
1203
1395
  const bufferLimit = opts.bufferLimit ?? DEFAULT_BUFFER_LIMIT;
1204
1396
  const flushIntervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
@@ -1256,10 +1448,10 @@ function createLogger(opts = {}) {
1256
1448
  }
1257
1449
 
1258
1450
  // src/lib/state-store.ts
1259
- import { mkdir as mkdir4, readFile as readFile4, rename as rename3, writeFile as writeFile3 } from "fs/promises";
1451
+ import { mkdir as mkdir5, readFile as readFile5, rename as rename4, writeFile as writeFile4 } from "fs/promises";
1260
1452
  import { homedir as homedir2 } from "os";
1261
- import { dirname as dirname3, join as join5 } from "path";
1262
- function hasCode5(err, code) {
1453
+ import { dirname as dirname4, join as join6 } from "path";
1454
+ function hasCode6(err, code) {
1263
1455
  return "code" in err && err.code === code;
1264
1456
  }
1265
1457
  function parseState(text) {
@@ -1271,28 +1463,28 @@ function parseState(text) {
1271
1463
  return state;
1272
1464
  }
1273
1465
  function createStateStore(opts = {}) {
1274
- const filePath = opts.filePath ?? join5(homedir2(), ".config/opencode/telegram-remote/state.json");
1466
+ const filePath = opts.filePath ?? join6(homedir2(), ".config/opencode/telegram-remote/state.json");
1275
1467
  return {
1276
1468
  async read() {
1277
1469
  try {
1278
- return parseState(await readFile4(filePath, "utf8"));
1470
+ return parseState(await readFile5(filePath, "utf8"));
1279
1471
  } catch (err) {
1280
- if (err instanceof Error && hasCode5(err, "ENOENT")) return {};
1472
+ if (err instanceof Error && hasCode6(err, "ENOENT")) return {};
1281
1473
  throw err;
1282
1474
  }
1283
1475
  },
1284
1476
  async write(patch) {
1285
1477
  const existing = await this.read();
1286
1478
  const next = { ...existing, ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
1287
- await mkdir4(dirname3(filePath), { recursive: true });
1479
+ await mkdir5(dirname4(filePath), { recursive: true });
1288
1480
  const tmpPath = `${filePath}.tmp.${process.pid}`;
1289
- await writeFile3(tmpPath, JSON.stringify(next, null, 2), "utf8");
1481
+ await writeFile4(tmpPath, JSON.stringify(next, null, 2), "utf8");
1290
1482
  try {
1291
- await rename3(tmpPath, filePath);
1483
+ await rename4(tmpPath, filePath);
1292
1484
  } catch (err) {
1293
- if (!(err instanceof Error) || !hasCode5(err, "ENOENT")) throw err;
1294
- await writeFile3(tmpPath, JSON.stringify(next, null, 2), "utf8");
1295
- await rename3(tmpPath, filePath);
1485
+ if (!(err instanceof Error) || !hasCode6(err, "ENOENT")) throw err;
1486
+ await writeFile4(tmpPath, JSON.stringify(next, null, 2), "utf8");
1487
+ await rename4(tmpPath, filePath);
1296
1488
  }
1297
1489
  return next;
1298
1490
  }
@@ -1300,6 +1492,10 @@ function createStateStore(opts = {}) {
1300
1492
  }
1301
1493
 
1302
1494
  // src/services/session-title-service.ts
1495
+ function agentFromSession(info) {
1496
+ const candidate = info;
1497
+ return typeof candidate.agent === "string" ? candidate.agent : void 0;
1498
+ }
1303
1499
  var SessionTitleService = class {
1304
1500
  sessions = /* @__PURE__ */ new Map();
1305
1501
  setSessionInfo(info) {
@@ -1307,6 +1503,7 @@ var SessionTitleService = class {
1307
1503
  this.sessions.set(info.id, {
1308
1504
  title: info.title || null,
1309
1505
  parentID: info.parentID ?? null,
1506
+ agent: agentFromSession(info) ?? existing?.agent,
1310
1507
  status: existing?.status,
1311
1508
  idleNotificationPending: existing?.idleNotificationPending ?? false
1312
1509
  });
@@ -1315,7 +1512,18 @@ var SessionTitleService = class {
1315
1512
  const existing = this.sessions.get(sessionId);
1316
1513
  this.sessions.set(sessionId, {
1317
1514
  title,
1318
- parentID: existing?.parentID ?? null,
1515
+ parentID: existing?.parentID,
1516
+ agent: existing?.agent,
1517
+ status: existing?.status,
1518
+ idleNotificationPending: existing?.idleNotificationPending ?? false
1519
+ });
1520
+ }
1521
+ setSessionAgent(sessionId, agent) {
1522
+ const existing = this.sessions.get(sessionId);
1523
+ this.sessions.set(sessionId, {
1524
+ title: existing?.title ?? null,
1525
+ parentID: existing?.parentID,
1526
+ agent,
1319
1527
  status: existing?.status,
1320
1528
  idleNotificationPending: existing?.idleNotificationPending ?? false
1321
1529
  });
@@ -1324,7 +1532,8 @@ var SessionTitleService = class {
1324
1532
  const existing = this.sessions.get(sessionId);
1325
1533
  this.sessions.set(sessionId, {
1326
1534
  title: existing?.title ?? null,
1327
- parentID: existing?.parentID ?? null,
1535
+ parentID: existing?.parentID,
1536
+ agent: existing?.agent,
1328
1537
  status,
1329
1538
  idleNotificationPending: status === "idle" ? existing?.idleNotificationPending ?? false : false
1330
1539
  });
@@ -1335,6 +1544,9 @@ var SessionTitleService = class {
1335
1544
  getParentID(sessionId) {
1336
1545
  return this.sessions.get(sessionId)?.parentID;
1337
1546
  }
1547
+ getSessionAgent(sessionId) {
1548
+ return this.sessions.get(sessionId)?.agent;
1549
+ }
1338
1550
  getSessionStatus(sessionId) {
1339
1551
  return this.sessions.get(sessionId)?.status;
1340
1552
  }
@@ -1350,7 +1562,8 @@ var SessionTitleService = class {
1350
1562
  const existing = this.sessions.get(sessionId);
1351
1563
  this.sessions.set(sessionId, {
1352
1564
  title: existing?.title ?? null,
1353
- parentID: existing?.parentID ?? null,
1565
+ parentID: existing?.parentID,
1566
+ agent: existing?.agent,
1354
1567
  status: existing?.status ?? "idle",
1355
1568
  idleNotificationPending: true
1356
1569
  });
@@ -1369,7 +1582,33 @@ var SessionTitleService = class {
1369
1582
  };
1370
1583
 
1371
1584
  // src/telegram-remote.ts
1372
- var pluginDir = dirname4(fileURLToPath(import.meta.url));
1585
+ var pluginDir = dirname5(fileURLToPath(import.meta.url));
1586
+ async function postToServer(serverUrl, path, body) {
1587
+ const url = new URL(path, serverUrl);
1588
+ const response = await fetch(url, {
1589
+ method: "POST",
1590
+ headers: { "Content-Type": "application/json" },
1591
+ body: JSON.stringify(body)
1592
+ });
1593
+ if (response.ok) return;
1594
+ const text = await response.text();
1595
+ throw new Error(
1596
+ `OpenCode request failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`
1597
+ );
1598
+ }
1599
+ function getSessionAgentFromMessage(event) {
1600
+ const info = event.properties.info;
1601
+ if (info.role !== "user") return void 0;
1602
+ return { sessionID: info.sessionID, agent: info.agent };
1603
+ }
1604
+ function getSessionAgentFromNextStep(event) {
1605
+ if (event.type !== "session.next.step.started") return void 0;
1606
+ const props = event.properties;
1607
+ if (!props) return void 0;
1608
+ if (typeof props.sessionID !== "string") return void 0;
1609
+ if (typeof props.agent !== "string") return void 0;
1610
+ return { sessionID: props.sessionID, agent: props.agent };
1611
+ }
1373
1612
  var TelegramRemote = async (input) => {
1374
1613
  const logger = createLogger({ namespace: "telegram" });
1375
1614
  try {
@@ -1378,11 +1617,12 @@ var TelegramRemote = async (input) => {
1378
1617
  const config = loadConfig({ logger, env: process.env });
1379
1618
  const stateStore = createStateStore();
1380
1619
  const initialState = await stateStore.read();
1381
- const tokenHash = createHash4("sha256").update(config.botToken).digest("hex").slice(0, 16);
1382
- const lockPath = join6(tmpdir4(), `opencoder-telegram-${tokenHash}.lock`);
1383
- const claimsDir = join6(tmpdir4(), `opencoder-telegram-claims-${tokenHash}`);
1620
+ const tokenHash = createHash5("sha256").update(config.botToken).digest("hex").slice(0, 16);
1621
+ const lockPath = join7(tmpdir5(), `opencoder-telegram-${tokenHash}.lock`);
1622
+ const claimsDir = join7(tmpdir5(), `opencoder-telegram-claims-${tokenHash}`);
1384
1623
  const pendingQuestions = createPendingQuestionStore({ tokenHash });
1385
1624
  const pendingPermissions = createPendingPermissionStore({ tokenHash });
1625
+ const pendingStartWorks = createPendingStartWorkStore({ tokenHash });
1386
1626
  const lockResult = await acquireLock({ lockPath });
1387
1627
  const isLeader = lockResult.acquired;
1388
1628
  logger.info(
@@ -1396,31 +1636,59 @@ var TelegramRemote = async (input) => {
1396
1636
  });
1397
1637
  const sessionTitleService = new SessionTitleService();
1398
1638
  const client = input.client;
1399
- const replyToQuestion = async (requestID, answers) => {
1639
+ const replyToQuestion = async (requestID, answers, serverUrl = input.serverUrl.href) => {
1640
+ const path = `/question/${encodeURIComponent(requestID)}/reply`;
1641
+ if (serverUrl !== input.serverUrl.href) {
1642
+ await postToServer(serverUrl, path, { answers });
1643
+ return;
1644
+ }
1400
1645
  await client._client.post({
1401
- url: `/question/${encodeURIComponent(requestID)}/reply`,
1646
+ url: path,
1402
1647
  headers: { "Content-Type": "application/json" },
1403
1648
  body: { answers },
1404
1649
  throwOnError: true
1405
1650
  });
1406
1651
  };
1407
- const replyToPermission = async (requestID, sessionID, reply, endpoint) => {
1652
+ const replyToPermission = async (requestID, sessionID, reply, endpoint, serverUrl = input.serverUrl.href) => {
1408
1653
  if (endpoint === "request") {
1654
+ const path2 = `/permission/${encodeURIComponent(requestID)}/reply`;
1655
+ if (serverUrl !== input.serverUrl.href) {
1656
+ await postToServer(serverUrl, path2, { reply });
1657
+ return;
1658
+ }
1409
1659
  await client._client.post({
1410
- url: `/permission/${encodeURIComponent(requestID)}/reply`,
1660
+ url: path2,
1411
1661
  headers: { "Content-Type": "application/json" },
1412
1662
  body: { reply },
1413
1663
  throwOnError: true
1414
1664
  });
1415
1665
  return;
1416
1666
  }
1667
+ const path = `/session/${encodeURIComponent(sessionID)}/permissions/${encodeURIComponent(requestID)}`;
1668
+ if (serverUrl !== input.serverUrl.href) {
1669
+ await postToServer(serverUrl, path, { response: reply });
1670
+ return;
1671
+ }
1417
1672
  await client._client.post({
1418
- url: `/session/${encodeURIComponent(sessionID)}/permissions/${encodeURIComponent(requestID)}`,
1673
+ url: path,
1419
1674
  headers: { "Content-Type": "application/json" },
1420
1675
  body: { response: reply },
1421
1676
  throwOnError: true
1422
1677
  });
1423
1678
  };
1679
+ const runSessionCommand = async (sessionID, command, serverUrl = input.serverUrl.href) => {
1680
+ const path = `/session/${encodeURIComponent(sessionID)}/command`;
1681
+ const body = { command, arguments: "" };
1682
+ if (serverUrl !== input.serverUrl.href) {
1683
+ await postToServer(serverUrl, path, body);
1684
+ return;
1685
+ }
1686
+ await input.client.session.command({
1687
+ path: { id: sessionID },
1688
+ body,
1689
+ throwOnError: true
1690
+ });
1691
+ };
1424
1692
  const bot = createTelegramBot({
1425
1693
  config,
1426
1694
  stateStore,
@@ -1465,12 +1733,15 @@ var TelegramRemote = async (input) => {
1465
1733
  tokenHash,
1466
1734
  pendingQuestions,
1467
1735
  pendingPermissions,
1736
+ pendingStartWorks,
1468
1737
  replyToQuestion,
1469
- replyToPermission
1738
+ replyToPermission,
1739
+ runSessionCommand
1470
1740
  };
1471
1741
  if (isLeader) {
1472
1742
  bot.setQuestionDispatcher(createQuestionDispatcher(ctx));
1473
1743
  bot.setPermissionDispatcher(createPermissionDispatcher(ctx));
1744
+ bot.setStartWorkDispatcher(createStartWorkDispatcher(ctx));
1474
1745
  }
1475
1746
  return {
1476
1747
  event: async ({ event }) => {
@@ -1485,9 +1756,20 @@ var TelegramRemote = async (input) => {
1485
1756
  return handleSessionCreated(event, ctx);
1486
1757
  case "session.updated":
1487
1758
  return handleSessionUpdated(event, ctx);
1759
+ case "message.updated": {
1760
+ const messageAgent = getSessionAgentFromMessage(event);
1761
+ if (messageAgent)
1762
+ ctx.sessionTitleService.setSessionAgent(messageAgent.sessionID, messageAgent.agent);
1763
+ return;
1764
+ }
1488
1765
  case "permission.updated":
1489
1766
  return handlePermissionUpdated(event, ctx);
1490
1767
  default: {
1768
+ const stepAgent = getSessionAgentFromNextStep(extEvent);
1769
+ if (stepAgent) {
1770
+ ctx.sessionTitleService.setSessionAgent(stepAgent.sessionID, stepAgent.agent);
1771
+ return;
1772
+ }
1491
1773
  if (isEventPermissionAsked(extEvent)) {
1492
1774
  if (!isLeader) return;
1493
1775
  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.9",
3
+ "version": "1.0.11",
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",