opencode-tbot 0.1.24 → 0.1.25
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 +2 -0
- package/dist/plugin.js +127 -21
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ A Telegram plugin for driving [OpenCode](https://opencode.ai) from chat.
|
|
|
12
12
|
|
|
13
13
|
- Text messages are forwarded to the active OpenCode session.
|
|
14
14
|
- Telegram photos and image documents are uploaded as OpenCode file parts.
|
|
15
|
+
- Image turns run in a temporary forked session, so later text-only prompts do not inherit image context.
|
|
15
16
|
- Telegram voice messages are explicitly rejected with a localized reply.
|
|
16
17
|
- Permission requests raised by OpenCode can be approved or rejected from Telegram inline buttons.
|
|
17
18
|
- Session completion and error events can be reported back to the bound Telegram chat.
|
|
@@ -124,6 +125,7 @@ Message handling:
|
|
|
124
125
|
|
|
125
126
|
- Non-command text is treated as a prompt and sent to OpenCode.
|
|
126
127
|
- Telegram photos and image documents are forwarded as OpenCode file parts.
|
|
128
|
+
- Image attachments are processed in a temporary fork of the active session so later text-only prompts stay clean.
|
|
127
129
|
- Telegram voice messages are not supported and receive a localized rejection reply.
|
|
128
130
|
|
|
129
131
|
## Development
|
package/dist/plugin.js
CHANGED
|
@@ -283,6 +283,24 @@ var OpenCodeClient = class {
|
|
|
283
283
|
});
|
|
284
284
|
return unwrapSdkData(await this.client.session.abort({ sessionID: sessionId }, SDK_OPTIONS));
|
|
285
285
|
}
|
|
286
|
+
async deleteSession(sessionId) {
|
|
287
|
+
if (hasRawSdkMethod(this.client, "delete")) return this.requestRaw("delete", {
|
|
288
|
+
url: "/session/{sessionID}",
|
|
289
|
+
path: { sessionID: sessionId }
|
|
290
|
+
});
|
|
291
|
+
return unwrapSdkData(await this.client.session.delete({ sessionID: sessionId }, SDK_OPTIONS));
|
|
292
|
+
}
|
|
293
|
+
async forkSession(sessionId, messageId) {
|
|
294
|
+
if (hasRawSdkMethod(this.client, "post")) return this.requestRaw("post", {
|
|
295
|
+
url: "/session/{sessionID}/fork",
|
|
296
|
+
path: { sessionID: sessionId },
|
|
297
|
+
...messageId?.trim() ? { body: { messageID: messageId.trim() } } : {}
|
|
298
|
+
});
|
|
299
|
+
return unwrapSdkData(await this.client.session.fork({
|
|
300
|
+
sessionID: sessionId,
|
|
301
|
+
...messageId?.trim() ? { messageID: messageId.trim() } : {}
|
|
302
|
+
}, SDK_OPTIONS));
|
|
303
|
+
}
|
|
286
304
|
async getPath() {
|
|
287
305
|
if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/path" });
|
|
288
306
|
return unwrapSdkData(await this.client.path.get(void 0, SDK_OPTIONS));
|
|
@@ -1146,29 +1164,73 @@ function extractErrorMessage(error) {
|
|
|
1146
1164
|
//#endregion
|
|
1147
1165
|
//#region src/services/session-activity/foreground-session-tracker.ts
|
|
1148
1166
|
var ForegroundSessionTracker = class {
|
|
1167
|
+
chatStacks = /* @__PURE__ */ new Map();
|
|
1149
1168
|
counts = /* @__PURE__ */ new Map();
|
|
1150
|
-
|
|
1169
|
+
sessionChats = /* @__PURE__ */ new Map();
|
|
1170
|
+
begin(chatId, sessionId) {
|
|
1151
1171
|
const currentCount = this.counts.get(sessionId) ?? 0;
|
|
1152
1172
|
this.counts.set(sessionId, currentCount + 1);
|
|
1173
|
+
this.incrementChat(chatId, sessionId);
|
|
1153
1174
|
return () => {
|
|
1154
|
-
this.decrement(sessionId);
|
|
1175
|
+
this.decrement(chatId, sessionId);
|
|
1155
1176
|
};
|
|
1156
1177
|
}
|
|
1157
1178
|
clear(sessionId) {
|
|
1158
1179
|
const wasForeground = this.counts.has(sessionId);
|
|
1180
|
+
const chatCounts = this.sessionChats.get(sessionId);
|
|
1159
1181
|
this.counts.delete(sessionId);
|
|
1182
|
+
this.sessionChats.delete(sessionId);
|
|
1183
|
+
for (const chatId of chatCounts?.keys() ?? []) {
|
|
1184
|
+
const stack = this.chatStacks.get(chatId);
|
|
1185
|
+
if (!stack) continue;
|
|
1186
|
+
const nextStack = stack.filter((trackedSessionId) => trackedSessionId !== sessionId);
|
|
1187
|
+
if (nextStack.length === 0) {
|
|
1188
|
+
this.chatStacks.delete(chatId);
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
this.chatStacks.set(chatId, nextStack);
|
|
1192
|
+
}
|
|
1160
1193
|
return wasForeground;
|
|
1161
1194
|
}
|
|
1195
|
+
getActiveSessionId(chatId) {
|
|
1196
|
+
return this.chatStacks.get(chatId)?.at(-1) ?? null;
|
|
1197
|
+
}
|
|
1162
1198
|
isForeground(sessionId) {
|
|
1163
1199
|
return this.counts.has(sessionId);
|
|
1164
1200
|
}
|
|
1165
|
-
|
|
1201
|
+
listChatIds(sessionId) {
|
|
1202
|
+
return [...this.sessionChats.get(sessionId)?.keys() ?? []];
|
|
1203
|
+
}
|
|
1204
|
+
decrement(chatId, sessionId) {
|
|
1166
1205
|
const currentCount = this.counts.get(sessionId);
|
|
1167
|
-
if (!currentCount || currentCount <= 1)
|
|
1168
|
-
|
|
1169
|
-
|
|
1206
|
+
if (!currentCount || currentCount <= 1) this.counts.delete(sessionId);
|
|
1207
|
+
else this.counts.set(sessionId, currentCount - 1);
|
|
1208
|
+
this.decrementChat(chatId, sessionId);
|
|
1209
|
+
}
|
|
1210
|
+
incrementChat(chatId, sessionId) {
|
|
1211
|
+
const stack = this.chatStacks.get(chatId) ?? [];
|
|
1212
|
+
stack.push(sessionId);
|
|
1213
|
+
this.chatStacks.set(chatId, stack);
|
|
1214
|
+
const chatCounts = this.sessionChats.get(sessionId) ?? /* @__PURE__ */ new Map();
|
|
1215
|
+
const currentCount = chatCounts.get(chatId) ?? 0;
|
|
1216
|
+
chatCounts.set(chatId, currentCount + 1);
|
|
1217
|
+
this.sessionChats.set(sessionId, chatCounts);
|
|
1218
|
+
}
|
|
1219
|
+
decrementChat(chatId, sessionId) {
|
|
1220
|
+
const stack = this.chatStacks.get(chatId);
|
|
1221
|
+
if (stack) {
|
|
1222
|
+
const index = stack.lastIndexOf(sessionId);
|
|
1223
|
+
if (index >= 0) stack.splice(index, 1);
|
|
1224
|
+
if (stack.length === 0) this.chatStacks.delete(chatId);
|
|
1225
|
+
else this.chatStacks.set(chatId, stack);
|
|
1170
1226
|
}
|
|
1171
|
-
this.
|
|
1227
|
+
const chatCounts = this.sessionChats.get(sessionId);
|
|
1228
|
+
if (!chatCounts) return;
|
|
1229
|
+
const currentCount = chatCounts.get(chatId) ?? 0;
|
|
1230
|
+
if (currentCount <= 1) chatCounts.delete(chatId);
|
|
1231
|
+
else chatCounts.set(chatId, currentCount - 1);
|
|
1232
|
+
if (chatCounts.size === 0) this.sessionChats.delete(sessionId);
|
|
1233
|
+
else this.sessionChats.set(sessionId, chatCounts);
|
|
1172
1234
|
}
|
|
1173
1235
|
};
|
|
1174
1236
|
var NOOP_FOREGROUND_SESSION_TRACKER = {
|
|
@@ -1178,25 +1240,33 @@ var NOOP_FOREGROUND_SESSION_TRACKER = {
|
|
|
1178
1240
|
clear() {
|
|
1179
1241
|
return false;
|
|
1180
1242
|
},
|
|
1243
|
+
getActiveSessionId() {
|
|
1244
|
+
return null;
|
|
1245
|
+
},
|
|
1181
1246
|
isForeground() {
|
|
1182
1247
|
return false;
|
|
1248
|
+
},
|
|
1249
|
+
listChatIds() {
|
|
1250
|
+
return [];
|
|
1183
1251
|
}
|
|
1184
1252
|
};
|
|
1185
1253
|
//#endregion
|
|
1186
1254
|
//#region src/use-cases/abort-prompt.usecase.ts
|
|
1187
1255
|
var AbortPromptUseCase = class {
|
|
1188
|
-
constructor(sessionRepo, opencodeClient) {
|
|
1256
|
+
constructor(sessionRepo, opencodeClient, foregroundSessionTracker = NOOP_FOREGROUND_SESSION_TRACKER) {
|
|
1189
1257
|
this.sessionRepo = sessionRepo;
|
|
1190
1258
|
this.opencodeClient = opencodeClient;
|
|
1259
|
+
this.foregroundSessionTracker = foregroundSessionTracker;
|
|
1191
1260
|
}
|
|
1192
1261
|
async execute(input) {
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1262
|
+
const activeSessionId = this.foregroundSessionTracker.getActiveSessionId(input.chatId);
|
|
1263
|
+
const binding = activeSessionId ? null : await this.sessionRepo.getByChatId(input.chatId);
|
|
1264
|
+
const sessionId = activeSessionId ?? binding?.sessionId ?? null;
|
|
1265
|
+
if (!sessionId) return {
|
|
1195
1266
|
sessionId: null,
|
|
1196
1267
|
status: "no_session",
|
|
1197
1268
|
sessionStatus: null
|
|
1198
1269
|
};
|
|
1199
|
-
const sessionId = binding.sessionId;
|
|
1200
1270
|
const sessionStatus = (await this.opencodeClient.getSessionStatuses())[sessionId] ?? null;
|
|
1201
1271
|
if (!sessionStatus || sessionStatus.type === "idle") return {
|
|
1202
1272
|
sessionId,
|
|
@@ -1693,6 +1763,7 @@ var SendPromptUseCase = class {
|
|
|
1693
1763
|
}
|
|
1694
1764
|
if (!binding || !binding.sessionId || !binding.projectId) throw new Error("Failed to initialize chat session.");
|
|
1695
1765
|
let activeBinding = binding;
|
|
1766
|
+
const shouldIsolateImageTurn = hasImageFiles(files);
|
|
1696
1767
|
const model = activeBinding.modelProviderId && activeBinding.modelId ? {
|
|
1697
1768
|
providerID: activeBinding.modelProviderId,
|
|
1698
1769
|
modelID: activeBinding.modelId
|
|
@@ -1702,11 +1773,13 @@ var SendPromptUseCase = class {
|
|
|
1702
1773
|
activeBinding = await clearStoredAgentSelection(this.sessionRepo, activeBinding);
|
|
1703
1774
|
this.logger.warn?.({ chatId: input.chatId }, "selected agent is no longer available, falling back to OpenCode default");
|
|
1704
1775
|
}
|
|
1705
|
-
const
|
|
1776
|
+
const temporarySessionId = shouldIsolateImageTurn ? await this.createTemporaryImageSession(input.chatId, activeBinding.sessionId) : null;
|
|
1777
|
+
const executionSessionId = temporarySessionId ?? activeBinding.sessionId;
|
|
1778
|
+
const endForegroundSession = this.foregroundSessionTracker.begin(input.chatId, executionSessionId);
|
|
1706
1779
|
let result;
|
|
1707
1780
|
try {
|
|
1708
1781
|
result = await this.opencodeClient.promptSession({
|
|
1709
|
-
sessionId:
|
|
1782
|
+
sessionId: executionSessionId,
|
|
1710
1783
|
prompt: promptText,
|
|
1711
1784
|
...files.length > 0 ? { files } : {},
|
|
1712
1785
|
...selectedAgent ? { agent: selectedAgent.name } : {},
|
|
@@ -1716,6 +1789,7 @@ var SendPromptUseCase = class {
|
|
|
1716
1789
|
});
|
|
1717
1790
|
} finally {
|
|
1718
1791
|
endForegroundSession();
|
|
1792
|
+
if (temporarySessionId) await this.cleanupTemporaryImageSession(input.chatId, activeBinding.sessionId, temporarySessionId);
|
|
1719
1793
|
}
|
|
1720
1794
|
await this.sessionRepo.touch(input.chatId);
|
|
1721
1795
|
return {
|
|
@@ -1729,6 +1803,32 @@ var SendPromptUseCase = class {
|
|
|
1729
1803
|
this.logger.warn?.({ chatId }, `${reason}, falling back to the current OpenCode project`);
|
|
1730
1804
|
return nextBinding;
|
|
1731
1805
|
}
|
|
1806
|
+
async createTemporaryImageSession(chatId, sessionId) {
|
|
1807
|
+
const temporarySession = await this.opencodeClient.forkSession(sessionId);
|
|
1808
|
+
if (!temporarySession.id || temporarySession.id === sessionId) throw new Error("OpenCode did not return a distinct temporary session for the image turn.");
|
|
1809
|
+
this.logger.info?.({
|
|
1810
|
+
chatId,
|
|
1811
|
+
parentSessionId: sessionId,
|
|
1812
|
+
sessionId: temporarySession.id
|
|
1813
|
+
}, "created temporary image session");
|
|
1814
|
+
return temporarySession.id;
|
|
1815
|
+
}
|
|
1816
|
+
async cleanupTemporaryImageSession(chatId, parentSessionId, sessionId) {
|
|
1817
|
+
try {
|
|
1818
|
+
if (!await this.opencodeClient.deleteSession(sessionId)) this.logger.warn?.({
|
|
1819
|
+
chatId,
|
|
1820
|
+
parentSessionId,
|
|
1821
|
+
sessionId
|
|
1822
|
+
}, "failed to delete temporary image session");
|
|
1823
|
+
} catch (error) {
|
|
1824
|
+
this.logger.warn?.({
|
|
1825
|
+
error,
|
|
1826
|
+
chatId,
|
|
1827
|
+
parentSessionId,
|
|
1828
|
+
sessionId
|
|
1829
|
+
}, "failed to delete temporary image session");
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1732
1832
|
};
|
|
1733
1833
|
function buildPromptText(text, files) {
|
|
1734
1834
|
const trimmedText = text?.trim() ?? "";
|
|
@@ -1745,6 +1845,9 @@ function buildPromptText(text, files) {
|
|
|
1745
1845
|
function isImageFile(file) {
|
|
1746
1846
|
return file.mime.trim().toLowerCase().startsWith("image/");
|
|
1747
1847
|
}
|
|
1848
|
+
function hasImageFiles(files) {
|
|
1849
|
+
return files.some(isImageFile);
|
|
1850
|
+
}
|
|
1748
1851
|
//#endregion
|
|
1749
1852
|
//#region src/use-cases/switch-agent.usecase.ts
|
|
1750
1853
|
var SwitchAgentUseCase = class {
|
|
@@ -1938,7 +2041,7 @@ function createContainer(config, opencodeClient, logger) {
|
|
|
1938
2041
|
apiRoot: config.telegramApiRoot
|
|
1939
2042
|
});
|
|
1940
2043
|
const uploadFileUseCase = new UploadFileUseCase(telegramFileClient);
|
|
1941
|
-
const abortPromptUseCase = new AbortPromptUseCase(sessionRepo, opencodeClient);
|
|
2044
|
+
const abortPromptUseCase = new AbortPromptUseCase(sessionRepo, opencodeClient, foregroundSessionTracker);
|
|
1942
2045
|
const createSessionUseCase = new CreateSessionUseCase(sessionRepo, opencodeClient, logger);
|
|
1943
2046
|
const getHealthUseCase = new GetHealthUseCase(opencodeClient);
|
|
1944
2047
|
const getPathUseCase = new GetPathUseCase(opencodeClient);
|
|
@@ -2072,19 +2175,20 @@ async function handleTelegramBotPluginEvent(runtime, event) {
|
|
|
2072
2175
|
}
|
|
2073
2176
|
async function handlePermissionAsked(runtime, request) {
|
|
2074
2177
|
const bindings = await runtime.container.sessionRepo.listBySessionId(request.sessionID);
|
|
2178
|
+
const chatIds = new Set([...bindings.map((binding) => binding.chatId), ...runtime.container.foregroundSessionTracker.listChatIds(request.sessionID)]);
|
|
2075
2179
|
const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(request.id);
|
|
2076
2180
|
const approvedChatIds = new Set(approvals.map((approval) => approval.chatId));
|
|
2077
|
-
for (const
|
|
2078
|
-
if (approvedChatIds.has(
|
|
2181
|
+
for (const chatId of chatIds) {
|
|
2182
|
+
if (approvedChatIds.has(chatId)) continue;
|
|
2079
2183
|
try {
|
|
2080
|
-
const message = await runtime.bot.api.sendMessage(
|
|
2184
|
+
const message = await runtime.bot.api.sendMessage(chatId, buildPermissionApprovalMessage(request), {
|
|
2081
2185
|
parse_mode: "MarkdownV2",
|
|
2082
2186
|
reply_markup: buildPermissionApprovalKeyboard(request.id)
|
|
2083
2187
|
});
|
|
2084
2188
|
await runtime.container.permissionApprovalRepo.set({
|
|
2085
2189
|
requestId: request.id,
|
|
2086
2190
|
sessionId: request.sessionID,
|
|
2087
|
-
chatId
|
|
2191
|
+
chatId,
|
|
2088
2192
|
messageId: message.message_id,
|
|
2089
2193
|
status: "pending",
|
|
2090
2194
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -2092,7 +2196,7 @@ async function handlePermissionAsked(runtime, request) {
|
|
|
2092
2196
|
} catch (error) {
|
|
2093
2197
|
runtime.container.logger.error({
|
|
2094
2198
|
error,
|
|
2095
|
-
chatId
|
|
2199
|
+
chatId,
|
|
2096
2200
|
requestId: request.id
|
|
2097
2201
|
}, "failed to deliver permission request to Telegram");
|
|
2098
2202
|
}
|
|
@@ -2118,7 +2222,7 @@ async function handleSessionError(runtime, sessionId, error) {
|
|
|
2118
2222
|
runtime.container.logger.error({ error }, "session error received without a session id");
|
|
2119
2223
|
return;
|
|
2120
2224
|
}
|
|
2121
|
-
if (runtime.container.foregroundSessionTracker.
|
|
2225
|
+
if (runtime.container.foregroundSessionTracker.clear(sessionId)) {
|
|
2122
2226
|
runtime.container.logger.warn({
|
|
2123
2227
|
error,
|
|
2124
2228
|
sessionId
|
|
@@ -4727,7 +4831,9 @@ function createHooks(runtime) {
|
|
|
4727
4831
|
await handleTelegramBotPluginEvent(runtime, event);
|
|
4728
4832
|
},
|
|
4729
4833
|
async "permission.ask"(input, output) {
|
|
4730
|
-
|
|
4834
|
+
const bindings = await runtime.container.sessionRepo.listBySessionId(input.sessionID);
|
|
4835
|
+
const foregroundChatIds = runtime.container.foregroundSessionTracker.listChatIds(input.sessionID);
|
|
4836
|
+
if (bindings.length > 0 || foregroundChatIds.length > 0) output.status = "ask";
|
|
4731
4837
|
}
|
|
4732
4838
|
};
|
|
4733
4839
|
}
|