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,389 @@
1
+ import { InlineKeyboard } from "grammy";
2
+ import { opencodeClient } from "../../opencode/client.js";
3
+ import { getCurrentProject } from "../../settings/manager.js";
4
+ import { clearSession, getCurrentSession, setCurrentSession, } from "../../session/manager.js";
5
+ import { ingestSessionInfoForCache } from "../../session/cache-manager.js";
6
+ import { interactionManager } from "../../interaction/manager.js";
7
+ import { summaryAggregator } from "../../summary/aggregator.js";
8
+ import { getStoredAgent } from "../../agent/manager.js";
9
+ import { getStoredModel } from "../../model/manager.js";
10
+ import { safeBackgroundTask } from "../../utils/safe-background-task.js";
11
+ import { logger } from "../../utils/logger.js";
12
+ import { t } from "../../i18n/index.js";
13
+ import { getScopeFromContext, getScopeKeyFromContext, getThreadSendOptions } from "../scope.js";
14
+ const COMMANDS_CALLBACK_PREFIX = "commands:";
15
+ const COMMANDS_CALLBACK_SELECT_PREFIX = `${COMMANDS_CALLBACK_PREFIX}select:`;
16
+ const COMMANDS_CALLBACK_CANCEL = `${COMMANDS_CALLBACK_PREFIX}cancel`;
17
+ const COMMANDS_CALLBACK_EXECUTE = `${COMMANDS_CALLBACK_PREFIX}execute`;
18
+ const MAX_INLINE_BUTTON_LABEL_LENGTH = 64;
19
+ function formatExecutingCommandMessage(commandName, args) {
20
+ const commandText = `/${commandName}`;
21
+ const argsSuffix = args ? ` ${args}` : "";
22
+ return `${t("commands.executing_prefix")}\n${commandText}${argsSuffix}`;
23
+ }
24
+ function normalizeDirectoryForCommandApi(directory) {
25
+ return directory.replace(/\\/g, "/");
26
+ }
27
+ function getCallbackMessageId(ctx) {
28
+ const message = ctx.callbackQuery?.message;
29
+ if (!message || !("message_id" in message)) {
30
+ return null;
31
+ }
32
+ const messageId = message.message_id;
33
+ return typeof messageId === "number" ? messageId : null;
34
+ }
35
+ function formatCommandButtonLabel(command) {
36
+ const description = command.description?.trim() || t("commands.no_description");
37
+ const rawLabel = `/${command.name} - ${description}`;
38
+ if (rawLabel.length <= MAX_INLINE_BUTTON_LABEL_LENGTH) {
39
+ return rawLabel;
40
+ }
41
+ return `${rawLabel.slice(0, MAX_INLINE_BUTTON_LABEL_LENGTH - 3)}...`;
42
+ }
43
+ function buildCommandsListKeyboard(commands) {
44
+ const keyboard = new InlineKeyboard();
45
+ commands.forEach((command, index) => {
46
+ keyboard
47
+ .text(formatCommandButtonLabel(command), `${COMMANDS_CALLBACK_SELECT_PREFIX}${index}`)
48
+ .row();
49
+ });
50
+ keyboard.text(t("commands.button.cancel"), COMMANDS_CALLBACK_CANCEL);
51
+ return keyboard;
52
+ }
53
+ function buildCommandsConfirmKeyboard() {
54
+ return new InlineKeyboard()
55
+ .text(t("commands.button.execute"), COMMANDS_CALLBACK_EXECUTE)
56
+ .text(t("commands.button.cancel"), COMMANDS_CALLBACK_CANCEL);
57
+ }
58
+ function parseCommandItems(value) {
59
+ if (!Array.isArray(value)) {
60
+ return null;
61
+ }
62
+ const commands = [];
63
+ for (const item of value) {
64
+ if (!item || typeof item !== "object") {
65
+ return null;
66
+ }
67
+ const commandName = item.name;
68
+ if (typeof commandName !== "string" || !commandName.trim()) {
69
+ return null;
70
+ }
71
+ const description = item.description;
72
+ commands.push({
73
+ name: commandName,
74
+ description: typeof description === "string" ? description : undefined,
75
+ });
76
+ }
77
+ return commands;
78
+ }
79
+ function parseCommandsMetadata(state) {
80
+ if (!state || state.kind !== "custom") {
81
+ return null;
82
+ }
83
+ const flow = state.metadata.flow;
84
+ const stage = state.metadata.stage;
85
+ const messageId = state.metadata.messageId;
86
+ const projectDirectory = state.metadata.projectDirectory;
87
+ if (flow !== "commands" ||
88
+ typeof messageId !== "number" ||
89
+ typeof projectDirectory !== "string") {
90
+ return null;
91
+ }
92
+ if (stage === "list") {
93
+ const commands = parseCommandItems(state.metadata.commands);
94
+ if (!commands) {
95
+ return null;
96
+ }
97
+ return {
98
+ flow,
99
+ stage,
100
+ messageId,
101
+ projectDirectory,
102
+ commands,
103
+ };
104
+ }
105
+ if (stage === "confirm") {
106
+ const commandName = state.metadata.commandName;
107
+ if (typeof commandName !== "string" || !commandName.trim()) {
108
+ return null;
109
+ }
110
+ return {
111
+ flow,
112
+ stage,
113
+ messageId,
114
+ projectDirectory,
115
+ commandName,
116
+ };
117
+ }
118
+ return null;
119
+ }
120
+ function clearCommandsInteraction(reason, scopeKey) {
121
+ const metadata = parseCommandsMetadata(interactionManager.getSnapshot(scopeKey));
122
+ if (metadata) {
123
+ interactionManager.clear(reason, scopeKey);
124
+ }
125
+ }
126
+ async function getCommandList(projectDirectory) {
127
+ const { data, error } = await opencodeClient.command.list({
128
+ directory: normalizeDirectoryForCommandApi(projectDirectory),
129
+ });
130
+ if (error || !data) {
131
+ throw error || new Error("No command data received");
132
+ }
133
+ return data
134
+ .filter((command) => typeof command.name === "string" && command.name.trim().length > 0)
135
+ .map((command) => ({
136
+ name: command.name,
137
+ description: command.description,
138
+ }));
139
+ }
140
+ function parseSelectIndex(data) {
141
+ if (!data.startsWith(COMMANDS_CALLBACK_SELECT_PREFIX)) {
142
+ return null;
143
+ }
144
+ const rawIndex = data.slice(COMMANDS_CALLBACK_SELECT_PREFIX.length);
145
+ const index = Number(rawIndex);
146
+ if (!Number.isInteger(index) || index < 0) {
147
+ return null;
148
+ }
149
+ return index;
150
+ }
151
+ async function isSessionBusy(sessionId, directory) {
152
+ try {
153
+ const { data, error } = await opencodeClient.session.status({ directory });
154
+ if (error || !data) {
155
+ logger.warn("[Commands] Failed to check session status before command:", error);
156
+ return false;
157
+ }
158
+ const sessionStatus = data[sessionId];
159
+ if (!sessionStatus) {
160
+ return false;
161
+ }
162
+ return sessionStatus.type === "busy";
163
+ }
164
+ catch (err) {
165
+ logger.warn("[Commands] Error checking session status before command:", err);
166
+ return false;
167
+ }
168
+ }
169
+ async function ensureSessionForProject(ctx, projectDirectory) {
170
+ const scopeKey = getScopeKeyFromContext(ctx);
171
+ let currentSession = getCurrentSession(scopeKey);
172
+ if (currentSession && currentSession.directory !== projectDirectory) {
173
+ logger.warn(`[Commands] Session/project mismatch detected. sessionDirectory=${currentSession.directory}, projectDirectory=${projectDirectory}. Resetting session context.`);
174
+ clearSession(scopeKey);
175
+ await ctx.reply(t("bot.session_reset_project_mismatch"));
176
+ currentSession = null;
177
+ }
178
+ if (currentSession) {
179
+ return currentSession;
180
+ }
181
+ await ctx.reply(t("bot.creating_session"));
182
+ const { data: session, error } = await opencodeClient.session.create({
183
+ directory: projectDirectory,
184
+ });
185
+ if (error || !session) {
186
+ await ctx.reply(t("bot.create_session_error"));
187
+ return null;
188
+ }
189
+ const sessionInfo = {
190
+ id: session.id,
191
+ title: session.title,
192
+ directory: projectDirectory,
193
+ };
194
+ setCurrentSession(sessionInfo, scopeKey);
195
+ summaryAggregator.setSession(sessionInfo.id);
196
+ await ingestSessionInfoForCache(session);
197
+ await ctx.reply(t("bot.session_created", { title: session.title }));
198
+ return sessionInfo;
199
+ }
200
+ async function executeCommand(ctx, deps, params) {
201
+ if (!ctx.chat) {
202
+ return;
203
+ }
204
+ const args = params.argumentsText.trim();
205
+ const threadId = getScopeFromContext(ctx)?.threadId ?? null;
206
+ await ctx.reply(formatExecutingCommandMessage(params.commandName, args));
207
+ const session = await ensureSessionForProject(ctx, params.projectDirectory);
208
+ if (!session) {
209
+ return;
210
+ }
211
+ await deps.ensureEventSubscription(session.directory);
212
+ summaryAggregator.setSession(session.id);
213
+ const sessionIsBusy = await isSessionBusy(session.id, session.directory);
214
+ if (sessionIsBusy) {
215
+ await ctx.reply(t("bot.session_busy"));
216
+ return;
217
+ }
218
+ const scopeKey = getScopeKeyFromContext(ctx);
219
+ const currentAgent = getStoredAgent(scopeKey);
220
+ const storedModel = getStoredModel(scopeKey);
221
+ const model = storedModel.providerID && storedModel.modelID
222
+ ? `${storedModel.providerID}/${storedModel.modelID}`
223
+ : undefined;
224
+ safeBackgroundTask({
225
+ taskName: "session.command",
226
+ task: () => opencodeClient.session.command({
227
+ sessionID: session.id,
228
+ directory: session.directory,
229
+ command: params.commandName,
230
+ arguments: args,
231
+ agent: currentAgent,
232
+ model,
233
+ variant: storedModel.variant,
234
+ }),
235
+ onSuccess: ({ error }) => {
236
+ if (error) {
237
+ logger.error("[Commands] OpenCode API returned an error for session.command", {
238
+ sessionId: session.id,
239
+ command: params.commandName,
240
+ args,
241
+ });
242
+ logger.error("[Commands] session.command error details:", error);
243
+ void ctx.api
244
+ .sendMessage(ctx.chat.id, t("commands.execute_error"), getThreadSendOptions(threadId))
245
+ .catch(() => { });
246
+ return;
247
+ }
248
+ logger.info(`[Commands] session.command completed: session=${session.id}, command=/${params.commandName}`);
249
+ },
250
+ onError: (error) => {
251
+ logger.error("[Commands] session.command background task failed", {
252
+ sessionId: session.id,
253
+ command: params.commandName,
254
+ args,
255
+ });
256
+ logger.error("[Commands] session.command background failure details:", error);
257
+ void ctx.api
258
+ .sendMessage(ctx.chat.id, t("commands.execute_error"), getThreadSendOptions(threadId))
259
+ .catch(() => { });
260
+ },
261
+ });
262
+ }
263
+ export async function commandsCommand(ctx) {
264
+ try {
265
+ const scopeKey = getScopeKeyFromContext(ctx);
266
+ const currentProject = getCurrentProject(scopeKey);
267
+ if (!currentProject) {
268
+ await ctx.reply(t("bot.project_not_selected"));
269
+ return;
270
+ }
271
+ const commands = await getCommandList(currentProject.worktree);
272
+ if (commands.length === 0) {
273
+ await ctx.reply(t("commands.empty"));
274
+ return;
275
+ }
276
+ const keyboard = buildCommandsListKeyboard(commands);
277
+ const message = await ctx.reply(t("commands.select"), {
278
+ reply_markup: keyboard,
279
+ });
280
+ interactionManager.start({
281
+ kind: "custom",
282
+ expectedInput: "callback",
283
+ metadata: {
284
+ flow: "commands",
285
+ stage: "list",
286
+ messageId: message.message_id,
287
+ projectDirectory: currentProject.worktree,
288
+ commands,
289
+ },
290
+ }, scopeKey);
291
+ }
292
+ catch (error) {
293
+ logger.error("[Commands] Error fetching commands list:", error);
294
+ await ctx.reply(t("commands.fetch_error"));
295
+ }
296
+ }
297
+ export async function handleCommandsCallback(ctx, deps) {
298
+ const data = ctx.callbackQuery?.data;
299
+ if (!data || !data.startsWith(COMMANDS_CALLBACK_PREFIX)) {
300
+ return false;
301
+ }
302
+ const scopeKey = getScopeKeyFromContext(ctx);
303
+ const metadata = parseCommandsMetadata(interactionManager.getSnapshot(scopeKey));
304
+ const callbackMessageId = getCallbackMessageId(ctx);
305
+ if (!metadata || callbackMessageId === null || metadata.messageId !== callbackMessageId) {
306
+ await ctx.answerCallbackQuery({ text: t("commands.inactive_callback"), show_alert: true });
307
+ return true;
308
+ }
309
+ try {
310
+ if (data === COMMANDS_CALLBACK_CANCEL) {
311
+ clearCommandsInteraction("commands_cancelled", scopeKey);
312
+ await ctx.answerCallbackQuery({ text: t("commands.cancelled_callback") });
313
+ await ctx.deleteMessage().catch(() => { });
314
+ return true;
315
+ }
316
+ if (data === COMMANDS_CALLBACK_EXECUTE) {
317
+ if (metadata.stage !== "confirm") {
318
+ await ctx.answerCallbackQuery({ text: t("commands.inactive_callback"), show_alert: true });
319
+ return true;
320
+ }
321
+ clearCommandsInteraction("commands_execute_clicked", scopeKey);
322
+ await ctx.answerCallbackQuery({ text: t("commands.execute_callback") });
323
+ await ctx.deleteMessage().catch(() => { });
324
+ await executeCommand(ctx, deps, {
325
+ projectDirectory: metadata.projectDirectory,
326
+ commandName: metadata.commandName,
327
+ argumentsText: "",
328
+ });
329
+ return true;
330
+ }
331
+ const commandIndex = parseSelectIndex(data);
332
+ if (commandIndex === null || metadata.stage !== "list") {
333
+ await ctx.answerCallbackQuery({ text: t("callback.processing_error"), show_alert: true });
334
+ return true;
335
+ }
336
+ const selectedCommand = metadata.commands[commandIndex];
337
+ if (!selectedCommand) {
338
+ await ctx.answerCallbackQuery({ text: t("commands.inactive_callback"), show_alert: true });
339
+ return true;
340
+ }
341
+ await ctx.answerCallbackQuery();
342
+ await ctx.editMessageText(t("commands.confirm", { command: `/${selectedCommand.name}` }), {
343
+ reply_markup: buildCommandsConfirmKeyboard(),
344
+ });
345
+ interactionManager.transition({
346
+ expectedInput: "mixed",
347
+ metadata: {
348
+ flow: "commands",
349
+ stage: "confirm",
350
+ messageId: metadata.messageId,
351
+ projectDirectory: metadata.projectDirectory,
352
+ commandName: selectedCommand.name,
353
+ },
354
+ }, scopeKey);
355
+ return true;
356
+ }
357
+ catch (error) {
358
+ logger.error("[Commands] Error handling command callback:", error);
359
+ clearCommandsInteraction("commands_callback_error", scopeKey);
360
+ await ctx.answerCallbackQuery({ text: t("callback.processing_error") }).catch(() => { });
361
+ return true;
362
+ }
363
+ }
364
+ export async function handleCommandTextArguments(ctx, deps) {
365
+ const text = ctx.message?.text;
366
+ if (!text || text.startsWith("/")) {
367
+ return false;
368
+ }
369
+ const scopeKey = getScopeKeyFromContext(ctx);
370
+ const metadata = parseCommandsMetadata(interactionManager.getSnapshot(scopeKey));
371
+ if (!metadata || metadata.stage !== "confirm") {
372
+ return false;
373
+ }
374
+ const argumentsText = text.trim();
375
+ if (!argumentsText) {
376
+ await ctx.reply(t("commands.arguments_empty"));
377
+ return true;
378
+ }
379
+ clearCommandsInteraction("commands_arguments_submitted", scopeKey);
380
+ if (ctx.chat) {
381
+ await ctx.api.deleteMessage(ctx.chat.id, metadata.messageId).catch(() => { });
382
+ }
383
+ await executeCommand(ctx, deps, {
384
+ projectDirectory: metadata.projectDirectory,
385
+ commandName: metadata.commandName,
386
+ argumentsText,
387
+ });
388
+ return true;
389
+ }
@@ -0,0 +1,20 @@
1
+ export const BOT_COMMAND = {
2
+ START: "start",
3
+ HELP: "help",
4
+ STATUS: "status",
5
+ NEW: "new",
6
+ ABORT: "abort",
7
+ SESSIONS: "sessions",
8
+ PROJECTS: "projects",
9
+ RENAME: "rename",
10
+ COMMANDS: "commands",
11
+ OPENCODE_START: "opencode_start",
12
+ OPENCODE_STOP: "opencode_stop",
13
+ };
14
+ export const DM_ALLOWED_COMMANDS = [
15
+ BOT_COMMAND.START,
16
+ BOT_COMMAND.HELP,
17
+ BOT_COMMAND.STATUS,
18
+ BOT_COMMAND.OPENCODE_START,
19
+ BOT_COMMAND.OPENCODE_STOP,
20
+ ];
@@ -0,0 +1,25 @@
1
+ import { t } from "../../i18n/index.js";
2
+ import { BOT_COMMAND } from "./constants.js";
3
+ /**
4
+ * List of all bot commands
5
+ * Update this array when adding new commands
6
+ */
7
+ const COMMAND_DEFINITIONS = [
8
+ { command: BOT_COMMAND.STATUS, descriptionKey: "cmd.description.status" },
9
+ { command: BOT_COMMAND.NEW, descriptionKey: "cmd.description.new" },
10
+ { command: BOT_COMMAND.ABORT, descriptionKey: "cmd.description.abort" },
11
+ { command: BOT_COMMAND.SESSIONS, descriptionKey: "cmd.description.sessions" },
12
+ { command: BOT_COMMAND.PROJECTS, descriptionKey: "cmd.description.projects" },
13
+ { command: BOT_COMMAND.RENAME, descriptionKey: "cmd.description.rename" },
14
+ { command: BOT_COMMAND.COMMANDS, descriptionKey: "cmd.description.commands" },
15
+ { command: BOT_COMMAND.OPENCODE_START, descriptionKey: "cmd.description.opencode_start" },
16
+ { command: BOT_COMMAND.OPENCODE_STOP, descriptionKey: "cmd.description.opencode_stop" },
17
+ { command: BOT_COMMAND.HELP, descriptionKey: "cmd.description.help" },
18
+ ];
19
+ export function getLocalizedBotCommands() {
20
+ return COMMAND_DEFINITIONS.map(({ command, descriptionKey }) => ({
21
+ command,
22
+ description: t(descriptionKey),
23
+ }));
24
+ }
25
+ export const BOT_COMMANDS = getLocalizedBotCommands();
@@ -0,0 +1,27 @@
1
+ import { t } from "../../i18n/index.js";
2
+ import { getLocalizedBotCommands } from "./definitions.js";
3
+ import { createDmKeyboard } from "../utils/keyboard.js";
4
+ function formatHelpText() {
5
+ const commands = getLocalizedBotCommands();
6
+ const lines = commands.map((item) => `/${item.command} - ${item.description}`);
7
+ return `📖 ${t("cmd.description.help")}\n\n${lines.join("\n")}\n\n${t("help.keyboard_hint")}`;
8
+ }
9
+ function formatDmHelpText() {
10
+ const lines = [
11
+ `/start - ${t("help.dm.command_start")}`,
12
+ `/status - ${t("cmd.description.status")}`,
13
+ `/help - ${t("cmd.description.help")}`,
14
+ `/opencode_start - ${t("cmd.description.opencode_start")}`,
15
+ `/opencode_stop - ${t("cmd.description.opencode_stop")}`,
16
+ ];
17
+ return `📖 ${t("help.dm.title")}\n\n${lines.join("\n")}\n\n${t("help.dm.hint")}`;
18
+ }
19
+ export async function helpCommand(ctx) {
20
+ if (ctx.chat?.type === "private") {
21
+ await ctx.reply(formatDmHelpText(), {
22
+ reply_markup: createDmKeyboard(),
23
+ });
24
+ return;
25
+ }
26
+ await ctx.reply(formatHelpText());
27
+ }
@@ -0,0 +1,38 @@
1
+ import { opencodeClient } from "../../opencode/client.js";
2
+ import { logger } from "../../utils/logger.js";
3
+ import { t } from "../../i18n/index.js";
4
+ export async function modelsCommand(ctx) {
5
+ try {
6
+ const { data: providersData, error } = await opencodeClient.config.providers();
7
+ if (error || !providersData) {
8
+ await ctx.reply(t("legacy.models.fetch_error"));
9
+ return;
10
+ }
11
+ const providers = providersData.providers;
12
+ if (!providers || providers.length === 0) {
13
+ await ctx.reply(t("legacy.models.empty"));
14
+ return;
15
+ }
16
+ let message = t("legacy.models.header");
17
+ for (const provider of providers) {
18
+ message += `🔹 **\`${provider.id}\`**\n`;
19
+ const models = Object.values(provider.models);
20
+ if (models.length === 0) {
21
+ message += t("legacy.models.no_provider_models");
22
+ }
23
+ else {
24
+ for (const model of models) {
25
+ message += ` • \`${model.id}\`\n`;
26
+ }
27
+ }
28
+ message += "\n";
29
+ }
30
+ message += t("legacy.models.env_hint");
31
+ message += "```\nOPENCODE_MODEL_PROVIDER=<provider.id>\nOPENCODE_MODEL_ID=<model.id>\n```";
32
+ await ctx.reply(message, { parse_mode: "Markdown" });
33
+ }
34
+ catch (error) {
35
+ logger.error("[ModelsCommand] Error listing models:", error);
36
+ await ctx.reply(t("legacy.models.error"));
37
+ }
38
+ }