opencode-telegram-group-topics-bot 0.11.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 (101) hide show
  1. package/.env.example +74 -0
  2. package/LICENSE +21 -0
  3. package/README.md +305 -0
  4. package/dist/agent/manager.js +60 -0
  5. package/dist/agent/types.js +26 -0
  6. package/dist/app/start-bot-app.js +47 -0
  7. package/dist/bot/commands/abort.js +116 -0
  8. package/dist/bot/commands/commands.js +389 -0
  9. package/dist/bot/commands/constants.js +20 -0
  10. package/dist/bot/commands/definitions.js +25 -0
  11. package/dist/bot/commands/help.js +27 -0
  12. package/dist/bot/commands/models.js +38 -0
  13. package/dist/bot/commands/new.js +247 -0
  14. package/dist/bot/commands/opencode-start.js +85 -0
  15. package/dist/bot/commands/opencode-stop.js +44 -0
  16. package/dist/bot/commands/projects.js +304 -0
  17. package/dist/bot/commands/rename.js +173 -0
  18. package/dist/bot/commands/sessions.js +491 -0
  19. package/dist/bot/commands/start.js +67 -0
  20. package/dist/bot/commands/status.js +138 -0
  21. package/dist/bot/constants.js +49 -0
  22. package/dist/bot/handlers/agent.js +127 -0
  23. package/dist/bot/handlers/context.js +125 -0
  24. package/dist/bot/handlers/document.js +65 -0
  25. package/dist/bot/handlers/inline-menu.js +124 -0
  26. package/dist/bot/handlers/model.js +152 -0
  27. package/dist/bot/handlers/permission.js +281 -0
  28. package/dist/bot/handlers/prompt.js +263 -0
  29. package/dist/bot/handlers/question.js +285 -0
  30. package/dist/bot/handlers/variant.js +147 -0
  31. package/dist/bot/handlers/voice.js +173 -0
  32. package/dist/bot/index.js +945 -0
  33. package/dist/bot/message-patterns.js +4 -0
  34. package/dist/bot/middleware/auth.js +30 -0
  35. package/dist/bot/middleware/interaction-guard.js +80 -0
  36. package/dist/bot/middleware/unknown-command.js +22 -0
  37. package/dist/bot/scope.js +222 -0
  38. package/dist/bot/telegram-constants.js +3 -0
  39. package/dist/bot/telegram-rate-limiter.js +263 -0
  40. package/dist/bot/utils/commands.js +21 -0
  41. package/dist/bot/utils/file-download.js +91 -0
  42. package/dist/bot/utils/keyboard.js +85 -0
  43. package/dist/bot/utils/send-with-markdown-fallback.js +57 -0
  44. package/dist/bot/utils/session-error-filter.js +34 -0
  45. package/dist/bot/utils/topic-link.js +29 -0
  46. package/dist/cli/args.js +98 -0
  47. package/dist/cli.js +80 -0
  48. package/dist/config.js +103 -0
  49. package/dist/i18n/de.js +330 -0
  50. package/dist/i18n/en.js +330 -0
  51. package/dist/i18n/es.js +330 -0
  52. package/dist/i18n/index.js +102 -0
  53. package/dist/i18n/ru.js +330 -0
  54. package/dist/i18n/zh.js +330 -0
  55. package/dist/index.js +28 -0
  56. package/dist/interaction/cleanup.js +24 -0
  57. package/dist/interaction/constants.js +25 -0
  58. package/dist/interaction/guard.js +100 -0
  59. package/dist/interaction/manager.js +113 -0
  60. package/dist/interaction/types.js +1 -0
  61. package/dist/keyboard/manager.js +115 -0
  62. package/dist/keyboard/types.js +1 -0
  63. package/dist/model/capabilities.js +62 -0
  64. package/dist/model/manager.js +257 -0
  65. package/dist/model/types.js +24 -0
  66. package/dist/opencode/client.js +13 -0
  67. package/dist/opencode/events.js +159 -0
  68. package/dist/opencode/prompt-submit-error.js +101 -0
  69. package/dist/permission/manager.js +92 -0
  70. package/dist/permission/types.js +1 -0
  71. package/dist/pinned/manager.js +405 -0
  72. package/dist/pinned/types.js +1 -0
  73. package/dist/process/manager.js +273 -0
  74. package/dist/process/types.js +1 -0
  75. package/dist/project/manager.js +88 -0
  76. package/dist/question/manager.js +186 -0
  77. package/dist/question/types.js +1 -0
  78. package/dist/rename/manager.js +64 -0
  79. package/dist/runtime/bootstrap.js +350 -0
  80. package/dist/runtime/mode.js +74 -0
  81. package/dist/runtime/paths.js +37 -0
  82. package/dist/runtime/process-error-handlers.js +24 -0
  83. package/dist/session/cache-manager.js +455 -0
  84. package/dist/session/manager.js +87 -0
  85. package/dist/settings/manager.js +283 -0
  86. package/dist/stt/client.js +64 -0
  87. package/dist/summary/aggregator.js +625 -0
  88. package/dist/summary/formatter.js +417 -0
  89. package/dist/summary/tool-message-batcher.js +277 -0
  90. package/dist/topic/colors.js +8 -0
  91. package/dist/topic/constants.js +10 -0
  92. package/dist/topic/manager.js +161 -0
  93. package/dist/topic/title-constants.js +2 -0
  94. package/dist/topic/title-format.js +10 -0
  95. package/dist/topic/title-sync.js +17 -0
  96. package/dist/utils/error-format.js +29 -0
  97. package/dist/utils/logger.js +175 -0
  98. package/dist/utils/safe-background-task.js +33 -0
  99. package/dist/variant/manager.js +103 -0
  100. package/dist/variant/types.js +1 -0
  101. package/package.json +76 -0
@@ -0,0 +1,283 @@
1
+ import path from "node:path";
2
+ import { getRuntimePaths } from "../runtime/paths.js";
3
+ import { logger } from "../utils/logger.js";
4
+ export const TOPIC_SESSION_STATUS = {
5
+ ACTIVE: "active",
6
+ CLOSED: "closed",
7
+ STALE: "stale",
8
+ ABANDONED: "abandoned",
9
+ ERROR: "error",
10
+ };
11
+ const GLOBAL_SCOPE_KEY = "global";
12
+ function getSettingsFilePath() {
13
+ return getRuntimePaths().settingsFilePath;
14
+ }
15
+ async function readSettingsFile() {
16
+ try {
17
+ const fs = await import("fs/promises");
18
+ const content = await fs.readFile(getSettingsFilePath(), "utf-8");
19
+ return JSON.parse(content);
20
+ }
21
+ catch (error) {
22
+ if (error.code !== "ENOENT") {
23
+ logger.error("[SettingsManager] Error reading settings file:", error);
24
+ }
25
+ return {};
26
+ }
27
+ }
28
+ let settingsWriteQueue = Promise.resolve();
29
+ function writeSettingsFile(settings) {
30
+ settingsWriteQueue = settingsWriteQueue
31
+ .catch(() => {
32
+ // Keep write queue alive after failed writes.
33
+ })
34
+ .then(async () => {
35
+ try {
36
+ const fs = await import("fs/promises");
37
+ const settingsFilePath = getSettingsFilePath();
38
+ await fs.mkdir(path.dirname(settingsFilePath), { recursive: true });
39
+ await fs.writeFile(settingsFilePath, JSON.stringify(settings, null, 2));
40
+ }
41
+ catch (err) {
42
+ logger.error("[SettingsManager] Error writing settings file:", err);
43
+ }
44
+ });
45
+ return settingsWriteQueue;
46
+ }
47
+ let currentSettings = {};
48
+ function getScopedMap(map, scopeKey) {
49
+ return map?.[scopeKey];
50
+ }
51
+ function setScopedMapValue(map, scopeKey, value) {
52
+ return {
53
+ ...(map ?? {}),
54
+ [scopeKey]: value,
55
+ };
56
+ }
57
+ function clearScopedMapValue(map, scopeKey) {
58
+ if (!map || !(scopeKey in map)) {
59
+ return map;
60
+ }
61
+ const rest = Object.fromEntries(Object.entries(map).filter(([key]) => key !== scopeKey));
62
+ return Object.keys(rest).length > 0 ? rest : undefined;
63
+ }
64
+ function isTopicSessionStatus(value) {
65
+ return Object.values(TOPIC_SESSION_STATUS).includes(value);
66
+ }
67
+ function sanitizeTopicSessionBindings(value) {
68
+ if (!value || typeof value !== "object") {
69
+ return undefined;
70
+ }
71
+ const sanitizedEntries = Object.entries(value)
72
+ .map(([bindingKey, rawBinding]) => {
73
+ if (!rawBinding || typeof rawBinding !== "object") {
74
+ return null;
75
+ }
76
+ const binding = rawBinding;
77
+ const now = Date.now();
78
+ const chatId = typeof binding.chatId === "number" ? binding.chatId : null;
79
+ const threadId = typeof binding.threadId === "number" ? binding.threadId : null;
80
+ const sessionId = typeof binding.sessionId === "string" ? binding.sessionId : "";
81
+ const projectId = typeof binding.projectId === "string" ? binding.projectId : "";
82
+ const scopeKey = typeof binding.scopeKey === "string" ? binding.scopeKey : "";
83
+ if (chatId === null || threadId === null || !sessionId || !projectId || !scopeKey) {
84
+ return null;
85
+ }
86
+ const createdAt = typeof binding.createdAt === "number" ? binding.createdAt : now;
87
+ const updatedAt = typeof binding.updatedAt === "number" ? binding.updatedAt : createdAt;
88
+ const sanitizedBinding = {
89
+ scopeKey,
90
+ chatId,
91
+ threadId,
92
+ sessionId,
93
+ projectId,
94
+ projectWorktree: typeof binding.projectWorktree === "string" ? binding.projectWorktree : undefined,
95
+ topicName: typeof binding.topicName === "string" ? binding.topicName : undefined,
96
+ status: isTopicSessionStatus(binding.status) ? binding.status : TOPIC_SESSION_STATUS.ACTIVE,
97
+ createdAt,
98
+ updatedAt,
99
+ closedAt: typeof binding.closedAt === "number" ? binding.closedAt : undefined,
100
+ };
101
+ return [bindingKey, sanitizedBinding];
102
+ })
103
+ .filter((entry) => entry !== null);
104
+ if (sanitizedEntries.length === 0) {
105
+ return undefined;
106
+ }
107
+ return Object.fromEntries(sanitizedEntries);
108
+ }
109
+ export function getCurrentProject(scopeKey = GLOBAL_SCOPE_KEY) {
110
+ return getScopedMap(currentSettings.scopedProjects, scopeKey);
111
+ }
112
+ export function setCurrentProject(projectInfo, scopeKey = GLOBAL_SCOPE_KEY) {
113
+ currentSettings.scopedProjects = setScopedMapValue(currentSettings.scopedProjects, scopeKey, projectInfo);
114
+ void writeSettingsFile(currentSettings);
115
+ }
116
+ export function clearProject(scopeKey = GLOBAL_SCOPE_KEY) {
117
+ currentSettings.scopedProjects = clearScopedMapValue(currentSettings.scopedProjects, scopeKey);
118
+ void writeSettingsFile(currentSettings);
119
+ }
120
+ export function getCurrentSession(scopeKey = GLOBAL_SCOPE_KEY) {
121
+ return getScopedMap(currentSettings.scopedSessions, scopeKey);
122
+ }
123
+ export function setCurrentSession(sessionInfo, scopeKey = GLOBAL_SCOPE_KEY) {
124
+ currentSettings.scopedSessions = setScopedMapValue(currentSettings.scopedSessions, scopeKey, sessionInfo);
125
+ void writeSettingsFile(currentSettings);
126
+ }
127
+ export function clearSession(scopeKey = GLOBAL_SCOPE_KEY) {
128
+ currentSettings.scopedSessions = clearScopedMapValue(currentSettings.scopedSessions, scopeKey);
129
+ void writeSettingsFile(currentSettings);
130
+ }
131
+ export function getScopedSessions() {
132
+ return { ...(currentSettings.scopedSessions ?? {}) };
133
+ }
134
+ export function setScopedSession(scopeKey, sessionInfo) {
135
+ setCurrentSession(sessionInfo, scopeKey);
136
+ }
137
+ export function clearScopedSession(scopeKey) {
138
+ clearSession(scopeKey);
139
+ }
140
+ export function getCurrentAgent(scopeKey = GLOBAL_SCOPE_KEY) {
141
+ return getScopedMap(currentSettings.scopedAgents, scopeKey);
142
+ }
143
+ export function setCurrentAgent(agentName, scopeKey = GLOBAL_SCOPE_KEY) {
144
+ currentSettings.scopedAgents = setScopedMapValue(currentSettings.scopedAgents, scopeKey, agentName);
145
+ void writeSettingsFile(currentSettings);
146
+ }
147
+ export function clearCurrentAgent(scopeKey = GLOBAL_SCOPE_KEY) {
148
+ currentSettings.scopedAgents = clearScopedMapValue(currentSettings.scopedAgents, scopeKey);
149
+ void writeSettingsFile(currentSettings);
150
+ }
151
+ export function getCurrentModel(scopeKey = GLOBAL_SCOPE_KEY) {
152
+ return getScopedMap(currentSettings.scopedModels, scopeKey);
153
+ }
154
+ export function getScopedModels() {
155
+ return { ...(currentSettings.scopedModels ?? {}) };
156
+ }
157
+ export function setCurrentModel(modelInfo, scopeKey = GLOBAL_SCOPE_KEY) {
158
+ currentSettings.scopedModels = setScopedMapValue(currentSettings.scopedModels, scopeKey, modelInfo);
159
+ void writeSettingsFile(currentSettings);
160
+ }
161
+ export function clearCurrentModel(scopeKey = GLOBAL_SCOPE_KEY) {
162
+ currentSettings.scopedModels = clearScopedMapValue(currentSettings.scopedModels, scopeKey);
163
+ void writeSettingsFile(currentSettings);
164
+ }
165
+ export function getScopedPinnedMessageId(scopeKey) {
166
+ return getScopedMap(currentSettings.scopedPinnedMessageIds, scopeKey);
167
+ }
168
+ export function setScopedPinnedMessageId(scopeKey, messageId) {
169
+ currentSettings.scopedPinnedMessageIds = setScopedMapValue(currentSettings.scopedPinnedMessageIds, scopeKey, messageId);
170
+ void writeSettingsFile(currentSettings);
171
+ }
172
+ export function clearScopedPinnedMessageId(scopeKey) {
173
+ currentSettings.scopedPinnedMessageIds = clearScopedMapValue(currentSettings.scopedPinnedMessageIds, scopeKey);
174
+ void writeSettingsFile(currentSettings);
175
+ }
176
+ export function getTopicSessionBindings() {
177
+ return { ...(currentSettings.topicSessionBindings ?? {}) };
178
+ }
179
+ export function getTopicSessionBinding(bindingKey) {
180
+ return getScopedMap(currentSettings.topicSessionBindings, bindingKey);
181
+ }
182
+ export function setTopicSessionBinding(bindingKey, binding) {
183
+ currentSettings.topicSessionBindings = setScopedMapValue(currentSettings.topicSessionBindings, bindingKey, binding);
184
+ void writeSettingsFile(currentSettings);
185
+ }
186
+ export function clearTopicSessionBinding(bindingKey) {
187
+ currentSettings.topicSessionBindings = clearScopedMapValue(currentSettings.topicSessionBindings, bindingKey);
188
+ void writeSettingsFile(currentSettings);
189
+ }
190
+ export function findTopicSessionBindingBySessionId(sessionId) {
191
+ return Object.values(currentSettings.topicSessionBindings ?? {}).find((binding) => binding.sessionId === sessionId);
192
+ }
193
+ export function findTopicSessionBindingByScopeKey(scopeKey) {
194
+ return Object.values(currentSettings.topicSessionBindings ?? {}).find((binding) => binding.scopeKey === scopeKey);
195
+ }
196
+ export function getTopicSessionBindingsByChat(chatId) {
197
+ return Object.values(currentSettings.topicSessionBindings ?? {}).filter((binding) => binding.chatId === chatId);
198
+ }
199
+ export function updateTopicSessionBindingStatus(bindingKey, status) {
200
+ const binding = getTopicSessionBinding(bindingKey);
201
+ if (!binding) {
202
+ return;
203
+ }
204
+ const updatedBinding = {
205
+ ...binding,
206
+ status,
207
+ updatedAt: Date.now(),
208
+ closedAt: status === TOPIC_SESSION_STATUS.CLOSED || status === TOPIC_SESSION_STATUS.STALE
209
+ ? Date.now()
210
+ : binding.closedAt,
211
+ };
212
+ setTopicSessionBinding(bindingKey, updatedBinding);
213
+ }
214
+ export function getServerProcess() {
215
+ return currentSettings.serverProcess;
216
+ }
217
+ export function setServerProcess(processInfo) {
218
+ currentSettings.serverProcess = processInfo;
219
+ void writeSettingsFile(currentSettings);
220
+ }
221
+ export function clearServerProcess() {
222
+ currentSettings.serverProcess = undefined;
223
+ void writeSettingsFile(currentSettings);
224
+ }
225
+ export function getSessionDirectoryCache() {
226
+ return currentSettings.sessionDirectoryCache;
227
+ }
228
+ export function setSessionDirectoryCache(cache) {
229
+ currentSettings.sessionDirectoryCache = cache;
230
+ return writeSettingsFile(currentSettings);
231
+ }
232
+ export function clearSessionDirectoryCache() {
233
+ currentSettings.sessionDirectoryCache = undefined;
234
+ void writeSettingsFile(currentSettings);
235
+ }
236
+ export function __resetSettingsForTests() {
237
+ currentSettings = {};
238
+ settingsWriteQueue = Promise.resolve();
239
+ }
240
+ export async function loadSettings() {
241
+ const loadedSettings = (await readSettingsFile());
242
+ let dirty = false;
243
+ if ("toolMessagesIntervalSec" in loadedSettings) {
244
+ delete loadedSettings.toolMessagesIntervalSec;
245
+ dirty = true;
246
+ }
247
+ if ("currentProject" in loadedSettings) {
248
+ delete loadedSettings.currentProject;
249
+ dirty = true;
250
+ }
251
+ if ("currentSession" in loadedSettings) {
252
+ delete loadedSettings.currentSession;
253
+ dirty = true;
254
+ }
255
+ if ("currentAgent" in loadedSettings) {
256
+ delete loadedSettings.currentAgent;
257
+ dirty = true;
258
+ }
259
+ if ("currentModel" in loadedSettings) {
260
+ delete loadedSettings.currentModel;
261
+ dirty = true;
262
+ }
263
+ if ("pinnedMessageId" in loadedSettings) {
264
+ delete loadedSettings.pinnedMessageId;
265
+ dirty = true;
266
+ }
267
+ const sanitizedTopicSessionBindings = sanitizeTopicSessionBindings(loadedSettings.topicSessionBindings);
268
+ if (loadedSettings.topicSessionBindings !== undefined) {
269
+ const loadedCount = loadedSettings.topicSessionBindings && typeof loadedSettings.topicSessionBindings === "object"
270
+ ? Object.keys(loadedSettings.topicSessionBindings).length
271
+ : 0;
272
+ const sanitizedCount = Object.keys(sanitizedTopicSessionBindings ?? {}).length;
273
+ if (loadedCount !== sanitizedCount) {
274
+ dirty = true;
275
+ logger.warn(`[SettingsManager] Removed ${loadedCount - sanitizedCount} invalid topic-session bindings during load`);
276
+ }
277
+ }
278
+ loadedSettings.topicSessionBindings = sanitizedTopicSessionBindings;
279
+ currentSettings = loadedSettings;
280
+ if (dirty) {
281
+ void writeSettingsFile(currentSettings);
282
+ }
283
+ }
@@ -0,0 +1,64 @@
1
+ import { config } from "../config.js";
2
+ import { logger } from "../utils/logger.js";
3
+ const STT_REQUEST_TIMEOUT_MS = 60_000;
4
+ /**
5
+ * Returns true if STT is configured (API URL and API key are set).
6
+ */
7
+ export function isSttConfigured() {
8
+ return Boolean(config.stt.apiUrl && config.stt.apiKey);
9
+ }
10
+ /**
11
+ * Transcribes an audio buffer using a Whisper-compatible API (OpenAI / Groq / etc.).
12
+ *
13
+ * Sends a multipart/form-data POST to `{STT_API_URL}/audio/transcriptions`.
14
+ *
15
+ * @param audioBuffer - Raw audio file bytes (ogg, mp3, wav, m4a, webm, etc.)
16
+ * @param filename - Original filename with extension (used by the API to detect format)
17
+ * @returns Transcribed text
18
+ * @throws Error if STT is not configured, the request fails, or the response is invalid
19
+ */
20
+ export async function transcribeAudio(audioBuffer, filename) {
21
+ if (!isSttConfigured()) {
22
+ throw new Error("STT is not configured: STT_API_URL and STT_API_KEY are required");
23
+ }
24
+ const url = `${config.stt.apiUrl}/audio/transcriptions`;
25
+ const formData = new FormData();
26
+ formData.append("file", new Blob([new Uint8Array(audioBuffer)]), filename);
27
+ formData.append("model", config.stt.model);
28
+ formData.append("response_format", "json");
29
+ if (config.stt.language) {
30
+ formData.append("language", config.stt.language);
31
+ }
32
+ logger.debug(`[STT] Sending transcription request: url=${url}, model=${config.stt.model}, filename=${filename}, size=${audioBuffer.length} bytes`);
33
+ const controller = new AbortController();
34
+ const timeout = setTimeout(() => controller.abort(), STT_REQUEST_TIMEOUT_MS);
35
+ try {
36
+ const response = await fetch(url, {
37
+ method: "POST",
38
+ headers: {
39
+ Authorization: `Bearer ${config.stt.apiKey}`,
40
+ },
41
+ body: formData,
42
+ signal: controller.signal,
43
+ });
44
+ if (!response.ok) {
45
+ const errorBody = await response.text().catch(() => "");
46
+ throw new Error(`STT API returned HTTP ${response.status}: ${errorBody || response.statusText}`);
47
+ }
48
+ const data = (await response.json());
49
+ if (typeof data.text !== "string") {
50
+ throw new Error("STT API response does not contain a text field");
51
+ }
52
+ logger.debug(`[STT] Transcription result: ${data.text.length} chars`);
53
+ return { text: data.text };
54
+ }
55
+ catch (err) {
56
+ if (err instanceof DOMException && err.name === "AbortError") {
57
+ throw new Error(`STT request timed out after ${STT_REQUEST_TIMEOUT_MS}ms`);
58
+ }
59
+ throw err;
60
+ }
61
+ finally {
62
+ clearTimeout(timeout);
63
+ }
64
+ }