opencode-telegram-mirror 0.4.0 → 0.4.2

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/main.ts +91 -9
package/README.md CHANGED
@@ -44,7 +44,7 @@ npm install -g opencode-telegram-mirror
44
44
  opencode-telegram-mirror .
45
45
  ```
46
46
 
47
- That's it! Your OpenCode sessions will now be mirrored to Telegram.
47
+ That's it! Your OpenCode session will now be mirrored to Telegram.
48
48
 
49
49
  ## How it works
50
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-telegram-mirror",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Standalone bot that mirrors OpenCode sessions to Telegram topics",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
package/src/main.ts CHANGED
@@ -98,6 +98,10 @@ interface BotState {
98
98
  assistantMessageIds: Set<string>;
99
99
  pendingParts: Map<string, Part[]>;
100
100
  sentPartIds: Set<string>;
101
+ typingIndicators: Map<
102
+ string,
103
+ { stop: () => void; timeout: ReturnType<typeof setTimeout> | null; mode: "idle" | "tool" }
104
+ >;
101
105
  }
102
106
 
103
107
  async function main() {
@@ -259,6 +263,7 @@ async function main() {
259
263
  assistantMessageIds: new Set(),
260
264
  pendingParts: new Map(),
261
265
  sentPartIds: new Set(),
266
+ typingIndicators: new Map(),
262
267
  }
263
268
 
264
269
  if (initialThreadTitle && config.threadId) {
@@ -644,7 +649,7 @@ async function handleTelegramMessage(
644
649
  directory: state.directory,
645
650
  })
646
651
  if (abortResult.data) {
647
- await state.telegram.sendMessage("Interrupted.")
652
+ log("info", "Abort request sent", { sessionId: state.sessionId })
648
653
  } else {
649
654
  log("error", "Failed to abort session", {
650
655
  sessionId: state.sessionId,
@@ -690,7 +695,7 @@ async function handleTelegramMessage(
690
695
  directory: state.directory,
691
696
  })
692
697
  if (abortResult.data) {
693
- await state.telegram.sendMessage("Interrupted.")
698
+ log("info", "Abort request sent", { sessionId: state.sessionId })
694
699
  } else {
695
700
  log("error", "Failed to abort session", {
696
701
  sessionId: state.sessionId,
@@ -1014,16 +1019,36 @@ async function handleOpenCodeEvent(state: BotState, ev: OpenCodeEvent) {
1014
1019
  const sessionId =
1015
1020
  ev.properties?.sessionID ??
1016
1021
  ev.properties?.info?.sessionID ??
1017
- ev.properties?.part?.sessionID
1022
+ ev.properties?.part?.sessionID ??
1023
+ ev.properties?.session?.id
1018
1024
  const sessionTitle = ev.properties?.session?.title
1019
1025
 
1020
1026
  // Log errors in full and send to Telegram
1021
1027
  if (ev.type === "session.error") {
1022
1028
  const errorMsg = JSON.stringify(ev.properties, null, 2)
1029
+ const error = ev.properties?.error as
1030
+ | { name?: string; data?: { message?: string } }
1031
+ | undefined
1032
+ const errorName = error?.name
1033
+ const errorText = error?.data?.message
1034
+ const isInterrupted =
1035
+ errorName === "MessageAbortedError" || errorText === "The operation was aborted."
1036
+
1023
1037
  log("error", "OpenCode session error", {
1024
1038
  sessionId,
1025
1039
  error: ev.properties,
1026
1040
  })
1041
+
1042
+ if (isInterrupted) {
1043
+ const sendResult = await state.telegram.sendMessage("Interrupted.")
1044
+ if (sendResult.status === "error") {
1045
+ log("error", "Failed to send interrupt message", {
1046
+ error: sendResult.error.message,
1047
+ })
1048
+ }
1049
+ return
1050
+ }
1051
+
1027
1052
  // Send error to Telegram for visibility
1028
1053
  const sendResult = await state.telegram.sendMessage(
1029
1054
  `OpenCode Error:\n${errorMsg.slice(0, 3500)}`
@@ -1076,6 +1101,12 @@ async function handleOpenCodeEvent(state: BotState, ev: OpenCodeEvent) {
1076
1101
  const key = `${info.sessionID}:${info.id}`
1077
1102
  state.assistantMessageIds.add(key)
1078
1103
  log("debug", "Registered assistant message", { key })
1104
+ const entry = state.typingIndicators.get(key)
1105
+ if (entry && entry.mode === "tool") {
1106
+ if (entry.timeout) clearTimeout(entry.timeout)
1107
+ entry.stop()
1108
+ state.typingIndicators.delete(key)
1109
+ }
1079
1110
  }
1080
1111
  }
1081
1112
 
@@ -1093,6 +1124,39 @@ async function handleOpenCodeEvent(state: BotState, ev: OpenCodeEvent) {
1093
1124
  return
1094
1125
  }
1095
1126
 
1127
+ const stopTypingIndicator = (targetKey: string) => {
1128
+ const entry = state.typingIndicators.get(targetKey)
1129
+ if (!entry) return
1130
+ if (entry.timeout) clearTimeout(entry.timeout)
1131
+ entry.stop()
1132
+ state.typingIndicators.delete(targetKey)
1133
+ }
1134
+
1135
+ const startTypingIndicator = (targetKey: string, mode: "idle" | "tool") => {
1136
+ const existing = state.typingIndicators.get(targetKey)
1137
+ if (existing && existing.mode === mode) return
1138
+ if (existing) {
1139
+ if (existing.timeout) clearTimeout(existing.timeout)
1140
+ existing.stop()
1141
+ }
1142
+
1143
+ const stop = state.telegram.startTyping(mode === "tool" ? 2000 : 4000)
1144
+ state.typingIndicators.set(targetKey, { stop, timeout: null, mode })
1145
+ }
1146
+
1147
+ const bumpTypingIndicator = (targetKey: string, mode: "idle" | "tool") => {
1148
+ const existing = state.typingIndicators.get(targetKey)
1149
+ if (!existing || existing.mode !== mode) {
1150
+ startTypingIndicator(targetKey, mode)
1151
+ return
1152
+ }
1153
+
1154
+ if (existing.timeout) clearTimeout(existing.timeout)
1155
+ existing.timeout = setTimeout(() => {
1156
+ stopTypingIndicator(targetKey)
1157
+ }, 12000)
1158
+ }
1159
+
1096
1160
  log("debug", "Processing message part", {
1097
1161
  key,
1098
1162
  partType: part.type,
@@ -1106,12 +1170,11 @@ async function handleOpenCodeEvent(state: BotState, ev: OpenCodeEvent) {
1106
1170
  state.pendingParts.set(key, existing)
1107
1171
 
1108
1172
  if (part.type !== "step-finish") {
1109
- const typingResult = await state.telegram.sendTypingAction()
1110
- if (typingResult.status === "error") {
1111
- log("debug", "Typing action failed", {
1112
- error: typingResult.error.message,
1113
- })
1114
- }
1173
+ const typingMode =
1174
+ part.type === "tool" && (part.tool === "edit" || part.tool === "write")
1175
+ ? "tool"
1176
+ : "idle"
1177
+ bumpTypingIndicator(key, typingMode)
1115
1178
  }
1116
1179
 
1117
1180
  // Send tools/reasoning immediately (except edit/write tools - wait for completion to get diff data)
@@ -1139,6 +1202,7 @@ async function handleOpenCodeEvent(state: BotState, ev: OpenCodeEvent) {
1139
1202
 
1140
1203
  // On step-finish, send remaining parts
1141
1204
  if (part.type === "step-finish") {
1205
+ stopTypingIndicator(key)
1142
1206
  for (const p of existing) {
1143
1207
  if (p.type === "step-start" || p.type === "step-finish") continue
1144
1208
  if (state.sentPartIds.has(p.id)) continue
@@ -1241,6 +1305,24 @@ async function handleOpenCodeEvent(state: BotState, ev: OpenCodeEvent) {
1241
1305
  }
1242
1306
  }
1243
1307
 
1308
+ if (ev.type === "message.updated") {
1309
+ const info = ev.properties.info
1310
+ if (info?.role === "assistant") {
1311
+ const key = `${info.sessionID}:${info.id}`
1312
+ const entry = state.typingIndicators.get(key)
1313
+ if (entry && entry.mode === "tool") {
1314
+ const stopTypingIndicator = (targetKey: string) => {
1315
+ const existing = state.typingIndicators.get(targetKey)
1316
+ if (!existing) return
1317
+ if (existing.timeout) clearTimeout(existing.timeout)
1318
+ existing.stop()
1319
+ state.typingIndicators.delete(targetKey)
1320
+ }
1321
+ stopTypingIndicator(key)
1322
+ }
1323
+ }
1324
+ }
1325
+
1244
1326
  const threadId = state.threadId ?? 0
1245
1327
 
1246
1328
  if (ev.type === "question.asked") {