@coinseeker/opencode-telegram-plugin 1.0.10 → 1.0.12

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.12"]
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.12`.
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
  }
@@ -497,6 +508,46 @@ async function handlePermissionUpdated(event, ctx) {
497
508
  async function handlePermissionAsked(event, ctx) {
498
509
  await handleNormalizedPermission(normalizeAsked(event.properties), ctx);
499
510
  }
511
+ function isEventPermissionReplied(event) {
512
+ if (event.type !== "permission.replied") return false;
513
+ const props = event.properties;
514
+ if (!props) return false;
515
+ if (typeof props.sessionID !== "string") return false;
516
+ const hasId = typeof props.permissionID === "string" || typeof props.requestID === "string";
517
+ return hasId;
518
+ }
519
+ function externalReplyLabel(value) {
520
+ if (value === "once") return "Allowed once in opencode";
521
+ if (value === "always") return "Always allowed in opencode";
522
+ if (value === "reject") return "Rejected in opencode";
523
+ return "Already answered in opencode";
524
+ }
525
+ async function handlePermissionReplied(event, ctx) {
526
+ const requestID = event.properties.requestID ?? event.properties.permissionID;
527
+ if (!requestID) return;
528
+ const found = await ctx.pendingPermissions.findByRequestID(requestID);
529
+ if (!found) return;
530
+ const label = externalReplyLabel(event.properties.reply ?? event.properties.response);
531
+ try {
532
+ await ctx.bot.editMessageRemoveKeyboard(
533
+ found.data.telegramMessageId,
534
+ `\u2705 ${label}
535
+
536
+ ${found.data.permission}: ${found.data.title}`
537
+ );
538
+ ctx.logger.info("permission externally replied - cleared pending", {
539
+ requestID,
540
+ sessionID: event.properties.sessionID
541
+ });
542
+ } catch (err) {
543
+ ctx.logger.error("failed to edit externally replied permission", {
544
+ error: String(err),
545
+ requestID
546
+ });
547
+ } finally {
548
+ await ctx.pendingPermissions.deletePending(found.shortHash);
549
+ }
550
+ }
500
551
  function createPermissionDispatcher(ctx) {
501
552
  return {
502
553
  async handleCallbackQuery(data, messageId) {
@@ -950,6 +1001,163 @@ async function handleSessionError(event, ctx) {
950
1001
  ctx.logger.info("session abort recorded", { sessionId: event.properties.sessionID ?? "global" });
951
1002
  }
952
1003
 
1004
+ // src/lib/pending-start-work.ts
1005
+ import { createHash as createHash4 } from "crypto";
1006
+ import { mkdir as mkdir4, readdir as readdir4, readFile as readFile3, rename as rename3, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
1007
+ import { tmpdir as tmpdir3 } from "os";
1008
+ import { dirname as dirname3, join as join4 } from "path";
1009
+ function hasCode4(err, code) {
1010
+ return "code" in err && err.code === code;
1011
+ }
1012
+ function pendingFilePath3(dir, shortHash) {
1013
+ return join4(dir, `${shortHash}.json`);
1014
+ }
1015
+ function parsePending3(text) {
1016
+ const parsed = JSON.parse(text);
1017
+ if (typeof parsed.sessionID !== "string")
1018
+ throw new Error("Invalid pending start-work: sessionID");
1019
+ if (parsed.serverUrl !== void 0 && typeof parsed.serverUrl !== "string")
1020
+ throw new Error("Invalid pending start-work: serverUrl");
1021
+ if (parsed.title !== void 0 && typeof parsed.title !== "string")
1022
+ throw new Error("Invalid pending start-work: title");
1023
+ if (typeof parsed.sentAt !== "number") throw new Error("Invalid pending start-work: sentAt");
1024
+ if (typeof parsed.expiresAt !== "number")
1025
+ throw new Error("Invalid pending start-work: expiresAt");
1026
+ if (typeof parsed.telegramMessageId !== "number")
1027
+ throw new Error("Invalid pending start-work: telegramMessageId");
1028
+ return parsed;
1029
+ }
1030
+ async function listPendingFiles3(dir) {
1031
+ try {
1032
+ const entries = await readdir4(dir, { withFileTypes: true });
1033
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name);
1034
+ } catch (err) {
1035
+ if (err instanceof Error && hasCode4(err, "ENOENT")) return [];
1036
+ throw err;
1037
+ }
1038
+ }
1039
+ function shortHashFromFileName3(fileName) {
1040
+ return fileName.slice(0, -".json".length);
1041
+ }
1042
+ function createPendingStartWorkStore(opts) {
1043
+ const dir = opts.baseDir ?? join4(tmpdir3(), `opencoder-telegram-pending-start-work-${opts.tokenHash}`);
1044
+ return {
1045
+ dir,
1046
+ async savePending(shortHash, data) {
1047
+ const filePath = pendingFilePath3(dir, shortHash);
1048
+ await mkdir4(dirname3(filePath), { recursive: true });
1049
+ const tmpPath = `${filePath}.tmp.${process.pid}`;
1050
+ await writeFile3(tmpPath, JSON.stringify(data, null, 2), "utf8");
1051
+ await rename3(tmpPath, filePath);
1052
+ },
1053
+ async loadPending(shortHash) {
1054
+ try {
1055
+ return parsePending3(await readFile3(pendingFilePath3(dir, shortHash), "utf8"));
1056
+ } catch (err) {
1057
+ if (err instanceof Error && hasCode4(err, "ENOENT")) return void 0;
1058
+ throw err;
1059
+ }
1060
+ },
1061
+ async deletePending(shortHash) {
1062
+ try {
1063
+ await unlink4(pendingFilePath3(dir, shortHash));
1064
+ } catch (err) {
1065
+ if (!(err instanceof Error) || !hasCode4(err, "ENOENT")) throw err;
1066
+ }
1067
+ },
1068
+ async sweepExpired() {
1069
+ const expired = [];
1070
+ for (const fileName of await listPendingFiles3(dir)) {
1071
+ const shortHash = shortHashFromFileName3(fileName);
1072
+ const data = await this.loadPending(shortHash);
1073
+ if (data && data.expiresAt < Date.now()) {
1074
+ expired.push(data);
1075
+ await this.deletePending(shortHash);
1076
+ }
1077
+ }
1078
+ return expired;
1079
+ }
1080
+ };
1081
+ }
1082
+ function createStartWorkShortHash(sessionID) {
1083
+ return createHash4("sha256").update(sessionID).digest("base64url").slice(0, 10);
1084
+ }
1085
+
1086
+ // src/events/start-work.ts
1087
+ var CALLBACK_RE3 = /^sw:([^:]+)$/;
1088
+ var START_WORK_COMMAND = "start-work";
1089
+ var START_WORK_EXPIRY_MS = 24 * 60 * 6e4;
1090
+ function startWorkKeyboard(shortHash) {
1091
+ const callbackData = `sw:${shortHash}`;
1092
+ if (Buffer.byteLength(callbackData, "utf8") > 64)
1093
+ throw new Error("Telegram callback_data exceeds 64 bytes");
1094
+ return [[{ text: "\u25B6\uFE0F Run /start-work", callback_data: callbackData }]];
1095
+ }
1096
+ function planCompleteMessage(title) {
1097
+ return title ? `plan \uC791\uC131\uC774 \uB05D\uB0AC\uC5B4\uC694.
1098
+
1099
+ ${title}` : "plan \uC791\uC131\uC774 \uB05D\uB0AC\uC5B4\uC694.";
1100
+ }
1101
+ function createPendingStartWork(sessionID, title, serverUrl, telegramMessageId) {
1102
+ const sentAt = Date.now();
1103
+ return {
1104
+ sessionID,
1105
+ serverUrl,
1106
+ title: title ?? void 0,
1107
+ sentAt,
1108
+ expiresAt: sentAt + START_WORK_EXPIRY_MS,
1109
+ telegramMessageId
1110
+ };
1111
+ }
1112
+ function startWorkShortHash(sessionID) {
1113
+ return createStartWorkShortHash(sessionID);
1114
+ }
1115
+ async function expirePending3(ctx, shortHash, pending, messageId) {
1116
+ await ctx.bot.editMessageRemoveKeyboard(messageId, "\u23F1 /start-work request expired");
1117
+ await ctx.pendingStartWorks.deletePending(shortHash);
1118
+ ctx.logger.info("pending start-work expired", { sessionID: pending.sessionID });
1119
+ }
1120
+ function createStartWorkDispatcher(ctx) {
1121
+ return {
1122
+ async handleCallbackQuery(data, messageId) {
1123
+ const match = CALLBACK_RE3.exec(data);
1124
+ if (!match) return;
1125
+ const shortHash = match[1];
1126
+ const pending = await ctx.pendingStartWorks.loadPending(shortHash);
1127
+ if (!pending) {
1128
+ await ctx.bot.editMessageRemoveKeyboard(messageId, "This /start-work request has expired.");
1129
+ return;
1130
+ }
1131
+ if (pending.expiresAt < Date.now()) {
1132
+ await expirePending3(ctx, shortHash, pending, messageId);
1133
+ return;
1134
+ }
1135
+ try {
1136
+ await ctx.runSessionCommand(pending.sessionID, START_WORK_COMMAND, pending.serverUrl);
1137
+ const label = pending.title ?? pending.sessionID;
1138
+ await ctx.bot.editMessageRemoveKeyboard(
1139
+ messageId,
1140
+ `\u25B6\uFE0F Sent /start-work to opencode.
1141
+
1142
+ Session: ${label}`
1143
+ );
1144
+ ctx.logger.info("start-work command sent", { sessionID: pending.sessionID });
1145
+ } catch (err) {
1146
+ await ctx.bot.editMessageRemoveKeyboard(
1147
+ messageId,
1148
+ "\u26A0\uFE0F Failed to send /start-work to opencode"
1149
+ );
1150
+ ctx.logger.error("failed to send start-work command", {
1151
+ sessionID: pending.sessionID,
1152
+ error: String(err)
1153
+ });
1154
+ } finally {
1155
+ await ctx.pendingStartWorks.deletePending(shortHash);
1156
+ }
1157
+ }
1158
+ };
1159
+ }
1160
+
953
1161
  // src/events/session-idle.ts
954
1162
  var ROOT_IDLE_RECHECK_DELAY_MS = 2500;
955
1163
  function sleep(ms) {
@@ -996,9 +1204,21 @@ async function sendIdleNotification(sessionId, ctx) {
996
1204
  });
997
1205
  if (!claimed) return;
998
1206
  const title = ctx.sessionTitleService.getSessionTitle(sessionId);
999
- const text = title ? `Agent has finished: ${title}` : "Agent has finished.";
1207
+ const isPlanSession = ctx.sessionTitleService.getSessionAgent(sessionId) === "plan";
1208
+ const text = isPlanSession ? planCompleteMessage(title) : title ? `Agent has finished: ${title}` : "Agent has finished.";
1000
1209
  try {
1001
- await ctx.bot.sendMessage(text);
1210
+ if (isPlanSession) {
1211
+ const shortHash = startWorkShortHash(sessionId);
1212
+ const message = await ctx.bot.sendMessage(text, {
1213
+ reply_markup: { inline_keyboard: startWorkKeyboard(shortHash) }
1214
+ });
1215
+ await ctx.pendingStartWorks.savePending(
1216
+ shortHash,
1217
+ createPendingStartWork(sessionId, title, ctx.serverUrl.href, message.message_id)
1218
+ );
1219
+ } else {
1220
+ await ctx.bot.sendMessage(text);
1221
+ }
1002
1222
  ctx.sessionTitleService.clearDeferredIdleNotification(sessionId);
1003
1223
  ctx.logger.info("idle notification sent", { sessionId, title });
1004
1224
  } catch (err) {
@@ -1076,14 +1296,14 @@ async function handleSessionUpdated(event, ctx) {
1076
1296
  // src/lib/env-loader.ts
1077
1297
  import { existsSync } from "fs";
1078
1298
  import { homedir } from "os";
1079
- import { join as join4 } from "path";
1299
+ import { join as join5 } from "path";
1080
1300
  import dotenv from "dotenv";
1081
1301
  function loadPluginEnv(opts) {
1082
1302
  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")
1303
+ join5(opts.pluginDir, "../../.env"),
1304
+ join5(opts.pluginDir, "..", ".env"),
1305
+ join5(opts.pluginDir, ".env"),
1306
+ join5(opts.homeDir ?? homedir(), ".config/opencode/telegram-remote/.env")
1087
1307
  ];
1088
1308
  const loadedFrom = [];
1089
1309
  const values = {};
@@ -1101,10 +1321,10 @@ function loadPluginEnv(opts) {
1101
1321
  }
1102
1322
 
1103
1323
  // src/lib/lock.ts
1104
- import { open as open2, readFile as readFile3, stat as stat2, unlink as unlink4 } from "fs/promises";
1324
+ import { open as open2, readFile as readFile4, stat as stat2, unlink as unlink5 } from "fs/promises";
1105
1325
  import { hostname } from "os";
1106
1326
  var DEFAULT_TTL_MS2 = 5 * 60 * 1e3;
1107
- function hasCode4(err, code) {
1327
+ function hasCode5(err, code) {
1108
1328
  return "code" in err && err.code === code;
1109
1329
  }
1110
1330
  function parseLockData(text) {
@@ -1123,7 +1343,7 @@ function isPidAlive(pid) {
1123
1343
  process.kill(pid, 0);
1124
1344
  return true;
1125
1345
  } catch (err) {
1126
- if (err instanceof Error && hasCode4(err, "ESRCH")) return false;
1346
+ if (err instanceof Error && hasCode5(err, "ESRCH")) return false;
1127
1347
  return true;
1128
1348
  }
1129
1349
  }
@@ -1144,7 +1364,7 @@ async function createLock(lockPath, pid) {
1144
1364
  if (released) return;
1145
1365
  released = true;
1146
1366
  try {
1147
- await unlink4(lockPath);
1367
+ await unlink5(lockPath);
1148
1368
  } catch {
1149
1369
  }
1150
1370
  }
@@ -1154,7 +1374,7 @@ async function inspectExisting(lockPath, ttlMs) {
1154
1374
  let ownerPid;
1155
1375
  let dead = false;
1156
1376
  try {
1157
- const text = await readFile3(lockPath, "utf8");
1377
+ const text = await readFile4(lockPath, "utf8");
1158
1378
  const data = parseLockData(text);
1159
1379
  if (data) {
1160
1380
  ownerPid = data.pid;
@@ -1180,7 +1400,7 @@ async function acquireLock(opts) {
1180
1400
  try {
1181
1401
  return { acquired: true, handle: await createLock(opts.lockPath, pid) };
1182
1402
  } catch (err) {
1183
- if (!(err instanceof Error) || !hasCode4(err, "EEXIST")) {
1403
+ if (!(err instanceof Error) || !hasCode5(err, "EEXIST")) {
1184
1404
  return { acquired: false, reason: err instanceof Error ? err.message : String(err) };
1185
1405
  }
1186
1406
  const existing = await inspectExisting(opts.lockPath, ttlMs);
@@ -1188,7 +1408,7 @@ async function acquireLock(opts) {
1188
1408
  return { acquired: false, reason: existing.reason, ownerPid: existing.ownerPid };
1189
1409
  }
1190
1410
  try {
1191
- await unlink4(opts.lockPath);
1411
+ await unlink5(opts.lockPath);
1192
1412
  } catch {
1193
1413
  return { acquired: false, reason: "failed to remove stale lock", ownerPid: existing.ownerPid };
1194
1414
  }
@@ -1199,7 +1419,7 @@ async function acquireLock(opts) {
1199
1419
 
1200
1420
  // src/lib/logger.ts
1201
1421
  import { appendFile } from "fs/promises";
1202
- import { tmpdir as tmpdir3 } from "os";
1422
+ import { tmpdir as tmpdir4 } from "os";
1203
1423
  var DEFAULT_BUFFER_LIMIT = 4096;
1204
1424
  var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
1205
1425
  function safeJson(data) {
@@ -1210,7 +1430,7 @@ function safeJson(data) {
1210
1430
  }
1211
1431
  }
1212
1432
  function createLogger(opts = {}) {
1213
- const filePath = opts.filePath ?? `${tmpdir3()}/opencoder-telegram.log`;
1433
+ const filePath = opts.filePath ?? `${tmpdir4()}/opencoder-telegram.log`;
1214
1434
  const namespace = opts.namespace ?? "default";
1215
1435
  const bufferLimit = opts.bufferLimit ?? DEFAULT_BUFFER_LIMIT;
1216
1436
  const flushIntervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
@@ -1268,10 +1488,10 @@ function createLogger(opts = {}) {
1268
1488
  }
1269
1489
 
1270
1490
  // src/lib/state-store.ts
1271
- import { mkdir as mkdir4, readFile as readFile4, rename as rename3, writeFile as writeFile3 } from "fs/promises";
1491
+ import { mkdir as mkdir5, readFile as readFile5, rename as rename4, writeFile as writeFile4 } from "fs/promises";
1272
1492
  import { homedir as homedir2 } from "os";
1273
- import { dirname as dirname3, join as join5 } from "path";
1274
- function hasCode5(err, code) {
1493
+ import { dirname as dirname4, join as join6 } from "path";
1494
+ function hasCode6(err, code) {
1275
1495
  return "code" in err && err.code === code;
1276
1496
  }
1277
1497
  function parseState(text) {
@@ -1283,28 +1503,28 @@ function parseState(text) {
1283
1503
  return state;
1284
1504
  }
1285
1505
  function createStateStore(opts = {}) {
1286
- const filePath = opts.filePath ?? join5(homedir2(), ".config/opencode/telegram-remote/state.json");
1506
+ const filePath = opts.filePath ?? join6(homedir2(), ".config/opencode/telegram-remote/state.json");
1287
1507
  return {
1288
1508
  async read() {
1289
1509
  try {
1290
- return parseState(await readFile4(filePath, "utf8"));
1510
+ return parseState(await readFile5(filePath, "utf8"));
1291
1511
  } catch (err) {
1292
- if (err instanceof Error && hasCode5(err, "ENOENT")) return {};
1512
+ if (err instanceof Error && hasCode6(err, "ENOENT")) return {};
1293
1513
  throw err;
1294
1514
  }
1295
1515
  },
1296
1516
  async write(patch) {
1297
1517
  const existing = await this.read();
1298
1518
  const next = { ...existing, ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
1299
- await mkdir4(dirname3(filePath), { recursive: true });
1519
+ await mkdir5(dirname4(filePath), { recursive: true });
1300
1520
  const tmpPath = `${filePath}.tmp.${process.pid}`;
1301
- await writeFile3(tmpPath, JSON.stringify(next, null, 2), "utf8");
1521
+ await writeFile4(tmpPath, JSON.stringify(next, null, 2), "utf8");
1302
1522
  try {
1303
- await rename3(tmpPath, filePath);
1523
+ await rename4(tmpPath, filePath);
1304
1524
  } 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);
1525
+ if (!(err instanceof Error) || !hasCode6(err, "ENOENT")) throw err;
1526
+ await writeFile4(tmpPath, JSON.stringify(next, null, 2), "utf8");
1527
+ await rename4(tmpPath, filePath);
1308
1528
  }
1309
1529
  return next;
1310
1530
  }
@@ -1312,6 +1532,10 @@ function createStateStore(opts = {}) {
1312
1532
  }
1313
1533
 
1314
1534
  // src/services/session-title-service.ts
1535
+ function agentFromSession(info) {
1536
+ const candidate = info;
1537
+ return typeof candidate.agent === "string" ? candidate.agent : void 0;
1538
+ }
1315
1539
  var SessionTitleService = class {
1316
1540
  sessions = /* @__PURE__ */ new Map();
1317
1541
  setSessionInfo(info) {
@@ -1319,6 +1543,7 @@ var SessionTitleService = class {
1319
1543
  this.sessions.set(info.id, {
1320
1544
  title: info.title || null,
1321
1545
  parentID: info.parentID ?? null,
1546
+ agent: agentFromSession(info) ?? existing?.agent,
1322
1547
  status: existing?.status,
1323
1548
  idleNotificationPending: existing?.idleNotificationPending ?? false
1324
1549
  });
@@ -1328,6 +1553,17 @@ var SessionTitleService = class {
1328
1553
  this.sessions.set(sessionId, {
1329
1554
  title,
1330
1555
  parentID: existing?.parentID,
1556
+ agent: existing?.agent,
1557
+ status: existing?.status,
1558
+ idleNotificationPending: existing?.idleNotificationPending ?? false
1559
+ });
1560
+ }
1561
+ setSessionAgent(sessionId, agent) {
1562
+ const existing = this.sessions.get(sessionId);
1563
+ this.sessions.set(sessionId, {
1564
+ title: existing?.title ?? null,
1565
+ parentID: existing?.parentID,
1566
+ agent,
1331
1567
  status: existing?.status,
1332
1568
  idleNotificationPending: existing?.idleNotificationPending ?? false
1333
1569
  });
@@ -1337,6 +1573,7 @@ var SessionTitleService = class {
1337
1573
  this.sessions.set(sessionId, {
1338
1574
  title: existing?.title ?? null,
1339
1575
  parentID: existing?.parentID,
1576
+ agent: existing?.agent,
1340
1577
  status,
1341
1578
  idleNotificationPending: status === "idle" ? existing?.idleNotificationPending ?? false : false
1342
1579
  });
@@ -1347,6 +1584,9 @@ var SessionTitleService = class {
1347
1584
  getParentID(sessionId) {
1348
1585
  return this.sessions.get(sessionId)?.parentID;
1349
1586
  }
1587
+ getSessionAgent(sessionId) {
1588
+ return this.sessions.get(sessionId)?.agent;
1589
+ }
1350
1590
  getSessionStatus(sessionId) {
1351
1591
  return this.sessions.get(sessionId)?.status;
1352
1592
  }
@@ -1363,6 +1603,7 @@ var SessionTitleService = class {
1363
1603
  this.sessions.set(sessionId, {
1364
1604
  title: existing?.title ?? null,
1365
1605
  parentID: existing?.parentID,
1606
+ agent: existing?.agent,
1366
1607
  status: existing?.status ?? "idle",
1367
1608
  idleNotificationPending: true
1368
1609
  });
@@ -1381,7 +1622,7 @@ var SessionTitleService = class {
1381
1622
  };
1382
1623
 
1383
1624
  // src/telegram-remote.ts
1384
- var pluginDir = dirname4(fileURLToPath(import.meta.url));
1625
+ var pluginDir = dirname5(fileURLToPath(import.meta.url));
1385
1626
  async function postToServer(serverUrl, path, body) {
1386
1627
  const url = new URL(path, serverUrl);
1387
1628
  const response = await fetch(url, {
@@ -1391,7 +1632,22 @@ async function postToServer(serverUrl, path, body) {
1391
1632
  });
1392
1633
  if (response.ok) return;
1393
1634
  const text = await response.text();
1394
- throw new Error(`OpenCode request failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`);
1635
+ throw new Error(
1636
+ `OpenCode request failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`
1637
+ );
1638
+ }
1639
+ function getSessionAgentFromMessage(event) {
1640
+ const info = event.properties.info;
1641
+ if (info.role !== "user") return void 0;
1642
+ return { sessionID: info.sessionID, agent: info.agent };
1643
+ }
1644
+ function getSessionAgentFromNextStep(event) {
1645
+ if (event.type !== "session.next.step.started") return void 0;
1646
+ const props = event.properties;
1647
+ if (!props) return void 0;
1648
+ if (typeof props.sessionID !== "string") return void 0;
1649
+ if (typeof props.agent !== "string") return void 0;
1650
+ return { sessionID: props.sessionID, agent: props.agent };
1395
1651
  }
1396
1652
  var TelegramRemote = async (input) => {
1397
1653
  const logger = createLogger({ namespace: "telegram" });
@@ -1401,11 +1657,12 @@ var TelegramRemote = async (input) => {
1401
1657
  const config = loadConfig({ logger, env: process.env });
1402
1658
  const stateStore = createStateStore();
1403
1659
  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}`);
1660
+ const tokenHash = createHash5("sha256").update(config.botToken).digest("hex").slice(0, 16);
1661
+ const lockPath = join7(tmpdir5(), `opencoder-telegram-${tokenHash}.lock`);
1662
+ const claimsDir = join7(tmpdir5(), `opencoder-telegram-claims-${tokenHash}`);
1407
1663
  const pendingQuestions = createPendingQuestionStore({ tokenHash });
1408
1664
  const pendingPermissions = createPendingPermissionStore({ tokenHash });
1665
+ const pendingStartWorks = createPendingStartWorkStore({ tokenHash });
1409
1666
  const lockResult = await acquireLock({ lockPath });
1410
1667
  const isLeader = lockResult.acquired;
1411
1668
  logger.info(
@@ -1459,6 +1716,19 @@ var TelegramRemote = async (input) => {
1459
1716
  throwOnError: true
1460
1717
  });
1461
1718
  };
1719
+ const runSessionCommand = async (sessionID, command, serverUrl = input.serverUrl.href) => {
1720
+ const path = `/session/${encodeURIComponent(sessionID)}/command`;
1721
+ const body = { command, arguments: "" };
1722
+ if (serverUrl !== input.serverUrl.href) {
1723
+ await postToServer(serverUrl, path, body);
1724
+ return;
1725
+ }
1726
+ await input.client.session.command({
1727
+ path: { id: sessionID },
1728
+ body,
1729
+ throwOnError: true
1730
+ });
1731
+ };
1462
1732
  const bot = createTelegramBot({
1463
1733
  config,
1464
1734
  stateStore,
@@ -1503,12 +1773,15 @@ var TelegramRemote = async (input) => {
1503
1773
  tokenHash,
1504
1774
  pendingQuestions,
1505
1775
  pendingPermissions,
1776
+ pendingStartWorks,
1506
1777
  replyToQuestion,
1507
- replyToPermission
1778
+ replyToPermission,
1779
+ runSessionCommand
1508
1780
  };
1509
1781
  if (isLeader) {
1510
1782
  bot.setQuestionDispatcher(createQuestionDispatcher(ctx));
1511
1783
  bot.setPermissionDispatcher(createPermissionDispatcher(ctx));
1784
+ bot.setStartWorkDispatcher(createStartWorkDispatcher(ctx));
1512
1785
  }
1513
1786
  return {
1514
1787
  event: async ({ event }) => {
@@ -1523,9 +1796,20 @@ var TelegramRemote = async (input) => {
1523
1796
  return handleSessionCreated(event, ctx);
1524
1797
  case "session.updated":
1525
1798
  return handleSessionUpdated(event, ctx);
1799
+ case "message.updated": {
1800
+ const messageAgent = getSessionAgentFromMessage(event);
1801
+ if (messageAgent)
1802
+ ctx.sessionTitleService.setSessionAgent(messageAgent.sessionID, messageAgent.agent);
1803
+ return;
1804
+ }
1526
1805
  case "permission.updated":
1527
1806
  return handlePermissionUpdated(event, ctx);
1528
1807
  default: {
1808
+ const stepAgent = getSessionAgentFromNextStep(extEvent);
1809
+ if (stepAgent) {
1810
+ ctx.sessionTitleService.setSessionAgent(stepAgent.sessionID, stepAgent.agent);
1811
+ return;
1812
+ }
1529
1813
  if (isEventPermissionAsked(extEvent)) {
1530
1814
  if (!isLeader) return;
1531
1815
  return handlePermissionAsked(extEvent, ctx);
@@ -1540,6 +1824,9 @@ var TelegramRemote = async (input) => {
1540
1824
  if (isEventQuestionReplied(extEvent)) {
1541
1825
  return handleQuestionReplied(extEvent, ctx);
1542
1826
  }
1827
+ if (isEventPermissionReplied(extEvent)) {
1828
+ return handlePermissionReplied(extEvent, ctx);
1829
+ }
1543
1830
  return;
1544
1831
  }
1545
1832
  }
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.12",
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",