@grinev/opencode-telegram-bot 0.11.4 → 0.12.1

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.
@@ -0,0 +1,220 @@
1
+ import { InlineKeyboard } from "grammy";
2
+ import { getDateLocale, t } from "../../i18n/index.js";
3
+ import { interactionManager } from "../../interaction/manager.js";
4
+ import { formatTaskListBadge } from "../../scheduled-task/display.js";
5
+ import { scheduledTaskRuntime } from "../../scheduled-task/runtime.js";
6
+ import { getScheduledTask, listScheduledTasks, removeScheduledTask, } from "../../scheduled-task/store.js";
7
+ import { logger } from "../../utils/logger.js";
8
+ const TASKLIST_CALLBACK_PREFIX = "tasklist:";
9
+ const TASKLIST_OPEN_PREFIX = `${TASKLIST_CALLBACK_PREFIX}open:`;
10
+ const TASKLIST_DELETE_PREFIX = `${TASKLIST_CALLBACK_PREFIX}delete:`;
11
+ const TASKLIST_CANCEL_CALLBACK = `${TASKLIST_CALLBACK_PREFIX}cancel`;
12
+ const MAX_INLINE_BUTTON_LABEL_LENGTH = 64;
13
+ function getCallbackMessageId(ctx) {
14
+ const message = ctx.callbackQuery?.message;
15
+ if (!message || !("message_id" in message)) {
16
+ return null;
17
+ }
18
+ const messageId = message.message_id;
19
+ return typeof messageId === "number" ? messageId : null;
20
+ }
21
+ function parseTaskListMetadata(state) {
22
+ if (!state || state.kind !== "custom") {
23
+ return null;
24
+ }
25
+ const flow = state.metadata.flow;
26
+ const stage = state.metadata.stage;
27
+ const messageId = state.metadata.messageId;
28
+ if (flow !== "tasklist" || typeof messageId !== "number") {
29
+ return null;
30
+ }
31
+ if (stage === "list") {
32
+ return {
33
+ flow,
34
+ stage,
35
+ messageId,
36
+ };
37
+ }
38
+ if (stage === "detail") {
39
+ const taskId = state.metadata.taskId;
40
+ if (typeof taskId !== "string" || !taskId) {
41
+ return null;
42
+ }
43
+ return {
44
+ flow,
45
+ stage,
46
+ messageId,
47
+ taskId,
48
+ };
49
+ }
50
+ return null;
51
+ }
52
+ function clearTaskListInteraction(reason) {
53
+ const metadata = parseTaskListMetadata(interactionManager.getSnapshot());
54
+ if (metadata) {
55
+ interactionManager.clear(reason);
56
+ }
57
+ }
58
+ function truncateText(text, maxLength) {
59
+ if (text.length <= maxLength) {
60
+ return text;
61
+ }
62
+ return `${text.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
63
+ }
64
+ function formatDateTime(dateIso, timezone) {
65
+ if (!dateIso) {
66
+ return "-";
67
+ }
68
+ try {
69
+ return new Intl.DateTimeFormat(getDateLocale(), {
70
+ dateStyle: "medium",
71
+ timeStyle: "short",
72
+ timeZone: timezone,
73
+ }).format(new Date(dateIso));
74
+ }
75
+ catch {
76
+ return dateIso;
77
+ }
78
+ }
79
+ function formatTaskButtonPrefix(task) {
80
+ return formatTaskListBadge(task);
81
+ }
82
+ function formatTaskButtonLabel(task) {
83
+ const prefix = `[${formatTaskButtonPrefix(task)}]`;
84
+ const prompt = task.prompt.replace(/\s+/g, " ").trim();
85
+ return truncateText(`${prefix} ${prompt}`, MAX_INLINE_BUTTON_LABEL_LENGTH);
86
+ }
87
+ function buildTaskListKeyboard(tasks) {
88
+ const keyboard = new InlineKeyboard();
89
+ tasks.forEach((task) => {
90
+ keyboard.text(formatTaskButtonLabel(task), `${TASKLIST_OPEN_PREFIX}${task.id}`).row();
91
+ });
92
+ keyboard.text(t("tasklist.button.cancel"), TASKLIST_CANCEL_CALLBACK);
93
+ return keyboard;
94
+ }
95
+ function buildTaskDetailsKeyboard(taskId) {
96
+ return new InlineKeyboard()
97
+ .text(t("tasklist.button.delete"), `${TASKLIST_DELETE_PREFIX}${taskId}`)
98
+ .text(t("tasklist.button.cancel"), TASKLIST_CANCEL_CALLBACK);
99
+ }
100
+ function sortTasks(tasks) {
101
+ return [...tasks].sort((left, right) => {
102
+ const leftNextRun = left.nextRunAt ? Date.parse(left.nextRunAt) : Number.POSITIVE_INFINITY;
103
+ const rightNextRun = right.nextRunAt ? Date.parse(right.nextRunAt) : Number.POSITIVE_INFINITY;
104
+ if (leftNextRun !== rightNextRun) {
105
+ return leftNextRun - rightNextRun;
106
+ }
107
+ return left.createdAt.localeCompare(right.createdAt);
108
+ });
109
+ }
110
+ function formatTaskDetails(task) {
111
+ const cronLine = task.kind === "cron" ? `${t("tasklist.details.cron", { cron: task.cron })}\n` : "";
112
+ return t("tasklist.details", {
113
+ prompt: task.prompt,
114
+ project: task.projectWorktree,
115
+ schedule: task.scheduleSummary,
116
+ cronLine,
117
+ timezone: task.timezone,
118
+ nextRunAt: formatDateTime(task.nextRunAt, task.timezone),
119
+ lastRunAt: formatDateTime(task.lastRunAt, task.timezone),
120
+ runCount: String(task.runCount),
121
+ });
122
+ }
123
+ export async function taskListCommand(ctx) {
124
+ try {
125
+ const tasks = sortTasks(listScheduledTasks());
126
+ if (tasks.length === 0) {
127
+ await ctx.reply(t("tasklist.empty"));
128
+ return;
129
+ }
130
+ const message = await ctx.reply(t("tasklist.select"), {
131
+ reply_markup: buildTaskListKeyboard(tasks),
132
+ });
133
+ interactionManager.start({
134
+ kind: "custom",
135
+ expectedInput: "callback",
136
+ metadata: {
137
+ flow: "tasklist",
138
+ stage: "list",
139
+ messageId: message.message_id,
140
+ },
141
+ });
142
+ }
143
+ catch (error) {
144
+ logger.error("[TaskList] Failed to open task list", error);
145
+ await ctx.reply(t("tasklist.load_error"));
146
+ }
147
+ }
148
+ export async function handleTaskListCallback(ctx) {
149
+ const data = ctx.callbackQuery?.data;
150
+ if (!data || !data.startsWith(TASKLIST_CALLBACK_PREFIX)) {
151
+ return false;
152
+ }
153
+ const metadata = parseTaskListMetadata(interactionManager.getSnapshot());
154
+ const callbackMessageId = getCallbackMessageId(ctx);
155
+ if (!metadata || callbackMessageId === null || metadata.messageId !== callbackMessageId) {
156
+ await ctx.answerCallbackQuery({ text: t("tasklist.inactive_callback"), show_alert: true });
157
+ return true;
158
+ }
159
+ try {
160
+ if (data === TASKLIST_CANCEL_CALLBACK) {
161
+ clearTaskListInteraction("tasklist_cancelled");
162
+ await ctx.answerCallbackQuery({ text: t("tasklist.cancelled_callback") });
163
+ await ctx.deleteMessage().catch(() => { });
164
+ return true;
165
+ }
166
+ if (data.startsWith(TASKLIST_OPEN_PREFIX)) {
167
+ if (metadata.stage !== "list") {
168
+ await ctx.answerCallbackQuery({ text: t("tasklist.inactive_callback"), show_alert: true });
169
+ return true;
170
+ }
171
+ const taskId = data.slice(TASKLIST_OPEN_PREFIX.length);
172
+ const task = getScheduledTask(taskId);
173
+ if (!task) {
174
+ clearTaskListInteraction("tasklist_selected_task_missing");
175
+ await ctx.answerCallbackQuery({ text: t("tasklist.inactive_callback"), show_alert: true });
176
+ await ctx.deleteMessage().catch(() => { });
177
+ return true;
178
+ }
179
+ await ctx.answerCallbackQuery();
180
+ await ctx.editMessageText(formatTaskDetails(task), {
181
+ reply_markup: buildTaskDetailsKeyboard(task.id),
182
+ });
183
+ interactionManager.transition({
184
+ expectedInput: "callback",
185
+ metadata: {
186
+ flow: "tasklist",
187
+ stage: "detail",
188
+ messageId: metadata.messageId,
189
+ taskId: task.id,
190
+ },
191
+ });
192
+ return true;
193
+ }
194
+ if (data.startsWith(TASKLIST_DELETE_PREFIX)) {
195
+ if (metadata.stage !== "detail") {
196
+ await ctx.answerCallbackQuery({ text: t("tasklist.inactive_callback"), show_alert: true });
197
+ return true;
198
+ }
199
+ const taskId = data.slice(TASKLIST_DELETE_PREFIX.length);
200
+ if (taskId !== metadata.taskId) {
201
+ await ctx.answerCallbackQuery({ text: t("tasklist.inactive_callback"), show_alert: true });
202
+ return true;
203
+ }
204
+ await removeScheduledTask(taskId);
205
+ scheduledTaskRuntime.removeTask(taskId);
206
+ clearTaskListInteraction("tasklist_deleted");
207
+ await ctx.answerCallbackQuery({ text: t("tasklist.deleted_callback") });
208
+ await ctx.deleteMessage().catch(() => { });
209
+ return true;
210
+ }
211
+ await ctx.answerCallbackQuery({ text: t("callback.processing_error"), show_alert: true });
212
+ return true;
213
+ }
214
+ catch (error) {
215
+ logger.error("[TaskList] Failed to handle task list callback", error);
216
+ clearTaskListInteraction("tasklist_callback_error");
217
+ await ctx.answerCallbackQuery({ text: t("callback.processing_error") }).catch(() => { });
218
+ return true;
219
+ }
220
+ }
@@ -16,6 +16,7 @@ import { safeBackgroundTask } from "../../utils/safe-background-task.js";
16
16
  import { formatErrorDetails } from "../../utils/error-format.js";
17
17
  import { logger } from "../../utils/logger.js";
18
18
  import { t } from "../../i18n/index.js";
19
+ import { foregroundSessionState } from "../../scheduled-task/foreground-state.js";
19
20
  /** Module-level references for async callbacks that don't have ctx. */
20
21
  let botInstance = null;
21
22
  let chatIdInstance = null;
@@ -47,6 +48,7 @@ async function isSessionBusy(sessionId, directory) {
47
48
  async function resetMismatchedSessionContext() {
48
49
  stopEventListening();
49
50
  summaryAggregator.clear();
51
+ foregroundSessionState.clearAll("session_mismatch_reset");
50
52
  clearAllInteractionState("session_mismatch_reset");
51
53
  clearSession();
52
54
  keyboardManager.clearContext();
@@ -192,6 +194,7 @@ export async function processUserPrompt(ctx, text, deps, fileParts = []) {
192
194
  fileCount: fileParts.length,
193
195
  };
194
196
  logger.info(`[Bot] Calling session.prompt (fire-and-forget) with agent=${currentAgent}, fileCount=${fileParts.length}...`);
197
+ foregroundSessionState.markBusy(currentSession.id);
195
198
  // CRITICAL: DO NOT wait for session.prompt to complete.
196
199
  // If we wait, the handler will not finish and grammY will not call getUpdates,
197
200
  // which blocks receiving button callback_query updates.
@@ -201,6 +204,7 @@ export async function processUserPrompt(ctx, text, deps, fileParts = []) {
201
204
  task: () => opencodeClient.session.prompt(promptOptions),
202
205
  onSuccess: ({ error }) => {
203
206
  if (error) {
207
+ foregroundSessionState.markIdle(currentSession.id);
204
208
  const details = formatErrorDetails(error, 6000);
205
209
  logger.error("[Bot] OpenCode API returned an error for session.prompt", promptErrorLogContext);
206
210
  logger.error("[Bot] session.prompt error details:", details);
@@ -212,6 +216,7 @@ export async function processUserPrompt(ctx, text, deps, fileParts = []) {
212
216
  logger.info("[Bot] session.prompt completed");
213
217
  },
214
218
  onError: (error) => {
219
+ foregroundSessionState.markIdle(currentSession.id);
215
220
  const details = formatErrorDetails(error, 6000);
216
221
  logger.error("[Bot] session.prompt background task failed", promptErrorLogContext);
217
222
  logger.error("[Bot] session.prompt background failure details:", details);
@@ -222,6 +227,9 @@ export async function processUserPrompt(ctx, text, deps, fileParts = []) {
222
227
  return true;
223
228
  }
224
229
  catch (err) {
230
+ if (currentSession) {
231
+ foregroundSessionState.markIdle(currentSession.id);
232
+ }
225
233
  logger.error("Error in prompt handler:", err);
226
234
  if (interactionManager.getSnapshot()) {
227
235
  clearAllInteractionState("message_handler_error");
package/dist/bot/index.js CHANGED
@@ -20,6 +20,8 @@ import { abortCommand } from "./commands/abort.js";
20
20
  import { opencodeStartCommand } from "./commands/opencode-start.js";
21
21
  import { opencodeStopCommand } from "./commands/opencode-stop.js";
22
22
  import { renameCommand, handleRenameCancel, handleRenameTextAnswer } from "./commands/rename.js";
23
+ import { handleTaskCallback, handleTaskTextInput, taskCommand } from "./commands/task.js";
24
+ import { handleTaskListCallback, taskListCommand } from "./commands/tasklist.js";
23
25
  import { commandsCommand, handleCommandsCallback, handleCommandTextArguments, } from "./commands/commands.js";
24
26
  import { handleQuestionCallback, showCurrentQuestion, handleQuestionTextAnswer, } from "./handlers/question.js";
25
27
  import { handlePermissionCallback, showPermissionRequest } from "./handlers/permission.js";
@@ -49,6 +51,8 @@ import { downloadTelegramFile, toDataUri } from "./utils/file-download.js";
49
51
  import { sendBotText } from "./utils/telegram-text.js";
50
52
  import { getModelCapabilities, supportsInput } from "../model/capabilities.js";
51
53
  import { getStoredModel } from "../model/manager.js";
54
+ import { foregroundSessionState } from "../scheduled-task/foreground-state.js";
55
+ import { scheduledTaskRuntime } from "../scheduled-task/runtime.js";
52
56
  let botInstance = null;
53
57
  let chatIdInstance = null;
54
58
  let commandsInitialized = false;
@@ -57,6 +61,12 @@ const SESSION_RETRY_PREFIX = "🔁";
57
61
  const __filename = fileURLToPath(import.meta.url);
58
62
  const __dirname = path.dirname(__filename);
59
63
  const TEMP_DIR = path.join(__dirname, "..", ".tmp");
64
+ function getCurrentReplyKeyboard() {
65
+ if (!keyboardManager.isInitialized()) {
66
+ return undefined;
67
+ }
68
+ return keyboardManager.getKeyboard();
69
+ }
60
70
  function prepareDocumentCaption(caption) {
61
71
  const normalizedCaption = caption.trim();
62
72
  if (!normalizedCaption) {
@@ -77,8 +87,10 @@ const toolMessageBatcher = new ToolMessageBatcher({
77
87
  if (!currentSession || currentSession.id !== sessionId) {
78
88
  return;
79
89
  }
90
+ const keyboard = getCurrentReplyKeyboard();
80
91
  await botInstance.api.sendMessage(chatIdInstance, text, {
81
92
  disable_notification: true,
93
+ ...(keyboard ? { reply_markup: keyboard } : {}),
82
94
  });
83
95
  },
84
96
  sendFile: async (sessionId, fileData) => {
@@ -94,9 +106,11 @@ const toolMessageBatcher = new ToolMessageBatcher({
94
106
  logger.debug(`[Bot] Sending code file: ${fileData.filename} (${fileData.buffer.length} bytes, session=${sessionId})`);
95
107
  await fs.mkdir(TEMP_DIR, { recursive: true });
96
108
  await fs.writeFile(tempFilePath, fileData.buffer);
109
+ const keyboard = getCurrentReplyKeyboard();
97
110
  await botInstance.api.sendDocument(chatIdInstance, new InputFile(tempFilePath), {
98
111
  caption: fileData.caption,
99
112
  disable_notification: true,
113
+ ...(keyboard ? { reply_markup: keyboard } : {}),
100
114
  });
101
115
  }
102
116
  finally {
@@ -141,10 +155,13 @@ async function ensureEventSubscription(directory) {
141
155
  summaryAggregator.setOnComplete(async (sessionId, messageText) => {
142
156
  if (!botInstance || !chatIdInstance) {
143
157
  logger.error("Bot or chat ID not available for sending message");
158
+ foregroundSessionState.markIdle(sessionId);
144
159
  return;
145
160
  }
146
161
  const currentSession = getCurrentSession();
147
162
  if (currentSession?.id !== sessionId) {
163
+ foregroundSessionState.markIdle(sessionId);
164
+ await scheduledTaskRuntime.flushDeferredDeliveries();
148
165
  return;
149
166
  }
150
167
  await toolMessageBatcher.flushSession(sessionId, "assistant_message_completed");
@@ -154,8 +171,7 @@ async function ensureEventSubscription(directory) {
154
171
  const assistantMessageFormat = assistantParseMode === "MarkdownV2" ? "markdown_v2" : "raw";
155
172
  logger.debug(`[Bot] Sending completed message to Telegram (chatId=${chatIdInstance}, parts=${parts.length})`);
156
173
  for (let i = 0; i < parts.length; i++) {
157
- const isLastPart = i === parts.length - 1;
158
- const keyboard = isLastPart && keyboardManager.isInitialized() ? keyboardManager.getKeyboard() : undefined;
174
+ const keyboard = getCurrentReplyKeyboard();
159
175
  const options = keyboard ? { reply_markup: keyboard } : undefined;
160
176
  await sendBotText({
161
177
  api: botInstance.api,
@@ -172,6 +188,10 @@ async function ensureEventSubscription(directory) {
172
188
  logger.error("[Bot] CRITICAL: Stopping event processing due to error");
173
189
  summaryAggregator.clear();
174
190
  }
191
+ finally {
192
+ foregroundSessionState.markIdle(sessionId);
193
+ await scheduledTaskRuntime.flushDeferredDeliveries();
194
+ }
175
195
  });
176
196
  summaryAggregator.setOnTool(async (toolInfo) => {
177
197
  if (!botInstance || !chatIdInstance) {
@@ -294,6 +314,18 @@ async function ensureEventSubscription(directory) {
294
314
  logger.error("[Bot] Error updating pinned message with tokens:", err);
295
315
  }
296
316
  });
317
+ summaryAggregator.setOnCost(async (cost) => {
318
+ if (!pinnedMessageManager.isInitialized()) {
319
+ return;
320
+ }
321
+ try {
322
+ logger.debug(`[Bot] Cost update: $${cost.toFixed(2)}`);
323
+ await pinnedMessageManager.onCostUpdate(cost);
324
+ }
325
+ catch (err) {
326
+ logger.error("[Bot] Error updating cost:", err);
327
+ }
328
+ });
297
329
  summaryAggregator.setOnSessionCompacted(async (sessionId, directory) => {
298
330
  if (!pinnedMessageManager.isInitialized()) {
299
331
  return;
@@ -308,10 +340,13 @@ async function ensureEventSubscription(directory) {
308
340
  });
309
341
  summaryAggregator.setOnSessionError(async (sessionId, message) => {
310
342
  if (!botInstance || !chatIdInstance) {
343
+ foregroundSessionState.markIdle(sessionId);
311
344
  return;
312
345
  }
313
346
  const currentSession = getCurrentSession();
314
347
  if (!currentSession || currentSession.id !== sessionId) {
348
+ foregroundSessionState.markIdle(sessionId);
349
+ await scheduledTaskRuntime.flushDeferredDeliveries();
315
350
  return;
316
351
  }
317
352
  await toolMessageBatcher.flushSession(sessionId, "session_error");
@@ -324,6 +359,8 @@ async function ensureEventSubscription(directory) {
324
359
  .catch((err) => {
325
360
  logger.error("[Bot] Failed to send session.error message:", err);
326
361
  });
362
+ foregroundSessionState.markIdle(sessionId);
363
+ await scheduledTaskRuntime.flushDeferredDeliveries();
327
364
  });
328
365
  summaryAggregator.setOnSessionRetry(async ({ sessionId, message }) => {
329
366
  if (!botInstance || !chatIdInstance) {
@@ -458,6 +495,8 @@ export function createBot() {
458
495
  bot.command("sessions", sessionsCommand);
459
496
  bot.command("new", newCommand);
460
497
  bot.command("abort", abortCommand);
498
+ bot.command("task", taskCommand);
499
+ bot.command("tasklist", taskListCommand);
461
500
  bot.command("rename", renameCommand);
462
501
  bot.command("commands", commandsCommand);
463
502
  bot.on("message:text", unknownCommandMiddleware);
@@ -478,9 +517,11 @@ export function createBot() {
478
517
  const handledModel = await handleModelSelect(ctx);
479
518
  const handledVariant = await handleVariantSelect(ctx);
480
519
  const handledCompactConfirm = await handleCompactConfirm(ctx);
520
+ const handledTask = await handleTaskCallback(ctx);
521
+ const handledTaskList = await handleTaskListCallback(ctx);
481
522
  const handledRenameCancel = await handleRenameCancel(ctx);
482
523
  const handledCommands = await handleCommandsCallback(ctx, { bot, ensureEventSubscription });
483
- logger.debug(`[Bot] Callback handled: inlineCancel=${handledInlineCancel}, session=${handledSession}, project=${handledProject}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, model=${handledModel}, variant=${handledVariant}, compactConfirm=${handledCompactConfirm}, rename=${handledRenameCancel}, commands=${handledCommands}`);
524
+ logger.debug(`[Bot] Callback handled: inlineCancel=${handledInlineCancel}, session=${handledSession}, project=${handledProject}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, model=${handledModel}, variant=${handledVariant}, compactConfirm=${handledCompactConfirm}, task=${handledTask}, taskList=${handledTaskList}, rename=${handledRenameCancel}, commands=${handledCommands}`);
484
525
  if (!handledInlineCancel &&
485
526
  !handledSession &&
486
527
  !handledProject &&
@@ -490,6 +531,8 @@ export function createBot() {
490
531
  !handledModel &&
491
532
  !handledVariant &&
492
533
  !handledCompactConfirm &&
534
+ !handledTask &&
535
+ !handledTaskList &&
493
536
  !handledRenameCancel &&
494
537
  !handledCommands) {
495
538
  logger.debug("Unknown callback query:", ctx.callbackQuery?.data);
@@ -677,6 +720,10 @@ export function createBot() {
677
720
  await handleQuestionTextAnswer(ctx);
678
721
  return;
679
722
  }
723
+ const handledTask = await handleTaskTextInput(ctx);
724
+ if (handledTask) {
725
+ return;
726
+ }
680
727
  const handledRename = await handleRenameTextAnswer(ctx);
681
728
  if (handledRename) {
682
729
  return;
@@ -46,6 +46,17 @@ function getInteractionBlockedMessage(reason, interactionKind) {
46
46
  return t("rename.blocked.expected_name");
47
47
  }
48
48
  }
49
+ if (interactionKind === "task") {
50
+ switch (reason) {
51
+ case "command_not_allowed":
52
+ return t("task.blocked.command_not_allowed");
53
+ case "expected_callback":
54
+ case "expected_command":
55
+ case "expected_text":
56
+ default:
57
+ return t("task.blocked.expected_input");
58
+ }
59
+ }
49
60
  switch (reason) {
50
61
  case "expired":
51
62
  return t("interaction.blocked.expired");
package/dist/config.js CHANGED
@@ -85,6 +85,7 @@ export const config = {
85
85
  bot: {
86
86
  sessionsListLimit: getOptionalPositiveIntEnvVar("SESSIONS_LIST_LIMIT", 10),
87
87
  projectsListLimit: getOptionalPositiveIntEnvVar("PROJECTS_LIST_LIMIT", 10),
88
+ taskLimit: getOptionalPositiveIntEnvVar("TASK_LIMIT", 10),
88
89
  locale: getOptionalLocaleEnvVar("BOT_LOCALE", "en"),
89
90
  serviceMessagesIntervalSec: getOptionalNonNegativeIntEnvVarFromKeys(["SERVICE_MESSAGES_INTERVAL_SEC", "TOOL_MESSAGES_INTERVAL_SEC"], 5),
90
91
  hideThinkingMessages: getOptionalBooleanEnvVar("HIDE_THINKING_MESSAGES", false),
package/dist/i18n/de.js CHANGED
@@ -4,6 +4,8 @@ export const de = {
4
4
  "cmd.description.stop": "Aktuelle Aktion stoppen",
5
5
  "cmd.description.sessions": "Sitzungen auflisten",
6
6
  "cmd.description.projects": "Projekte auflisten",
7
+ "cmd.description.task": "Geplante Aufgabe erstellen",
8
+ "cmd.description.tasklist": "Geplante Aufgaben anzeigen",
7
9
  "cmd.description.commands": "Benutzerdefinierte Befehle",
8
10
  "cmd.description.opencode_start": "OpenCode-Server starten",
9
11
  "cmd.description.opencode_stop": "OpenCode-Server stoppen",
@@ -30,7 +32,7 @@ export const de = {
30
32
  "inline.cancelled_callback": "Abgebrochen",
31
33
  "common.unknown": "unbekannt",
32
34
  "common.unknown_error": "unbekannter Fehler",
33
- "start.welcome": "👋 Willkommen beim OpenCode Telegram Bot!\n\nNutze Befehle:\n/projects — Projekt auswählen\n/sessions — Sitzungsliste\n/new — neue Sitzung\n/status — Status\n/help — Hilfe\n\nNutze die unteren Buttons, um Modus, Modell und Variante zu wählen.",
35
+ "start.welcome": "👋 Willkommen beim OpenCode Telegram Bot!\n\nNutze Befehle:\n/projects — Projekt auswählen\n/sessions — Sitzungsliste\n/new — neue Sitzung\n/task — geplante Aufgabe\n/tasklist — geplante Aufgaben\n/status — Status\n/help — Hilfe\n\nNutze die unteren Buttons, um Modus, Modell und Variante zu wählen.",
34
36
  "help.keyboard_hint": "💡 Nutze die unteren Buttons für Modus, Modell, Variante und Kontextaktionen.",
35
37
  "help.text": "📖 **Hilfe**\n\n/status - Serverstatus prüfen\n/sessions - Sitzungsliste\n/new - Neue Sitzung erstellen\n/help - Hilfe",
36
38
  "bot.thinking": "💭 Denke...",
@@ -214,6 +216,7 @@ export const de = {
214
216
  "pinned.line.project": "Projekt: {project}",
215
217
  "pinned.line.model": "Modell: {model}",
216
218
  "pinned.line.context": "Kontext: {used} / {limit} ({percent}%)",
219
+ "pinned.line.cost": "Kosten: {cost} ausgegeben",
217
220
  "pinned.files.title": "Dateien ({count}):",
218
221
  "pinned.files.item": " {path}{diff}",
219
222
  "pinned.files.more": " ... und {count} mehr",
@@ -247,6 +250,41 @@ export const de = {
247
250
  "rename.blocked.expected_name": "⚠️ Sende den neuen Sitzungsnamen als Text oder tippe in der Umbenennen-Nachricht auf Abbrechen.",
248
251
  "rename.blocked.command_not_allowed": "⚠️ Dieser Befehl ist nicht verfügbar, solange beim Umbenennen auf einen neuen Namen gewartet wird.",
249
252
  "rename.button.cancel": "❌ Abbrechen",
253
+ "task.prompt.schedule": "⏰ Sende den Zeitplan der Aufgabe in natürlicher Sprache.\n\nBeispiele:\n- alle 5 Minuten\n- jeden Tag um 17:00\n- morgen um 12:00",
254
+ "task.schedule_empty": "⚠️ Der Zeitplan darf nicht leer sein.",
255
+ "task.parse.in_progress": "⏳ Zeitplan wird verarbeitet...",
256
+ "task.parse_error": "🔴 Zeitplan konnte nicht erkannt werden.\n\n{message}\n\nSende den Zeitraum bitte noch einmal klarer formuliert.",
257
+ "task.schedule_preview": "✅ Zeitplan erkannt\n\nVerstanden als: {summary}\n{cronLine}Zeitzone: {timezone}\nTyp: {kind}\nNächster Lauf: {nextRunAt}",
258
+ "task.schedule_preview.cron": "Cron: {cron}",
259
+ "task.prompt.body": "📝 Sende jetzt, was der Bot nach Zeitplan tun soll.",
260
+ "task.prompt_empty": "⚠️ Der Aufgabentext darf nicht leer sein.",
261
+ "task.created": "✅ Geplante Aufgabe erstellt\n\nAufgabe: {description}\nProjekt: {project}\nModell: {model}\nZeitplan: {schedule}\n{cronLine}Nächster Lauf: {nextRunAt}",
262
+ "task.created.cron": "Cron: {cron}",
263
+ "task.button.retry_schedule": "🔁 Zeitplan neu eingeben",
264
+ "task.button.cancel": "❌ Abbrechen",
265
+ "task.retry_schedule_callback": "Zeitplaneingabe wird zurückgesetzt...",
266
+ "task.cancel_callback": "Abbruch...",
267
+ "task.cancelled": "❌ Erstellung der geplanten Aufgabe abgebrochen.",
268
+ "task.inactive_callback": "Dieser Ablauf für geplante Aufgaben ist nicht mehr aktiv",
269
+ "task.inactive": "⚠️ Die Erstellung geplanter Aufgaben ist nicht aktiv. Starte /task erneut.",
270
+ "task.blocked.expected_input": "⚠️ Schließe zuerst die aktuelle geplante Aufgabe ab: Sende Text oder nutze die Schaltfläche in der Zeitplan-Nachricht.",
271
+ "task.blocked.command_not_allowed": "⚠️ Dieser Befehl ist nicht verfügbar, solange die Erstellung einer geplanten Aufgabe aktiv ist.",
272
+ "task.limit_reached": "⚠️ Aufgabenlimit erreicht ({limit}). Lösche zuerst eine bestehende geplante Aufgabe.",
273
+ "task.schedule_too_frequent": "Der wiederkehrende Zeitplan ist zu häufig. Das minimale erlaubte Intervall ist einmal alle 5 Minuten.",
274
+ "task.kind.cron": "wiederkehrend",
275
+ "task.kind.once": "einmalig",
276
+ "task.run.success": "⏰ Geplante Aufgabe abgeschlossen: {description}\n\n{result}",
277
+ "task.run.error": "🔴 Geplante Aufgabe fehlgeschlagen: {description}\n\nFehler: {error}",
278
+ "tasklist.empty": "📭 Noch keine geplanten Aufgaben.",
279
+ "tasklist.select": "Wähle eine geplante Aufgabe:",
280
+ "tasklist.details": "⏰ Geplante Aufgabe\n\nAufgabe: {prompt}\nProjekt: {project}\nZeitplan: {schedule}\n{cronLine}Zeitzone: {timezone}\nNächster Lauf: {nextRunAt}\nLetzter Lauf: {lastRunAt}\nAnzahl Läufe: {runCount}",
281
+ "tasklist.details.cron": "Cron: {cron}",
282
+ "tasklist.button.delete": "🗑 Löschen",
283
+ "tasklist.button.cancel": "❌ Abbrechen",
284
+ "tasklist.deleted_callback": "Gelöscht",
285
+ "tasklist.cancelled_callback": "Abgebrochen",
286
+ "tasklist.inactive_callback": "Dieses Menü für geplante Aufgaben ist inaktiv",
287
+ "tasklist.load_error": "🔴 Geplante Aufgaben konnten nicht geladen werden.",
250
288
  "commands.select": "Wähle einen OpenCode-Befehl:",
251
289
  "commands.empty": "📭 Für dieses Projekt sind keine OpenCode-Befehle verfügbar.",
252
290
  "commands.fetch_error": "🔴 OpenCode-Befehle konnten nicht geladen werden.",
package/dist/i18n/en.js CHANGED
@@ -4,6 +4,8 @@ export const en = {
4
4
  "cmd.description.stop": "Stop current action",
5
5
  "cmd.description.sessions": "List sessions",
6
6
  "cmd.description.projects": "List projects",
7
+ "cmd.description.task": "Create a scheduled task",
8
+ "cmd.description.tasklist": "List scheduled tasks",
7
9
  "cmd.description.commands": "Custom commands",
8
10
  "cmd.description.opencode_start": "Start OpenCode server",
9
11
  "cmd.description.opencode_stop": "Stop OpenCode server",
@@ -30,7 +32,7 @@ export const en = {
30
32
  "inline.cancelled_callback": "Cancelled",
31
33
  "common.unknown": "unknown",
32
34
  "common.unknown_error": "unknown error",
33
- "start.welcome": "👋 Welcome to OpenCode Telegram Bot!\n\nUse commands:\n/projects — select project\n/sessions — session list\n/new — new session\n/status — status\n/help — help\n\nUse the bottom buttons to select agent mode, model, and variant.",
35
+ "start.welcome": "👋 Welcome to OpenCode Telegram Bot!\n\nUse commands:\n/projects — select project\n/sessions — session list\n/new — new session\n/task — scheduled task\n/tasklist — scheduled tasks\n/status — status\n/help — help\n\nUse the bottom buttons to select agent mode, model, and variant.",
34
36
  "help.keyboard_hint": "💡 Use the bottom keyboard buttons for agent mode, model, variant, and context actions.",
35
37
  "help.text": "📖 **Help**\n\n/status - Check server status\n/sessions - Session list\n/new - Create new session\n/help - Help",
36
38
  "bot.thinking": "💭 Thinking...",
@@ -214,6 +216,7 @@ export const en = {
214
216
  "pinned.line.project": "Project: {project}",
215
217
  "pinned.line.model": "Model: {model}",
216
218
  "pinned.line.context": "Context: {used} / {limit} ({percent}%)",
219
+ "pinned.line.cost": "Cost: {cost} spent",
217
220
  "pinned.files.title": "Files ({count}):",
218
221
  "pinned.files.item": " {path}{diff}",
219
222
  "pinned.files.more": " ... and {count} more",
@@ -247,6 +250,41 @@ export const en = {
247
250
  "rename.blocked.expected_name": "⚠️ Enter a new session name as text or tap Cancel in rename message.",
248
251
  "rename.blocked.command_not_allowed": "⚠️ This command is not available while rename is waiting for a new name.",
249
252
  "rename.button.cancel": "❌ Cancel",
253
+ "task.prompt.schedule": "⏰ Send the task schedule in natural language.\n\nExamples:\n- every 5 minutes\n- every day at 17:00\n- tomorrow at 12:00",
254
+ "task.schedule_empty": "⚠️ Schedule cannot be empty.",
255
+ "task.parse.in_progress": "⏳ Parsing schedule...",
256
+ "task.parse_error": "🔴 Failed to parse schedule.\n\n{message}\n\nSend the schedule again in a clearer form.",
257
+ "task.schedule_preview": "✅ Schedule parsed\n\nHow I understood it: {summary}\n{cronLine}Timezone: {timezone}\nType: {kind}\nNext run: {nextRunAt}",
258
+ "task.schedule_preview.cron": "Cron: {cron}",
259
+ "task.prompt.body": "📝 Now send what the bot should do on schedule.",
260
+ "task.prompt_empty": "⚠️ Task text cannot be empty.",
261
+ "task.created": "✅ Scheduled task created\n\nTask: {description}\nProject: {project}\nModel: {model}\nSchedule: {schedule}\n{cronLine}Next run: {nextRunAt}",
262
+ "task.created.cron": "Cron: {cron}",
263
+ "task.button.retry_schedule": "🔁 Re-enter schedule",
264
+ "task.button.cancel": "❌ Cancel",
265
+ "task.retry_schedule_callback": "Re-entering schedule...",
266
+ "task.cancel_callback": "Cancelling...",
267
+ "task.cancelled": "❌ Scheduled task creation cancelled.",
268
+ "task.inactive_callback": "This scheduled task flow is inactive",
269
+ "task.inactive": "⚠️ Scheduled task creation is not active. Run /task again.",
270
+ "task.blocked.expected_input": "⚠️ Finish the current scheduled task setup first by sending text or using the button in the schedule message.",
271
+ "task.blocked.command_not_allowed": "⚠️ This command is not available while scheduled task creation is active.",
272
+ "task.limit_reached": "⚠️ Task limit reached ({limit}). Delete an existing scheduled task first.",
273
+ "task.schedule_too_frequent": "Recurring schedule is too frequent. The minimum allowed interval is once every 5 minutes.",
274
+ "task.kind.cron": "recurring",
275
+ "task.kind.once": "one-time",
276
+ "task.run.success": "⏰ Scheduled task completed: {description}\n\n{result}",
277
+ "task.run.error": "🔴 Scheduled task failed: {description}\n\nError: {error}",
278
+ "tasklist.empty": "📭 No scheduled tasks yet.",
279
+ "tasklist.select": "Select a scheduled task:",
280
+ "tasklist.details": "⏰ Scheduled task\n\nTask: {prompt}\nProject: {project}\nSchedule: {schedule}\n{cronLine}Timezone: {timezone}\nNext run: {nextRunAt}\nLast run: {lastRunAt}\nRun count: {runCount}",
281
+ "tasklist.details.cron": "Cron: {cron}",
282
+ "tasklist.button.delete": "🗑 Delete",
283
+ "tasklist.button.cancel": "❌ Cancel",
284
+ "tasklist.deleted_callback": "Deleted",
285
+ "tasklist.cancelled_callback": "Cancelled",
286
+ "tasklist.inactive_callback": "This scheduled task menu is inactive",
287
+ "tasklist.load_error": "🔴 Failed to load scheduled tasks.",
250
288
  "commands.select": "Choose an OpenCode command:",
251
289
  "commands.empty": "📭 No OpenCode commands are available for this project.",
252
290
  "commands.fetch_error": "🔴 Failed to load OpenCode commands.",