zardbot-telegram 1.0.0

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 (111) hide show
  1. package/.env.example +116 -0
  2. package/LICENSE +21 -0
  3. package/README.md +250 -0
  4. package/dist/agent/manager.js +88 -0
  5. package/dist/agent/types.js +26 -0
  6. package/dist/app/start-bot-app.js +49 -0
  7. package/dist/bot/commands/abort.js +121 -0
  8. package/dist/bot/commands/commands.js +480 -0
  9. package/dist/bot/commands/definitions.js +27 -0
  10. package/dist/bot/commands/help.js +10 -0
  11. package/dist/bot/commands/models.js +38 -0
  12. package/dist/bot/commands/new.js +70 -0
  13. package/dist/bot/commands/opencode-start.js +101 -0
  14. package/dist/bot/commands/opencode-stop.js +44 -0
  15. package/dist/bot/commands/projects.js +223 -0
  16. package/dist/bot/commands/rename.js +139 -0
  17. package/dist/bot/commands/sessions.js +351 -0
  18. package/dist/bot/commands/start.js +43 -0
  19. package/dist/bot/commands/status.js +95 -0
  20. package/dist/bot/commands/task.js +399 -0
  21. package/dist/bot/commands/tasklist.js +220 -0
  22. package/dist/bot/commands/voice.js +145 -0
  23. package/dist/bot/handlers/agent.js +118 -0
  24. package/dist/bot/handlers/context.js +100 -0
  25. package/dist/bot/handlers/document.js +65 -0
  26. package/dist/bot/handlers/inline-menu.js +119 -0
  27. package/dist/bot/handlers/model.js +143 -0
  28. package/dist/bot/handlers/permission.js +235 -0
  29. package/dist/bot/handlers/prompt.js +240 -0
  30. package/dist/bot/handlers/question.js +390 -0
  31. package/dist/bot/handlers/tts.js +89 -0
  32. package/dist/bot/handlers/variant.js +138 -0
  33. package/dist/bot/handlers/voice.js +173 -0
  34. package/dist/bot/index.js +977 -0
  35. package/dist/bot/message-patterns.js +4 -0
  36. package/dist/bot/middleware/auth.js +30 -0
  37. package/dist/bot/middleware/interaction-guard.js +95 -0
  38. package/dist/bot/middleware/unknown-command.js +22 -0
  39. package/dist/bot/streaming/response-streamer.js +286 -0
  40. package/dist/bot/streaming/tool-call-streamer.js +285 -0
  41. package/dist/bot/utils/busy-guard.js +15 -0
  42. package/dist/bot/utils/commands.js +21 -0
  43. package/dist/bot/utils/file-download.js +91 -0
  44. package/dist/bot/utils/finalize-assistant-response.js +52 -0
  45. package/dist/bot/utils/keyboard.js +69 -0
  46. package/dist/bot/utils/send-with-markdown-fallback.js +165 -0
  47. package/dist/bot/utils/telegram-text.js +28 -0
  48. package/dist/bot/utils/thinking-message.js +8 -0
  49. package/dist/cli/args.js +98 -0
  50. package/dist/cli.js +80 -0
  51. package/dist/config.js +97 -0
  52. package/dist/i18n/de.js +357 -0
  53. package/dist/i18n/en.js +357 -0
  54. package/dist/i18n/es.js +357 -0
  55. package/dist/i18n/fr.js +357 -0
  56. package/dist/i18n/index.js +109 -0
  57. package/dist/i18n/ru.js +357 -0
  58. package/dist/i18n/zh.js +357 -0
  59. package/dist/index.js +26 -0
  60. package/dist/interaction/busy.js +8 -0
  61. package/dist/interaction/cleanup.js +32 -0
  62. package/dist/interaction/guard.js +140 -0
  63. package/dist/interaction/manager.js +106 -0
  64. package/dist/interaction/types.js +1 -0
  65. package/dist/keyboard/manager.js +172 -0
  66. package/dist/keyboard/types.js +1 -0
  67. package/dist/model/capabilities.js +62 -0
  68. package/dist/model/context-limit.js +57 -0
  69. package/dist/model/manager.js +259 -0
  70. package/dist/model/types.js +24 -0
  71. package/dist/opencode/client.js +13 -0
  72. package/dist/opencode/events.js +140 -0
  73. package/dist/permission/manager.js +100 -0
  74. package/dist/permission/types.js +1 -0
  75. package/dist/pinned/format.js +29 -0
  76. package/dist/pinned/manager.js +682 -0
  77. package/dist/pinned/types.js +1 -0
  78. package/dist/process/manager.js +273 -0
  79. package/dist/process/types.js +1 -0
  80. package/dist/project/manager.js +88 -0
  81. package/dist/question/manager.js +176 -0
  82. package/dist/question/types.js +1 -0
  83. package/dist/rename/manager.js +53 -0
  84. package/dist/runtime/bootstrap.js +350 -0
  85. package/dist/runtime/mode.js +74 -0
  86. package/dist/runtime/paths.js +37 -0
  87. package/dist/scheduled-task/creation-manager.js +113 -0
  88. package/dist/scheduled-task/display.js +239 -0
  89. package/dist/scheduled-task/executor.js +87 -0
  90. package/dist/scheduled-task/foreground-state.js +32 -0
  91. package/dist/scheduled-task/next-run.js +207 -0
  92. package/dist/scheduled-task/runtime.js +368 -0
  93. package/dist/scheduled-task/schedule-parser.js +169 -0
  94. package/dist/scheduled-task/store.js +65 -0
  95. package/dist/scheduled-task/types.js +19 -0
  96. package/dist/session/cache-manager.js +455 -0
  97. package/dist/session/manager.js +10 -0
  98. package/dist/settings/manager.js +158 -0
  99. package/dist/stt/client.js +97 -0
  100. package/dist/summary/aggregator.js +1136 -0
  101. package/dist/summary/formatter.js +491 -0
  102. package/dist/summary/subagent-formatter.js +63 -0
  103. package/dist/summary/tool-message-batcher.js +90 -0
  104. package/dist/tts/client.js +130 -0
  105. package/dist/utils/error-format.js +29 -0
  106. package/dist/utils/logger.js +127 -0
  107. package/dist/utils/safe-background-task.js +33 -0
  108. package/dist/utils/telegram-rate-limit-retry.js +93 -0
  109. package/dist/variant/manager.js +103 -0
  110. package/dist/variant/types.js +1 -0
  111. package/package.json +79 -0
@@ -0,0 +1,480 @@
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 { foregroundSessionState } from "../../scheduled-task/foreground-state.js";
14
+ import { config } from "../../config.js";
15
+ const COMMANDS_CALLBACK_PREFIX = "commands:";
16
+ const COMMANDS_CALLBACK_SELECT_PREFIX = `${COMMANDS_CALLBACK_PREFIX}select:`;
17
+ const COMMANDS_CALLBACK_PAGE_PREFIX = `${COMMANDS_CALLBACK_PREFIX}page:`;
18
+ const COMMANDS_CALLBACK_CANCEL = `${COMMANDS_CALLBACK_PREFIX}cancel`;
19
+ const COMMANDS_CALLBACK_EXECUTE = `${COMMANDS_CALLBACK_PREFIX}execute`;
20
+ const MAX_INLINE_BUTTON_LABEL_LENGTH = 64;
21
+ function formatExecutingCommandMessage(commandName, args) {
22
+ const prefix = t("commands.executing_prefix");
23
+ const commandText = `/${commandName}`;
24
+ const argsSuffix = args ? ` ${args}` : "";
25
+ return {
26
+ text: `${prefix}\n${commandText}${argsSuffix}`,
27
+ entities: [
28
+ {
29
+ type: "code",
30
+ offset: prefix.length + 1,
31
+ length: commandText.length,
32
+ },
33
+ ],
34
+ };
35
+ }
36
+ export function buildCommandPageCallback(page) {
37
+ return `${COMMANDS_CALLBACK_PAGE_PREFIX}${page}`;
38
+ }
39
+ export function parseCommandPageCallback(data) {
40
+ if (!data.startsWith(COMMANDS_CALLBACK_PAGE_PREFIX)) {
41
+ return null;
42
+ }
43
+ const rawPage = data.slice(COMMANDS_CALLBACK_PAGE_PREFIX.length);
44
+ const page = Number(rawPage);
45
+ if (!Number.isInteger(page) || page < 0) {
46
+ return null;
47
+ }
48
+ return page;
49
+ }
50
+ export function formatCommandsSelectText(page) {
51
+ if (page === 0) {
52
+ return t("commands.select");
53
+ }
54
+ return t("commands.select_page", { page: page + 1 });
55
+ }
56
+ function normalizeDirectoryForCommandApi(directory) {
57
+ return directory.replace(/\\/g, "/");
58
+ }
59
+ function getCallbackMessageId(ctx) {
60
+ const message = ctx.callbackQuery?.message;
61
+ if (!message || !("message_id" in message)) {
62
+ return null;
63
+ }
64
+ const messageId = message.message_id;
65
+ return typeof messageId === "number" ? messageId : null;
66
+ }
67
+ function formatCommandButtonLabel(command) {
68
+ const description = command.description?.trim() || t("commands.no_description");
69
+ const rawLabel = `/${command.name} - ${description}`;
70
+ if (rawLabel.length <= MAX_INLINE_BUTTON_LABEL_LENGTH) {
71
+ return rawLabel;
72
+ }
73
+ return `${rawLabel.slice(0, MAX_INLINE_BUTTON_LABEL_LENGTH - 3)}...`;
74
+ }
75
+ export function calculateCommandsPaginationRange(totalCommands, page, pageSize) {
76
+ const safePageSize = Math.max(1, pageSize);
77
+ const totalPages = Math.max(1, Math.ceil(totalCommands / safePageSize));
78
+ const normalizedPage = Math.min(Math.max(0, page), totalPages - 1);
79
+ const startIndex = normalizedPage * safePageSize;
80
+ const endIndex = Math.min(startIndex + safePageSize, totalCommands);
81
+ return {
82
+ page: normalizedPage,
83
+ totalPages,
84
+ startIndex,
85
+ endIndex,
86
+ };
87
+ }
88
+ function buildCommandsListKeyboard(commands, page, pageSize) {
89
+ const keyboard = new InlineKeyboard();
90
+ const { page: normalizedPage, totalPages, startIndex, endIndex, } = calculateCommandsPaginationRange(commands.length, page, pageSize);
91
+ commands.slice(startIndex, endIndex).forEach((command, index) => {
92
+ const globalIndex = startIndex + index;
93
+ keyboard
94
+ .text(formatCommandButtonLabel(command), `${COMMANDS_CALLBACK_SELECT_PREFIX}${globalIndex}`)
95
+ .row();
96
+ });
97
+ if (totalPages > 1) {
98
+ if (normalizedPage > 0) {
99
+ keyboard.text(t("commands.button.prev_page"), buildCommandPageCallback(normalizedPage - 1));
100
+ }
101
+ if (normalizedPage < totalPages - 1) {
102
+ keyboard.text(t("commands.button.next_page"), buildCommandPageCallback(normalizedPage + 1));
103
+ }
104
+ keyboard.row();
105
+ }
106
+ keyboard.text(t("commands.button.cancel"), COMMANDS_CALLBACK_CANCEL);
107
+ return keyboard;
108
+ }
109
+ function buildCommandsConfirmKeyboard() {
110
+ return new InlineKeyboard()
111
+ .text(t("commands.button.execute"), COMMANDS_CALLBACK_EXECUTE)
112
+ .text(t("commands.button.cancel"), COMMANDS_CALLBACK_CANCEL);
113
+ }
114
+ function parseCommandItems(value) {
115
+ if (!Array.isArray(value)) {
116
+ return null;
117
+ }
118
+ const commands = [];
119
+ for (const item of value) {
120
+ if (!item || typeof item !== "object") {
121
+ return null;
122
+ }
123
+ const commandName = item.name;
124
+ if (typeof commandName !== "string" || !commandName.trim()) {
125
+ return null;
126
+ }
127
+ const description = item.description;
128
+ commands.push({
129
+ name: commandName,
130
+ description: typeof description === "string" ? description : undefined,
131
+ });
132
+ }
133
+ return commands;
134
+ }
135
+ function parseCommandsMetadata(state) {
136
+ if (!state || state.kind !== "custom") {
137
+ return null;
138
+ }
139
+ const flow = state.metadata.flow;
140
+ const stage = state.metadata.stage;
141
+ const messageId = state.metadata.messageId;
142
+ const projectDirectory = state.metadata.projectDirectory;
143
+ if (flow !== "commands" ||
144
+ typeof messageId !== "number" ||
145
+ typeof projectDirectory !== "string") {
146
+ return null;
147
+ }
148
+ if (stage === "list") {
149
+ const commands = parseCommandItems(state.metadata.commands);
150
+ if (!commands) {
151
+ return null;
152
+ }
153
+ const page = typeof state.metadata.page === "number" && Number.isInteger(state.metadata.page)
154
+ ? Math.max(0, state.metadata.page)
155
+ : 0;
156
+ return {
157
+ flow,
158
+ stage,
159
+ messageId,
160
+ projectDirectory,
161
+ commands,
162
+ page,
163
+ };
164
+ }
165
+ if (stage === "confirm") {
166
+ const commandName = state.metadata.commandName;
167
+ if (typeof commandName !== "string" || !commandName.trim()) {
168
+ return null;
169
+ }
170
+ return {
171
+ flow,
172
+ stage,
173
+ messageId,
174
+ projectDirectory,
175
+ commandName,
176
+ };
177
+ }
178
+ return null;
179
+ }
180
+ function clearCommandsInteraction(reason) {
181
+ const metadata = parseCommandsMetadata(interactionManager.getSnapshot());
182
+ if (metadata) {
183
+ interactionManager.clear(reason);
184
+ }
185
+ }
186
+ async function getCommandList(projectDirectory) {
187
+ const { data, error } = await opencodeClient.command.list({
188
+ directory: normalizeDirectoryForCommandApi(projectDirectory),
189
+ });
190
+ if (error || !data) {
191
+ throw error || new Error("No command data received");
192
+ }
193
+ return data
194
+ .filter((command) => {
195
+ const source = command.source;
196
+ return (typeof command.name === "string" && command.name.trim().length > 0 && source === "command");
197
+ })
198
+ .map((command) => ({
199
+ name: command.name,
200
+ description: command.description,
201
+ }));
202
+ }
203
+ function parseSelectIndex(data) {
204
+ if (!data.startsWith(COMMANDS_CALLBACK_SELECT_PREFIX)) {
205
+ return null;
206
+ }
207
+ const rawIndex = data.slice(COMMANDS_CALLBACK_SELECT_PREFIX.length);
208
+ const index = Number(rawIndex);
209
+ if (!Number.isInteger(index) || index < 0) {
210
+ return null;
211
+ }
212
+ return index;
213
+ }
214
+ async function isSessionBusy(sessionId, directory) {
215
+ try {
216
+ const { data, error } = await opencodeClient.session.status({ directory });
217
+ if (error || !data) {
218
+ logger.warn("[Commands] Failed to check session status before command:", error);
219
+ return false;
220
+ }
221
+ const sessionStatus = data[sessionId];
222
+ if (!sessionStatus) {
223
+ return false;
224
+ }
225
+ return sessionStatus.type === "busy";
226
+ }
227
+ catch (err) {
228
+ logger.warn("[Commands] Error checking session status before command:", err);
229
+ return false;
230
+ }
231
+ }
232
+ async function ensureSessionForProject(ctx, projectDirectory) {
233
+ let currentSession = getCurrentSession();
234
+ if (currentSession && currentSession.directory !== projectDirectory) {
235
+ logger.warn(`[Commands] Session/project mismatch detected. sessionDirectory=${currentSession.directory}, projectDirectory=${projectDirectory}. Resetting session context.`);
236
+ clearSession();
237
+ summaryAggregator.clear();
238
+ foregroundSessionState.clearAll("session_mismatch_reset");
239
+ await ctx.reply(t("bot.session_reset_project_mismatch"));
240
+ currentSession = null;
241
+ }
242
+ if (currentSession) {
243
+ return currentSession;
244
+ }
245
+ await ctx.reply(t("bot.creating_session"));
246
+ const { data: session, error } = await opencodeClient.session.create({
247
+ directory: projectDirectory,
248
+ });
249
+ if (error || !session) {
250
+ await ctx.reply(t("bot.create_session_error"));
251
+ return null;
252
+ }
253
+ const sessionInfo = {
254
+ id: session.id,
255
+ title: session.title,
256
+ directory: projectDirectory,
257
+ };
258
+ setCurrentSession(sessionInfo);
259
+ await ingestSessionInfoForCache(session);
260
+ await ctx.reply(t("bot.session_created", { title: session.title }));
261
+ return sessionInfo;
262
+ }
263
+ async function executeCommand(ctx, deps, params) {
264
+ if (!ctx.chat) {
265
+ return;
266
+ }
267
+ const args = params.argumentsText.trim();
268
+ const executingMessage = formatExecutingCommandMessage(params.commandName, args);
269
+ await ctx.reply(executingMessage.text, { entities: executingMessage.entities });
270
+ const session = await ensureSessionForProject(ctx, params.projectDirectory);
271
+ if (!session) {
272
+ return;
273
+ }
274
+ await deps.ensureEventSubscription(session.directory);
275
+ summaryAggregator.setSession(session.id);
276
+ summaryAggregator.setBotAndChatId(deps.bot, ctx.chat.id);
277
+ const sessionIsBusy = await isSessionBusy(session.id, session.directory);
278
+ if (sessionIsBusy) {
279
+ await ctx.reply(t("bot.session_busy"));
280
+ return;
281
+ }
282
+ const currentAgent = getStoredAgent();
283
+ const storedModel = getStoredModel();
284
+ const model = storedModel.providerID && storedModel.modelID
285
+ ? `${storedModel.providerID}/${storedModel.modelID}`
286
+ : undefined;
287
+ foregroundSessionState.markBusy(session.id);
288
+ safeBackgroundTask({
289
+ taskName: "session.command",
290
+ task: () => opencodeClient.session.command({
291
+ sessionID: session.id,
292
+ directory: session.directory,
293
+ command: params.commandName,
294
+ arguments: args,
295
+ agent: currentAgent,
296
+ model,
297
+ variant: storedModel.variant,
298
+ }),
299
+ onSuccess: ({ error }) => {
300
+ if (error) {
301
+ foregroundSessionState.markIdle(session.id);
302
+ logger.error("[Commands] OpenCode API returned an error for session.command", {
303
+ sessionId: session.id,
304
+ command: params.commandName,
305
+ args,
306
+ });
307
+ logger.error("[Commands] session.command error details:", error);
308
+ void ctx.api.sendMessage(ctx.chat.id, t("commands.execute_error")).catch(() => { });
309
+ return;
310
+ }
311
+ logger.info(`[Commands] session.command completed: session=${session.id}, command=/${params.commandName}`);
312
+ },
313
+ onError: (error) => {
314
+ foregroundSessionState.markIdle(session.id);
315
+ logger.error("[Commands] session.command background task failed", {
316
+ sessionId: session.id,
317
+ command: params.commandName,
318
+ args,
319
+ });
320
+ logger.error("[Commands] session.command background failure details:", error);
321
+ void ctx.api.sendMessage(ctx.chat.id, t("commands.execute_error")).catch(() => { });
322
+ },
323
+ });
324
+ }
325
+ export async function commandsCommand(ctx) {
326
+ try {
327
+ const currentProject = getCurrentProject();
328
+ if (!currentProject) {
329
+ await ctx.reply(t("bot.project_not_selected"));
330
+ return;
331
+ }
332
+ const commands = await getCommandList(currentProject.worktree);
333
+ if (commands.length === 0) {
334
+ await ctx.reply(t("commands.empty"));
335
+ return;
336
+ }
337
+ const pageSize = config.bot.commandsListLimit;
338
+ const keyboard = buildCommandsListKeyboard(commands, 0, pageSize);
339
+ const message = await ctx.reply(formatCommandsSelectText(0), {
340
+ reply_markup: keyboard,
341
+ });
342
+ interactionManager.start({
343
+ kind: "custom",
344
+ expectedInput: "callback",
345
+ metadata: {
346
+ flow: "commands",
347
+ stage: "list",
348
+ messageId: message.message_id,
349
+ projectDirectory: currentProject.worktree,
350
+ commands,
351
+ page: 0,
352
+ },
353
+ });
354
+ }
355
+ catch (error) {
356
+ logger.error("[Commands] Error fetching commands list:", error);
357
+ await ctx.reply(t("commands.fetch_error"));
358
+ }
359
+ }
360
+ export async function handleCommandsCallback(ctx, deps) {
361
+ const data = ctx.callbackQuery?.data;
362
+ if (!data || !data.startsWith(COMMANDS_CALLBACK_PREFIX)) {
363
+ return false;
364
+ }
365
+ const metadata = parseCommandsMetadata(interactionManager.getSnapshot());
366
+ const callbackMessageId = getCallbackMessageId(ctx);
367
+ if (!metadata || callbackMessageId === null || metadata.messageId !== callbackMessageId) {
368
+ await ctx.answerCallbackQuery({ text: t("commands.inactive_callback"), show_alert: true });
369
+ return true;
370
+ }
371
+ try {
372
+ if (data === COMMANDS_CALLBACK_CANCEL) {
373
+ clearCommandsInteraction("commands_cancelled");
374
+ await ctx.answerCallbackQuery({ text: t("commands.cancelled_callback") });
375
+ await ctx.deleteMessage().catch(() => { });
376
+ return true;
377
+ }
378
+ if (data === COMMANDS_CALLBACK_EXECUTE) {
379
+ if (metadata.stage !== "confirm") {
380
+ await ctx.answerCallbackQuery({ text: t("commands.inactive_callback"), show_alert: true });
381
+ return true;
382
+ }
383
+ clearCommandsInteraction("commands_execute_clicked");
384
+ await ctx.answerCallbackQuery({ text: t("commands.execute_callback") });
385
+ await ctx.deleteMessage().catch(() => { });
386
+ await executeCommand(ctx, deps, {
387
+ projectDirectory: metadata.projectDirectory,
388
+ commandName: metadata.commandName,
389
+ argumentsText: "",
390
+ });
391
+ return true;
392
+ }
393
+ const page = parseCommandPageCallback(data);
394
+ if (page !== null) {
395
+ if (metadata.stage !== "list") {
396
+ await ctx.answerCallbackQuery({ text: t("callback.processing_error"), show_alert: true });
397
+ return true;
398
+ }
399
+ const pageSize = config.bot.commandsListLimit;
400
+ const { page: normalizedPage, totalPages } = calculateCommandsPaginationRange(metadata.commands.length, page, pageSize);
401
+ if (page >= totalPages || page < 0) {
402
+ await ctx.answerCallbackQuery({ text: t("commands.page_empty_callback") });
403
+ return true;
404
+ }
405
+ const keyboard = buildCommandsListKeyboard(metadata.commands, normalizedPage, pageSize);
406
+ await ctx.editMessageText(formatCommandsSelectText(normalizedPage), {
407
+ reply_markup: keyboard,
408
+ });
409
+ await ctx.answerCallbackQuery();
410
+ interactionManager.transition({
411
+ expectedInput: "callback",
412
+ metadata: {
413
+ flow: "commands",
414
+ stage: "list",
415
+ messageId: metadata.messageId,
416
+ projectDirectory: metadata.projectDirectory,
417
+ commands: metadata.commands,
418
+ page: normalizedPage,
419
+ },
420
+ });
421
+ return true;
422
+ }
423
+ const commandIndex = parseSelectIndex(data);
424
+ if (commandIndex === null || metadata.stage !== "list") {
425
+ await ctx.answerCallbackQuery({ text: t("callback.processing_error"), show_alert: true });
426
+ return true;
427
+ }
428
+ const selectedCommand = metadata.commands[commandIndex];
429
+ if (!selectedCommand) {
430
+ await ctx.answerCallbackQuery({ text: t("commands.inactive_callback"), show_alert: true });
431
+ return true;
432
+ }
433
+ await ctx.answerCallbackQuery();
434
+ await ctx.editMessageText(t("commands.confirm", { command: `/${selectedCommand.name}` }), {
435
+ reply_markup: buildCommandsConfirmKeyboard(),
436
+ });
437
+ interactionManager.transition({
438
+ expectedInput: "mixed",
439
+ metadata: {
440
+ flow: "commands",
441
+ stage: "confirm",
442
+ messageId: metadata.messageId,
443
+ projectDirectory: metadata.projectDirectory,
444
+ commandName: selectedCommand.name,
445
+ },
446
+ });
447
+ return true;
448
+ }
449
+ catch (error) {
450
+ logger.error("[Commands] Error handling command callback:", error);
451
+ clearCommandsInteraction("commands_callback_error");
452
+ await ctx.answerCallbackQuery({ text: t("callback.processing_error") }).catch(() => { });
453
+ return true;
454
+ }
455
+ }
456
+ export async function handleCommandTextArguments(ctx, deps) {
457
+ const text = ctx.message?.text;
458
+ if (!text || text.startsWith("/")) {
459
+ return false;
460
+ }
461
+ const metadata = parseCommandsMetadata(interactionManager.getSnapshot());
462
+ if (!metadata || metadata.stage !== "confirm") {
463
+ return false;
464
+ }
465
+ const argumentsText = text.trim();
466
+ if (!argumentsText) {
467
+ await ctx.reply(t("commands.arguments_empty"));
468
+ return true;
469
+ }
470
+ clearCommandsInteraction("commands_arguments_submitted");
471
+ if (ctx.chat) {
472
+ await ctx.api.deleteMessage(ctx.chat.id, metadata.messageId).catch(() => { });
473
+ }
474
+ await executeCommand(ctx, deps, {
475
+ projectDirectory: metadata.projectDirectory,
476
+ commandName: metadata.commandName,
477
+ argumentsText,
478
+ });
479
+ return true;
480
+ }
@@ -0,0 +1,27 @@
1
+ import { t } from "../../i18n/index.js";
2
+ /**
3
+ * List of all bot commands
4
+ * Update this array when adding new commands
5
+ */
6
+ const COMMAND_DEFINITIONS = [
7
+ { command: "status", descriptionKey: "cmd.description.status" },
8
+ { command: "new", descriptionKey: "cmd.description.new" },
9
+ { command: "abort", descriptionKey: "cmd.description.stop" },
10
+ { command: "sessions", descriptionKey: "cmd.description.sessions" },
11
+ { command: "projects", descriptionKey: "cmd.description.projects" },
12
+ { command: "task", descriptionKey: "cmd.description.task" },
13
+ { command: "tasklist", descriptionKey: "cmd.description.tasklist" },
14
+ { command: "rename", descriptionKey: "cmd.description.rename" },
15
+ { command: "voice", descriptionKey: "cmd.description.voice" },
16
+ { command: "commands", descriptionKey: "cmd.description.commands" },
17
+ { command: "opencode_start", descriptionKey: "cmd.description.opencode_start" },
18
+ { command: "opencode_stop", descriptionKey: "cmd.description.opencode_stop" },
19
+ { command: "help", descriptionKey: "cmd.description.help" },
20
+ ];
21
+ export function getLocalizedBotCommands() {
22
+ return COMMAND_DEFINITIONS.map(({ command, descriptionKey }) => ({
23
+ command,
24
+ description: t(descriptionKey),
25
+ }));
26
+ }
27
+ export const BOT_COMMANDS = getLocalizedBotCommands();
@@ -0,0 +1,10 @@
1
+ import { t } from "../../i18n/index.js";
2
+ import { getLocalizedBotCommands } from "./definitions.js";
3
+ function formatHelpText() {
4
+ const commands = getLocalizedBotCommands();
5
+ const lines = commands.map((item) => `/${item.command} - ${item.description}`);
6
+ return `📖 ${t("cmd.description.help")}\n\n${lines.join("\n")}\n\n${t("help.keyboard_hint")}`;
7
+ }
8
+ export async function helpCommand(ctx) {
9
+ await ctx.reply(formatHelpText());
10
+ }
@@ -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 += "OPENCODE_MODEL_PROVIDER=<provider.id>\nOPENCODE_MODEL_ID=<model.id>";
32
+ await ctx.reply(message);
33
+ }
34
+ catch (error) {
35
+ logger.error("[ModelsCommand] Error listing models:", error);
36
+ await ctx.reply(t("legacy.models.error"));
37
+ }
38
+ }
@@ -0,0 +1,70 @@
1
+ import { opencodeClient } from "../../opencode/client.js";
2
+ import { setCurrentSession } from "../../session/manager.js";
3
+ import { ingestSessionInfoForCache } from "../../session/cache-manager.js";
4
+ import { getCurrentProject } from "../../settings/manager.js";
5
+ import { clearAllInteractionState } from "../../interaction/cleanup.js";
6
+ import { summaryAggregator } from "../../summary/aggregator.js";
7
+ import { pinnedMessageManager } from "../../pinned/manager.js";
8
+ import { keyboardManager } from "../../keyboard/manager.js";
9
+ import { getStoredAgent } from "../../agent/manager.js";
10
+ import { getStoredModel } from "../../model/manager.js";
11
+ import { formatVariantForButton } from "../../variant/manager.js";
12
+ import { createMainKeyboard } from "../utils/keyboard.js";
13
+ import { isForegroundBusy, replyBusyBlocked } from "../utils/busy-guard.js";
14
+ import { logger } from "../../utils/logger.js";
15
+ import { t } from "../../i18n/index.js";
16
+ export async function newCommand(ctx) {
17
+ try {
18
+ if (isForegroundBusy()) {
19
+ await replyBusyBlocked(ctx);
20
+ return;
21
+ }
22
+ const currentProject = getCurrentProject();
23
+ if (!currentProject) {
24
+ await ctx.reply(t("new.project_not_selected"));
25
+ return;
26
+ }
27
+ logger.debug("[Bot] Creating new session for directory:", currentProject.worktree);
28
+ const { data: session, error } = await opencodeClient.session.create({
29
+ directory: currentProject.worktree,
30
+ });
31
+ if (error || !session) {
32
+ throw error || new Error("No data received from server");
33
+ }
34
+ logger.info(`[Bot] Created new session via /new command: id=${session.id}, title="${session.title}", project=${currentProject.worktree}`);
35
+ const sessionInfo = {
36
+ id: session.id,
37
+ title: session.title,
38
+ directory: currentProject.worktree,
39
+ };
40
+ setCurrentSession(sessionInfo);
41
+ summaryAggregator.clear();
42
+ clearAllInteractionState("session_created");
43
+ await ingestSessionInfoForCache(session);
44
+ // Initialize pinned message manager and create pinned message
45
+ if (!pinnedMessageManager.isInitialized()) {
46
+ pinnedMessageManager.initialize(ctx.api, ctx.chat.id);
47
+ }
48
+ // Initialize keyboard manager if not already
49
+ keyboardManager.initialize(ctx.api, ctx.chat.id);
50
+ try {
51
+ await pinnedMessageManager.onSessionChange(session.id, session.title);
52
+ }
53
+ catch (err) {
54
+ logger.error("[Bot] Error creating pinned message:", err);
55
+ }
56
+ // Get current state for keyboard
57
+ const currentAgent = getStoredAgent();
58
+ const currentModel = getStoredModel();
59
+ const contextInfo = pinnedMessageManager.getContextInfo();
60
+ const variantName = formatVariantForButton(currentModel.variant || "default");
61
+ const keyboard = createMainKeyboard(currentAgent, currentModel, contextInfo ?? undefined, variantName);
62
+ await ctx.reply(t("new.created", { title: session.title }), {
63
+ reply_markup: keyboard,
64
+ });
65
+ }
66
+ catch (error) {
67
+ logger.error("[Bot] Error creating session:", error);
68
+ await ctx.reply(t("new.create_error"));
69
+ }
70
+ }