@coinseeker/opencode-telegram-plugin 1.0.10 → 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.10"]
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.10`.
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
  }
@@ -950,6 +961,163 @@ async function handleSessionError(event, ctx) {
950
961
  ctx.logger.info("session abort recorded", { sessionId: event.properties.sessionID ?? "global" });
951
962
  }
952
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
+
953
1121
  // src/events/session-idle.ts
954
1122
  var ROOT_IDLE_RECHECK_DELAY_MS = 2500;
955
1123
  function sleep(ms) {
@@ -996,9 +1164,21 @@ async function sendIdleNotification(sessionId, ctx) {
996
1164
  });
997
1165
  if (!claimed) return;
998
1166
  const title = ctx.sessionTitleService.getSessionTitle(sessionId);
999
- 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.";
1000
1169
  try {
1001
- 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
+ }
1002
1182
  ctx.sessionTitleService.clearDeferredIdleNotification(sessionId);
1003
1183
  ctx.logger.info("idle notification sent", { sessionId, title });
1004
1184
  } catch (err) {
@@ -1076,14 +1256,14 @@ async function handleSessionUpdated(event, ctx) {
1076
1256
  // src/lib/env-loader.ts
1077
1257
  import { existsSync } from "fs";
1078
1258
  import { homedir } from "os";
1079
- import { join as join4 } from "path";
1259
+ import { join as join5 } from "path";
1080
1260
  import dotenv from "dotenv";
1081
1261
  function loadPluginEnv(opts) {
1082
1262
  const paths = [
1083
- join4(opts.pluginDir, "../../.env"),
1084
- join4(opts.pluginDir, "..", ".env"),
1085
- join4(opts.pluginDir, ".env"),
1086
- 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")
1087
1267
  ];
1088
1268
  const loadedFrom = [];
1089
1269
  const values = {};
@@ -1101,10 +1281,10 @@ function loadPluginEnv(opts) {
1101
1281
  }
1102
1282
 
1103
1283
  // src/lib/lock.ts
1104
- 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";
1105
1285
  import { hostname } from "os";
1106
1286
  var DEFAULT_TTL_MS2 = 5 * 60 * 1e3;
1107
- function hasCode4(err, code) {
1287
+ function hasCode5(err, code) {
1108
1288
  return "code" in err && err.code === code;
1109
1289
  }
1110
1290
  function parseLockData(text) {
@@ -1123,7 +1303,7 @@ function isPidAlive(pid) {
1123
1303
  process.kill(pid, 0);
1124
1304
  return true;
1125
1305
  } catch (err) {
1126
- if (err instanceof Error && hasCode4(err, "ESRCH")) return false;
1306
+ if (err instanceof Error && hasCode5(err, "ESRCH")) return false;
1127
1307
  return true;
1128
1308
  }
1129
1309
  }
@@ -1144,7 +1324,7 @@ async function createLock(lockPath, pid) {
1144
1324
  if (released) return;
1145
1325
  released = true;
1146
1326
  try {
1147
- await unlink4(lockPath);
1327
+ await unlink5(lockPath);
1148
1328
  } catch {
1149
1329
  }
1150
1330
  }
@@ -1154,7 +1334,7 @@ async function inspectExisting(lockPath, ttlMs) {
1154
1334
  let ownerPid;
1155
1335
  let dead = false;
1156
1336
  try {
1157
- const text = await readFile3(lockPath, "utf8");
1337
+ const text = await readFile4(lockPath, "utf8");
1158
1338
  const data = parseLockData(text);
1159
1339
  if (data) {
1160
1340
  ownerPid = data.pid;
@@ -1180,7 +1360,7 @@ async function acquireLock(opts) {
1180
1360
  try {
1181
1361
  return { acquired: true, handle: await createLock(opts.lockPath, pid) };
1182
1362
  } catch (err) {
1183
- if (!(err instanceof Error) || !hasCode4(err, "EEXIST")) {
1363
+ if (!(err instanceof Error) || !hasCode5(err, "EEXIST")) {
1184
1364
  return { acquired: false, reason: err instanceof Error ? err.message : String(err) };
1185
1365
  }
1186
1366
  const existing = await inspectExisting(opts.lockPath, ttlMs);
@@ -1188,7 +1368,7 @@ async function acquireLock(opts) {
1188
1368
  return { acquired: false, reason: existing.reason, ownerPid: existing.ownerPid };
1189
1369
  }
1190
1370
  try {
1191
- await unlink4(opts.lockPath);
1371
+ await unlink5(opts.lockPath);
1192
1372
  } catch {
1193
1373
  return { acquired: false, reason: "failed to remove stale lock", ownerPid: existing.ownerPid };
1194
1374
  }
@@ -1199,7 +1379,7 @@ async function acquireLock(opts) {
1199
1379
 
1200
1380
  // src/lib/logger.ts
1201
1381
  import { appendFile } from "fs/promises";
1202
- import { tmpdir as tmpdir3 } from "os";
1382
+ import { tmpdir as tmpdir4 } from "os";
1203
1383
  var DEFAULT_BUFFER_LIMIT = 4096;
1204
1384
  var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
1205
1385
  function safeJson(data) {
@@ -1210,7 +1390,7 @@ function safeJson(data) {
1210
1390
  }
1211
1391
  }
1212
1392
  function createLogger(opts = {}) {
1213
- const filePath = opts.filePath ?? `${tmpdir3()}/opencoder-telegram.log`;
1393
+ const filePath = opts.filePath ?? `${tmpdir4()}/opencoder-telegram.log`;
1214
1394
  const namespace = opts.namespace ?? "default";
1215
1395
  const bufferLimit = opts.bufferLimit ?? DEFAULT_BUFFER_LIMIT;
1216
1396
  const flushIntervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
@@ -1268,10 +1448,10 @@ function createLogger(opts = {}) {
1268
1448
  }
1269
1449
 
1270
1450
  // src/lib/state-store.ts
1271
- 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";
1272
1452
  import { homedir as homedir2 } from "os";
1273
- import { dirname as dirname3, join as join5 } from "path";
1274
- function hasCode5(err, code) {
1453
+ import { dirname as dirname4, join as join6 } from "path";
1454
+ function hasCode6(err, code) {
1275
1455
  return "code" in err && err.code === code;
1276
1456
  }
1277
1457
  function parseState(text) {
@@ -1283,28 +1463,28 @@ function parseState(text) {
1283
1463
  return state;
1284
1464
  }
1285
1465
  function createStateStore(opts = {}) {
1286
- 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");
1287
1467
  return {
1288
1468
  async read() {
1289
1469
  try {
1290
- return parseState(await readFile4(filePath, "utf8"));
1470
+ return parseState(await readFile5(filePath, "utf8"));
1291
1471
  } catch (err) {
1292
- if (err instanceof Error && hasCode5(err, "ENOENT")) return {};
1472
+ if (err instanceof Error && hasCode6(err, "ENOENT")) return {};
1293
1473
  throw err;
1294
1474
  }
1295
1475
  },
1296
1476
  async write(patch) {
1297
1477
  const existing = await this.read();
1298
1478
  const next = { ...existing, ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
1299
- await mkdir4(dirname3(filePath), { recursive: true });
1479
+ await mkdir5(dirname4(filePath), { recursive: true });
1300
1480
  const tmpPath = `${filePath}.tmp.${process.pid}`;
1301
- await writeFile3(tmpPath, JSON.stringify(next, null, 2), "utf8");
1481
+ await writeFile4(tmpPath, JSON.stringify(next, null, 2), "utf8");
1302
1482
  try {
1303
- await rename3(tmpPath, filePath);
1483
+ await rename4(tmpPath, filePath);
1304
1484
  } catch (err) {
1305
- if (!(err instanceof Error) || !hasCode5(err, "ENOENT")) throw err;
1306
- await writeFile3(tmpPath, JSON.stringify(next, null, 2), "utf8");
1307
- 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);
1308
1488
  }
1309
1489
  return next;
1310
1490
  }
@@ -1312,6 +1492,10 @@ function createStateStore(opts = {}) {
1312
1492
  }
1313
1493
 
1314
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
+ }
1315
1499
  var SessionTitleService = class {
1316
1500
  sessions = /* @__PURE__ */ new Map();
1317
1501
  setSessionInfo(info) {
@@ -1319,6 +1503,7 @@ var SessionTitleService = class {
1319
1503
  this.sessions.set(info.id, {
1320
1504
  title: info.title || null,
1321
1505
  parentID: info.parentID ?? null,
1506
+ agent: agentFromSession(info) ?? existing?.agent,
1322
1507
  status: existing?.status,
1323
1508
  idleNotificationPending: existing?.idleNotificationPending ?? false
1324
1509
  });
@@ -1328,6 +1513,17 @@ var SessionTitleService = class {
1328
1513
  this.sessions.set(sessionId, {
1329
1514
  title,
1330
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,
1331
1527
  status: existing?.status,
1332
1528
  idleNotificationPending: existing?.idleNotificationPending ?? false
1333
1529
  });
@@ -1337,6 +1533,7 @@ var SessionTitleService = class {
1337
1533
  this.sessions.set(sessionId, {
1338
1534
  title: existing?.title ?? null,
1339
1535
  parentID: existing?.parentID,
1536
+ agent: existing?.agent,
1340
1537
  status,
1341
1538
  idleNotificationPending: status === "idle" ? existing?.idleNotificationPending ?? false : false
1342
1539
  });
@@ -1347,6 +1544,9 @@ var SessionTitleService = class {
1347
1544
  getParentID(sessionId) {
1348
1545
  return this.sessions.get(sessionId)?.parentID;
1349
1546
  }
1547
+ getSessionAgent(sessionId) {
1548
+ return this.sessions.get(sessionId)?.agent;
1549
+ }
1350
1550
  getSessionStatus(sessionId) {
1351
1551
  return this.sessions.get(sessionId)?.status;
1352
1552
  }
@@ -1363,6 +1563,7 @@ var SessionTitleService = class {
1363
1563
  this.sessions.set(sessionId, {
1364
1564
  title: existing?.title ?? null,
1365
1565
  parentID: existing?.parentID,
1566
+ agent: existing?.agent,
1366
1567
  status: existing?.status ?? "idle",
1367
1568
  idleNotificationPending: true
1368
1569
  });
@@ -1381,7 +1582,7 @@ var SessionTitleService = class {
1381
1582
  };
1382
1583
 
1383
1584
  // src/telegram-remote.ts
1384
- var pluginDir = dirname4(fileURLToPath(import.meta.url));
1585
+ var pluginDir = dirname5(fileURLToPath(import.meta.url));
1385
1586
  async function postToServer(serverUrl, path, body) {
1386
1587
  const url = new URL(path, serverUrl);
1387
1588
  const response = await fetch(url, {
@@ -1391,7 +1592,22 @@ async function postToServer(serverUrl, path, body) {
1391
1592
  });
1392
1593
  if (response.ok) return;
1393
1594
  const text = await response.text();
1394
- throw new Error(`OpenCode request failed: ${response.status} ${response.statusText}${text ? ` - ${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 };
1395
1611
  }
1396
1612
  var TelegramRemote = async (input) => {
1397
1613
  const logger = createLogger({ namespace: "telegram" });
@@ -1401,11 +1617,12 @@ var TelegramRemote = async (input) => {
1401
1617
  const config = loadConfig({ logger, env: process.env });
1402
1618
  const stateStore = createStateStore();
1403
1619
  const initialState = await stateStore.read();
1404
- const tokenHash = createHash4("sha256").update(config.botToken).digest("hex").slice(0, 16);
1405
- const lockPath = join6(tmpdir4(), `opencoder-telegram-${tokenHash}.lock`);
1406
- 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}`);
1407
1623
  const pendingQuestions = createPendingQuestionStore({ tokenHash });
1408
1624
  const pendingPermissions = createPendingPermissionStore({ tokenHash });
1625
+ const pendingStartWorks = createPendingStartWorkStore({ tokenHash });
1409
1626
  const lockResult = await acquireLock({ lockPath });
1410
1627
  const isLeader = lockResult.acquired;
1411
1628
  logger.info(
@@ -1459,6 +1676,19 @@ var TelegramRemote = async (input) => {
1459
1676
  throwOnError: true
1460
1677
  });
1461
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
+ };
1462
1692
  const bot = createTelegramBot({
1463
1693
  config,
1464
1694
  stateStore,
@@ -1503,12 +1733,15 @@ var TelegramRemote = async (input) => {
1503
1733
  tokenHash,
1504
1734
  pendingQuestions,
1505
1735
  pendingPermissions,
1736
+ pendingStartWorks,
1506
1737
  replyToQuestion,
1507
- replyToPermission
1738
+ replyToPermission,
1739
+ runSessionCommand
1508
1740
  };
1509
1741
  if (isLeader) {
1510
1742
  bot.setQuestionDispatcher(createQuestionDispatcher(ctx));
1511
1743
  bot.setPermissionDispatcher(createPermissionDispatcher(ctx));
1744
+ bot.setStartWorkDispatcher(createStartWorkDispatcher(ctx));
1512
1745
  }
1513
1746
  return {
1514
1747
  event: async ({ event }) => {
@@ -1523,9 +1756,20 @@ var TelegramRemote = async (input) => {
1523
1756
  return handleSessionCreated(event, ctx);
1524
1757
  case "session.updated":
1525
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
+ }
1526
1765
  case "permission.updated":
1527
1766
  return handlePermissionUpdated(event, ctx);
1528
1767
  default: {
1768
+ const stepAgent = getSessionAgentFromNextStep(extEvent);
1769
+ if (stepAgent) {
1770
+ ctx.sessionTitleService.setSessionAgent(stepAgent.sessionID, stepAgent.agent);
1771
+ return;
1772
+ }
1529
1773
  if (isEventPermissionAsked(extEvent)) {
1530
1774
  if (!isLeader) return;
1531
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.10",
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",