@grinev/opencode-telegram-bot 0.16.0 → 0.17.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.
- package/.env.example +8 -0
- package/README.md +72 -42
- package/dist/app/start-bot-app.js +76 -8
- package/dist/bot/commands/definitions.js +2 -0
- package/dist/bot/commands/opencode-start.js +27 -24
- package/dist/bot/commands/opencode-stop.js +51 -23
- package/dist/bot/commands/projects.js +30 -6
- package/dist/bot/commands/skills.js +376 -0
- package/dist/bot/commands/status.js +21 -14
- package/dist/bot/commands/worktree.js +196 -0
- package/dist/bot/handlers/inline-menu.js +1 -0
- package/dist/bot/handlers/voice.js +8 -1
- package/dist/bot/index.js +39 -4
- package/dist/cli/args.js +36 -7
- package/dist/cli.js +131 -17
- package/dist/config.js +3 -0
- package/dist/git/worktree.js +158 -0
- package/dist/i18n/de.js +39 -11
- package/dist/i18n/en.js +39 -11
- package/dist/i18n/es.js +39 -11
- package/dist/i18n/fr.js +39 -11
- package/dist/i18n/ru.js +39 -11
- package/dist/i18n/zh.js +39 -11
- package/dist/opencode/process.js +182 -0
- package/dist/pinned/manager.js +28 -61
- package/dist/project/manager.js +47 -32
- package/dist/runtime/bootstrap.js +119 -7
- package/dist/scheduled-task/executor.js +137 -7
- package/dist/scheduled-task/runtime.js +8 -0
- package/dist/service/manager.js +244 -0
- package/dist/service/runtime.js +19 -0
- package/dist/settings/manager.js +9 -12
- package/package.json +1 -1
- package/dist/process/manager.js +0 -273
- /package/dist/{process → service}/types.js +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import { config } from "../../config.js";
|
|
3
|
+
import { getGitWorktreeContext } from "../../git/worktree.js";
|
|
4
|
+
import { clearAllInteractionState } from "../../interaction/cleanup.js";
|
|
5
|
+
import { getProjectByWorktree } from "../../project/manager.js";
|
|
6
|
+
import { upsertSessionDirectory } from "../../session/cache-manager.js";
|
|
7
|
+
import { getCurrentProject } from "../../settings/manager.js";
|
|
8
|
+
import { logger } from "../../utils/logger.js";
|
|
9
|
+
import { t } from "../../i18n/index.js";
|
|
10
|
+
import { appendInlineMenuCancelButton, ensureActiveInlineMenu, replyWithInlineMenu, } from "../handlers/inline-menu.js";
|
|
11
|
+
import { switchToProject } from "../utils/switch-project.js";
|
|
12
|
+
import { isForegroundBusy, replyBusyBlocked } from "../utils/busy-guard.js";
|
|
13
|
+
import { buildProjectButtonLabel, calculateProjectsPaginationRange } from "./projects.js";
|
|
14
|
+
const MAX_INLINE_BUTTON_LABEL_LENGTH = 64;
|
|
15
|
+
const WORKTREE_CALLBACK_PREFIX = "worktree:";
|
|
16
|
+
const WORKTREE_PAGE_CALLBACK_PREFIX = "worktree:page:";
|
|
17
|
+
function formatWorktreeButtonLabel(label, isActive) {
|
|
18
|
+
const prefix = isActive ? "✅ " : "";
|
|
19
|
+
const availableLength = MAX_INLINE_BUTTON_LABEL_LENGTH - prefix.length;
|
|
20
|
+
if (label.length <= availableLength) {
|
|
21
|
+
return `${prefix}${label}`;
|
|
22
|
+
}
|
|
23
|
+
return `${prefix}${label.slice(0, Math.max(0, availableLength - 3))}...`;
|
|
24
|
+
}
|
|
25
|
+
function buildWorktreeButtonLabel(index, entry) {
|
|
26
|
+
return buildProjectButtonLabel(index, entry.path);
|
|
27
|
+
}
|
|
28
|
+
function parseWorktreePageCallback(data) {
|
|
29
|
+
if (!data.startsWith(WORKTREE_PAGE_CALLBACK_PREFIX)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const rawPage = data.slice(WORKTREE_PAGE_CALLBACK_PREFIX.length);
|
|
33
|
+
if (!/^\d+$/.test(rawPage)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return Number.parseInt(rawPage, 10);
|
|
37
|
+
}
|
|
38
|
+
function parseWorktreeIndexCallback(data) {
|
|
39
|
+
if (!data.startsWith(WORKTREE_CALLBACK_PREFIX) ||
|
|
40
|
+
data.startsWith(WORKTREE_PAGE_CALLBACK_PREFIX)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const rawIndex = data.slice(WORKTREE_CALLBACK_PREFIX.length);
|
|
44
|
+
if (!/^\d+$/.test(rawIndex)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return Number.parseInt(rawIndex, 10);
|
|
48
|
+
}
|
|
49
|
+
function buildWorktreeMenuText(page, totalPages) {
|
|
50
|
+
const baseText = t("worktree.select_with_current");
|
|
51
|
+
if (totalPages <= 1) {
|
|
52
|
+
return baseText;
|
|
53
|
+
}
|
|
54
|
+
return `${baseText}\n\n${t("projects.page_indicator", {
|
|
55
|
+
current: String(page + 1),
|
|
56
|
+
total: String(totalPages),
|
|
57
|
+
})}`;
|
|
58
|
+
}
|
|
59
|
+
function buildWorktreeKeyboard(worktrees, page) {
|
|
60
|
+
const keyboard = new InlineKeyboard();
|
|
61
|
+
const pageSize = config.bot.projectsListLimit;
|
|
62
|
+
const { page: normalizedPage, totalPages, startIndex, endIndex, } = calculateProjectsPaginationRange(worktrees.length, page, pageSize);
|
|
63
|
+
worktrees.slice(startIndex, endIndex).forEach((entry, index) => {
|
|
64
|
+
const label = buildWorktreeButtonLabel(startIndex + index, entry);
|
|
65
|
+
keyboard
|
|
66
|
+
.text(formatWorktreeButtonLabel(label, entry.isCurrent), `${WORKTREE_CALLBACK_PREFIX}${startIndex + index}`)
|
|
67
|
+
.row();
|
|
68
|
+
});
|
|
69
|
+
if (totalPages > 1) {
|
|
70
|
+
if (normalizedPage > 0) {
|
|
71
|
+
keyboard.text(t("projects.prev_page"), `${WORKTREE_PAGE_CALLBACK_PREFIX}${normalizedPage - 1}`);
|
|
72
|
+
}
|
|
73
|
+
if (normalizedPage < totalPages - 1) {
|
|
74
|
+
keyboard.text(t("projects.next_page"), `${WORKTREE_PAGE_CALLBACK_PREFIX}${normalizedPage + 1}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return keyboard;
|
|
78
|
+
}
|
|
79
|
+
function buildWorktreeMenuView(worktrees, page) {
|
|
80
|
+
const { page: normalizedPage, totalPages } = calculateProjectsPaginationRange(worktrees.length, page, config.bot.projectsListLimit);
|
|
81
|
+
return {
|
|
82
|
+
text: buildWorktreeMenuText(normalizedPage, totalPages),
|
|
83
|
+
keyboard: buildWorktreeKeyboard(worktrees, normalizedPage),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function loadCurrentWorktreeContext() {
|
|
87
|
+
const currentProject = getCurrentProject();
|
|
88
|
+
if (!currentProject) {
|
|
89
|
+
return { currentProject: null, context: null };
|
|
90
|
+
}
|
|
91
|
+
const context = await getGitWorktreeContext(currentProject.worktree);
|
|
92
|
+
return { currentProject, context };
|
|
93
|
+
}
|
|
94
|
+
export async function worktreeCommand(ctx) {
|
|
95
|
+
try {
|
|
96
|
+
if (isForegroundBusy()) {
|
|
97
|
+
await replyBusyBlocked(ctx);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const { currentProject, context } = await loadCurrentWorktreeContext();
|
|
101
|
+
if (!currentProject) {
|
|
102
|
+
await ctx.reply(t("worktree.project_not_selected"));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!context) {
|
|
106
|
+
await ctx.reply(t("worktree.not_git_repo"));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (context.worktrees.length === 0) {
|
|
110
|
+
await ctx.reply(t("worktree.empty"));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const { text, keyboard } = buildWorktreeMenuView(context.worktrees, 0);
|
|
114
|
+
await replyWithInlineMenu(ctx, {
|
|
115
|
+
menuKind: "worktree",
|
|
116
|
+
text,
|
|
117
|
+
keyboard,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
logger.error("[Bot] Error loading worktrees:", error);
|
|
122
|
+
await ctx.reply(t("worktree.fetch_error"));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export async function handleWorktreeCallback(ctx) {
|
|
126
|
+
const callbackQuery = ctx.callbackQuery;
|
|
127
|
+
if (!callbackQuery?.data || !callbackQuery.data.startsWith(WORKTREE_CALLBACK_PREFIX)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
if (isForegroundBusy()) {
|
|
131
|
+
await replyBusyBlocked(ctx);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
const page = parseWorktreePageCallback(callbackQuery.data);
|
|
135
|
+
const index = parseWorktreeIndexCallback(callbackQuery.data);
|
|
136
|
+
const isActiveMenu = await ensureActiveInlineMenu(ctx, "worktree");
|
|
137
|
+
if (!isActiveMenu) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const { currentProject, context } = await loadCurrentWorktreeContext();
|
|
142
|
+
if (!currentProject) {
|
|
143
|
+
clearAllInteractionState("worktree_project_missing");
|
|
144
|
+
await ctx.answerCallbackQuery();
|
|
145
|
+
await ctx.reply(t("worktree.project_not_selected"));
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
if (!context) {
|
|
149
|
+
clearAllInteractionState("worktree_git_context_missing");
|
|
150
|
+
await ctx.answerCallbackQuery({ text: t("worktree.not_git_repo_callback") });
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
if (page !== null) {
|
|
154
|
+
if (context.worktrees.length === 0) {
|
|
155
|
+
await ctx.answerCallbackQuery({ text: t("worktree.page_empty_callback") });
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
const { text, keyboard } = buildWorktreeMenuView(context.worktrees, page);
|
|
159
|
+
await ctx.answerCallbackQuery();
|
|
160
|
+
await ctx.editMessageText(text, {
|
|
161
|
+
reply_markup: appendInlineMenuCancelButton(keyboard, "worktree"),
|
|
162
|
+
});
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (index === null) {
|
|
166
|
+
await ctx.answerCallbackQuery({ text: t("callback.processing_error") });
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
const selectedWorktree = context.worktrees[index];
|
|
170
|
+
if (!selectedWorktree) {
|
|
171
|
+
await ctx.answerCallbackQuery({ text: t("worktree.selection_missing_callback") });
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
if (selectedWorktree.isCurrent) {
|
|
175
|
+
await ctx.answerCallbackQuery({ text: t("worktree.already_selected_callback") });
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
logger.info(`[Bot] Worktree selected: ${selectedWorktree.path}`);
|
|
179
|
+
await upsertSessionDirectory(selectedWorktree.path, Date.now());
|
|
180
|
+
const projectInfo = await getProjectByWorktree(selectedWorktree.path);
|
|
181
|
+
const replyKeyboard = await switchToProject(ctx, { ...projectInfo, name: selectedWorktree.path }, "worktree_switched");
|
|
182
|
+
await ctx.answerCallbackQuery();
|
|
183
|
+
await ctx.reply(t("worktree.selected", { worktree: selectedWorktree.path }), {
|
|
184
|
+
reply_markup: replyKeyboard,
|
|
185
|
+
});
|
|
186
|
+
await ctx.deleteMessage();
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
logger.error("[Bot] Error handling worktree callback:", error);
|
|
191
|
+
clearAllInteractionState("worktree_select_error");
|
|
192
|
+
await ctx.answerCallbackQuery({ text: t("callback.processing_error") });
|
|
193
|
+
await ctx.reply(t("worktree.select_error"));
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -156,8 +156,15 @@ export async function handleVoiceMessage(ctx, deps) {
|
|
|
156
156
|
logger.warn("[Voice] Failed to edit status message with recognized text:", editError);
|
|
157
157
|
}
|
|
158
158
|
logger.info(`[Voice] Transcribed audio: ${recognizedText.length} chars`);
|
|
159
|
+
let textForLLM = recognizedText;
|
|
160
|
+
const notePrompt = config.stt.notePrompt.trim();
|
|
161
|
+
if (notePrompt && notePrompt.toLowerCase() !== "false" && notePrompt !== "0") {
|
|
162
|
+
const llmNote = `[Note: ${notePrompt}]`;
|
|
163
|
+
logger.debug(`[Voice] Added STT note to LLM prompt: ${llmNote}`);
|
|
164
|
+
textForLLM = `${llmNote}\n${recognizedText}`;
|
|
165
|
+
}
|
|
159
166
|
// Process the recognized text as a prompt
|
|
160
|
-
await processPrompt(ctx,
|
|
167
|
+
await processPrompt(ctx, textForLLM, deps);
|
|
161
168
|
}
|
|
162
169
|
catch (err) {
|
|
163
170
|
const errorMessage = err instanceof Error ? err.message : "unknown error";
|
package/dist/bot/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { AGENT_MODE_BUTTON_TEXT_PATTERN, MODEL_BUTTON_TEXT_PATTERN, VARIANT_BUTT
|
|
|
16
16
|
import { sessionsCommand, handleSessionSelect } from "./commands/sessions.js";
|
|
17
17
|
import { newCommand } from "./commands/new.js";
|
|
18
18
|
import { projectsCommand, handleProjectSelect } from "./commands/projects.js";
|
|
19
|
+
import { worktreeCommand, handleWorktreeCallback } from "./commands/worktree.js";
|
|
19
20
|
import { openCommand, handleOpenCallback, clearOpenPathIndex } from "./commands/open.js";
|
|
20
21
|
import { abortCommand } from "./commands/abort.js";
|
|
21
22
|
import { opencodeStartCommand } from "./commands/opencode-start.js";
|
|
@@ -24,6 +25,7 @@ import { renameCommand, handleRenameCancel, handleRenameTextAnswer } from "./com
|
|
|
24
25
|
import { handleTaskCallback, handleTaskTextInput, taskCommand } from "./commands/task.js";
|
|
25
26
|
import { handleTaskListCallback, taskListCommand } from "./commands/tasklist.js";
|
|
26
27
|
import { commandsCommand, handleCommandsCallback, handleCommandTextArguments, } from "./commands/commands.js";
|
|
28
|
+
import { skillsCommand, handleSkillsCallback, handleSkillTextArguments, } from "./commands/skills.js";
|
|
27
29
|
import { ttsCommand } from "./commands/tts.js";
|
|
28
30
|
import { handleQuestionCallback, showCurrentQuestion, handleQuestionTextAnswer, } from "./handlers/question.js";
|
|
29
31
|
import { handlePermissionCallback, showPermissionRequest } from "./handlers/permission.js";
|
|
@@ -36,7 +38,7 @@ import { questionManager } from "../question/manager.js";
|
|
|
36
38
|
import { interactionManager } from "../interaction/manager.js";
|
|
37
39
|
import { clearAllInteractionState } from "../interaction/cleanup.js";
|
|
38
40
|
import { keyboardManager } from "../keyboard/manager.js";
|
|
39
|
-
import { subscribeToEvents } from "../opencode/events.js";
|
|
41
|
+
import { stopEventListening, subscribeToEvents } from "../opencode/events.js";
|
|
40
42
|
import { summaryAggregator } from "../summary/aggregator.js";
|
|
41
43
|
import { formatToolInfo } from "../summary/formatter.js";
|
|
42
44
|
import { renderSubagentCards } from "../summary/subagent-formatter.js";
|
|
@@ -68,6 +70,7 @@ import { prepareAssistantFinalStreamingPayload, prepareAssistantStreamingPayload
|
|
|
68
70
|
let botInstance = null;
|
|
69
71
|
let chatIdInstance = null;
|
|
70
72
|
let commandsInitialized = false;
|
|
73
|
+
let heartbeatTimer = null;
|
|
71
74
|
const TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH = 1024;
|
|
72
75
|
const RESPONSE_STREAM_THROTTLE_MS = config.bot.responseStreamThrottleMs;
|
|
73
76
|
const RESPONSE_STREAM_TEXT_LIMIT = 3800;
|
|
@@ -442,6 +445,9 @@ async function ensureEventSubscription(directory) {
|
|
|
442
445
|
if (!currentSession || currentSession.id !== fileInfo.sessionId) {
|
|
443
446
|
return;
|
|
444
447
|
}
|
|
448
|
+
if (config.bot.hideToolFileMessages) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
445
451
|
try {
|
|
446
452
|
await toolCallStreamer.breakSession(fileInfo.sessionId, "tool_file_boundary");
|
|
447
453
|
const toolMessage = formatToolInfo(fileInfo);
|
|
@@ -718,6 +724,10 @@ export function createBot() {
|
|
|
718
724
|
clearAllInteractionState("bot_startup");
|
|
719
725
|
sessionCompletionTasks.clear();
|
|
720
726
|
assistantRunState.clearAll("bot_startup");
|
|
727
|
+
if (heartbeatTimer) {
|
|
728
|
+
clearInterval(heartbeatTimer);
|
|
729
|
+
heartbeatTimer = null;
|
|
730
|
+
}
|
|
721
731
|
const botOptions = {};
|
|
722
732
|
if (config.telegram.proxyUrl) {
|
|
723
733
|
const proxyUrl = config.telegram.proxyUrl;
|
|
@@ -740,7 +750,7 @@ export function createBot() {
|
|
|
740
750
|
const bot = new Bot(config.telegram.token, botOptions);
|
|
741
751
|
// Heartbeat for diagnostics: verify the event loop is not blocked
|
|
742
752
|
let heartbeatCounter = 0;
|
|
743
|
-
setInterval(() => {
|
|
753
|
+
heartbeatTimer = setInterval(() => {
|
|
744
754
|
heartbeatCounter++;
|
|
745
755
|
if (heartbeatCounter % 6 === 0) {
|
|
746
756
|
// Log every 30 seconds (5 sec * 6)
|
|
@@ -793,6 +803,7 @@ export function createBot() {
|
|
|
793
803
|
bot.command("opencode_start", opencodeStartCommand);
|
|
794
804
|
bot.command("opencode_stop", opencodeStopCommand);
|
|
795
805
|
bot.command("projects", projectsCommand);
|
|
806
|
+
bot.command("worktree", worktreeCommand);
|
|
796
807
|
bot.command("open", openCommand);
|
|
797
808
|
bot.command("sessions", sessionsCommand);
|
|
798
809
|
bot.command("new", newCommand);
|
|
@@ -801,6 +812,7 @@ export function createBot() {
|
|
|
801
812
|
bot.command("tasklist", taskListCommand);
|
|
802
813
|
bot.command("rename", renameCommand);
|
|
803
814
|
bot.command("commands", commandsCommand);
|
|
815
|
+
bot.command("skills", skillsCommand);
|
|
804
816
|
bot.on("message:text", unknownCommandMiddleware);
|
|
805
817
|
bot.on("callback_query:data", async (ctx) => {
|
|
806
818
|
logger.debug(`[Bot] Received callback_query:data: ${ctx.callbackQuery?.data}`);
|
|
@@ -817,6 +829,7 @@ export function createBot() {
|
|
|
817
829
|
}
|
|
818
830
|
const handledSession = await handleSessionSelect(ctx);
|
|
819
831
|
const handledProject = await handleProjectSelect(ctx);
|
|
832
|
+
const handledWorktree = await handleWorktreeCallback(ctx);
|
|
820
833
|
const handledOpen = await handleOpenCallback(ctx);
|
|
821
834
|
const handledQuestion = await handleQuestionCallback(ctx);
|
|
822
835
|
const handledPermission = await handlePermissionCallback(ctx);
|
|
@@ -828,10 +841,12 @@ export function createBot() {
|
|
|
828
841
|
const handledTaskList = await handleTaskListCallback(ctx);
|
|
829
842
|
const handledRenameCancel = await handleRenameCancel(ctx);
|
|
830
843
|
const handledCommands = await handleCommandsCallback(ctx, { bot, ensureEventSubscription });
|
|
831
|
-
|
|
844
|
+
const handledSkills = await handleSkillsCallback(ctx, { bot, ensureEventSubscription });
|
|
845
|
+
logger.debug(`[Bot] Callback handled: inlineCancel=${handledInlineCancel}, session=${handledSession}, project=${handledProject}, worktree=${handledWorktree}, open=${handledOpen}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, model=${handledModel}, variant=${handledVariant}, compactConfirm=${handledCompactConfirm}, task=${handledTask}, taskList=${handledTaskList}, rename=${handledRenameCancel}, commands=${handledCommands}, skills=${handledSkills}`);
|
|
832
846
|
if (!handledInlineCancel &&
|
|
833
847
|
!handledSession &&
|
|
834
848
|
!handledProject &&
|
|
849
|
+
!handledWorktree &&
|
|
835
850
|
!handledOpen &&
|
|
836
851
|
!handledQuestion &&
|
|
837
852
|
!handledPermission &&
|
|
@@ -842,7 +857,8 @@ export function createBot() {
|
|
|
842
857
|
!handledTask &&
|
|
843
858
|
!handledTaskList &&
|
|
844
859
|
!handledRenameCancel &&
|
|
845
|
-
!handledCommands
|
|
860
|
+
!handledCommands &&
|
|
861
|
+
!handledSkills) {
|
|
846
862
|
logger.debug("Unknown callback query:", ctx.callbackQuery?.data);
|
|
847
863
|
await ctx.answerCallbackQuery({ text: t("callback.unknown_command") });
|
|
848
864
|
}
|
|
@@ -1041,6 +1057,10 @@ export function createBot() {
|
|
|
1041
1057
|
if (handledCommandArgs) {
|
|
1042
1058
|
return;
|
|
1043
1059
|
}
|
|
1060
|
+
const handledSkillArgs = await handleSkillTextArguments(ctx, promptDeps);
|
|
1061
|
+
if (handledSkillArgs) {
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1044
1064
|
await processUserPrompt(ctx, text, promptDeps);
|
|
1045
1065
|
logger.debug("[Bot] message:text handler completed (prompt sent in background)");
|
|
1046
1066
|
});
|
|
@@ -1053,3 +1073,18 @@ export function createBot() {
|
|
|
1053
1073
|
});
|
|
1054
1074
|
return bot;
|
|
1055
1075
|
}
|
|
1076
|
+
export function cleanupBotRuntime(reason) {
|
|
1077
|
+
stopEventListening();
|
|
1078
|
+
summaryAggregator.clear();
|
|
1079
|
+
responseStreamer.clearAll(reason);
|
|
1080
|
+
toolCallStreamer.clearAll(reason);
|
|
1081
|
+
toolMessageBatcher.clearAll(reason);
|
|
1082
|
+
sessionCompletionTasks.clear();
|
|
1083
|
+
assistantRunState.clearAll(reason);
|
|
1084
|
+
if (heartbeatTimer) {
|
|
1085
|
+
clearInterval(heartbeatTimer);
|
|
1086
|
+
heartbeatTimer = null;
|
|
1087
|
+
}
|
|
1088
|
+
botInstance = null;
|
|
1089
|
+
chatIdInstance = null;
|
|
1090
|
+
}
|
package/dist/cli/args.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
const CLI_MESSAGES = {
|
|
2
|
+
unknownCommand: (value) => `Unknown command: ${value}`,
|
|
3
|
+
modeRequiresValue: "Option --mode requires a value: sources|installed",
|
|
4
|
+
invalidMode: (value) => `Invalid mode value: ${value}. Expected sources|installed`,
|
|
5
|
+
unknownOption: (value) => `Unknown option: ${value}`,
|
|
6
|
+
modeOnlyStart: "Option --mode is supported only for the start command",
|
|
7
|
+
daemonOnlyStart: "Option --daemon is supported only for the start command",
|
|
8
|
+
};
|
|
2
9
|
const SUPPORTED_COMMANDS = ["start", "status", "stop", "config"];
|
|
3
10
|
function isCliCommand(value) {
|
|
4
11
|
return SUPPORTED_COMMANDS.includes(value);
|
|
@@ -16,6 +23,7 @@ export function parseCliArgs(argv) {
|
|
|
16
23
|
const args = [...argv];
|
|
17
24
|
let command = "start";
|
|
18
25
|
let mode;
|
|
26
|
+
let daemon = false;
|
|
19
27
|
let showHelp = false;
|
|
20
28
|
let currentIndex = 0;
|
|
21
29
|
const firstArg = args[0];
|
|
@@ -23,8 +31,9 @@ export function parseCliArgs(argv) {
|
|
|
23
31
|
if (!isCliCommand(firstArg)) {
|
|
24
32
|
return {
|
|
25
33
|
command,
|
|
34
|
+
daemon,
|
|
26
35
|
showHelp: true,
|
|
27
|
-
error:
|
|
36
|
+
error: CLI_MESSAGES.unknownCommand(firstArg),
|
|
28
37
|
};
|
|
29
38
|
}
|
|
30
39
|
command = firstArg;
|
|
@@ -37,23 +46,30 @@ export function parseCliArgs(argv) {
|
|
|
37
46
|
currentIndex += 1;
|
|
38
47
|
continue;
|
|
39
48
|
}
|
|
49
|
+
if (token === "--daemon") {
|
|
50
|
+
daemon = true;
|
|
51
|
+
currentIndex += 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
40
54
|
if (token === "--mode") {
|
|
41
55
|
const modeValue = args[currentIndex + 1];
|
|
42
56
|
if (!modeValue || modeValue.startsWith("-")) {
|
|
43
57
|
return {
|
|
44
58
|
command,
|
|
59
|
+
daemon,
|
|
45
60
|
mode,
|
|
46
61
|
showHelp: true,
|
|
47
|
-
error:
|
|
62
|
+
error: CLI_MESSAGES.modeRequiresValue,
|
|
48
63
|
};
|
|
49
64
|
}
|
|
50
65
|
const parsedMode = normalizeMode(modeValue);
|
|
51
66
|
if (!parsedMode) {
|
|
52
67
|
return {
|
|
53
68
|
command,
|
|
69
|
+
daemon,
|
|
54
70
|
mode,
|
|
55
71
|
showHelp: true,
|
|
56
|
-
error:
|
|
72
|
+
error: CLI_MESSAGES.invalidMode(modeValue),
|
|
57
73
|
};
|
|
58
74
|
}
|
|
59
75
|
mode = parsedMode;
|
|
@@ -66,9 +82,10 @@ export function parseCliArgs(argv) {
|
|
|
66
82
|
if (!parsedMode) {
|
|
67
83
|
return {
|
|
68
84
|
command,
|
|
85
|
+
daemon,
|
|
69
86
|
mode,
|
|
70
87
|
showHelp: true,
|
|
71
|
-
error:
|
|
88
|
+
error: CLI_MESSAGES.invalidMode(modeValue),
|
|
72
89
|
};
|
|
73
90
|
}
|
|
74
91
|
mode = parsedMode;
|
|
@@ -77,21 +94,33 @@ export function parseCliArgs(argv) {
|
|
|
77
94
|
}
|
|
78
95
|
return {
|
|
79
96
|
command,
|
|
97
|
+
daemon,
|
|
80
98
|
mode,
|
|
81
99
|
showHelp: true,
|
|
82
|
-
error:
|
|
100
|
+
error: CLI_MESSAGES.unknownOption(token),
|
|
83
101
|
};
|
|
84
102
|
}
|
|
85
103
|
if (command !== "start" && mode) {
|
|
86
104
|
return {
|
|
87
105
|
command,
|
|
106
|
+
daemon,
|
|
107
|
+
mode,
|
|
108
|
+
showHelp: true,
|
|
109
|
+
error: CLI_MESSAGES.modeOnlyStart,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (command !== "start" && daemon) {
|
|
113
|
+
return {
|
|
114
|
+
command,
|
|
115
|
+
daemon,
|
|
88
116
|
mode,
|
|
89
117
|
showHelp: true,
|
|
90
|
-
error:
|
|
118
|
+
error: CLI_MESSAGES.daemonOnlyStart,
|
|
91
119
|
};
|
|
92
120
|
}
|
|
93
121
|
return {
|
|
94
122
|
command,
|
|
123
|
+
daemon,
|
|
95
124
|
mode,
|
|
96
125
|
showHelp,
|
|
97
126
|
};
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseCliArgs } from "./cli/args.js";
|
|
3
3
|
import { resolveRuntimeMode, setRuntimeMode } from "./runtime/mode.js";
|
|
4
|
-
import {
|
|
4
|
+
import { getRuntimePaths } from "./runtime/paths.js";
|
|
5
5
|
const EXIT_SUCCESS = 0;
|
|
6
6
|
const EXIT_RUNTIME_ERROR = 1;
|
|
7
7
|
const EXIT_INVALID_ARGS = 2;
|
|
8
|
+
const CLI_USAGE = `Usage:
|
|
9
|
+
opencode-telegram [start] [--daemon] [--mode installed]
|
|
10
|
+
opencode-telegram status
|
|
11
|
+
opencode-telegram stop
|
|
12
|
+
opencode-telegram config
|
|
13
|
+
|
|
14
|
+
Notes:
|
|
15
|
+
- No command defaults to start
|
|
16
|
+
- start runs in foreground by default
|
|
17
|
+
- --daemon is supported for the installed runtime only`;
|
|
18
|
+
const CLI_MESSAGES = {
|
|
19
|
+
daemonRequiresInstalled: "Daemon mode is supported only for the installed runtime. Use `opencode-telegram start` for foreground source runs.",
|
|
20
|
+
unknownServiceError: "Unknown service error.",
|
|
21
|
+
cleanupStale: "Removed stale daemon state file.",
|
|
22
|
+
cleanupInvalid: "Removed invalid daemon state file.",
|
|
23
|
+
startSuccess: "OpenCode Telegram Bot daemon started.",
|
|
24
|
+
startAlreadyRunning: "OpenCode Telegram Bot daemon is already running.",
|
|
25
|
+
statusRunning: "Service status: running",
|
|
26
|
+
statusStopped: "Service status: stopped",
|
|
27
|
+
stopSuccess: "OpenCode Telegram Bot daemon stopped.",
|
|
28
|
+
stopAlreadyStopped: "OpenCode Telegram Bot daemon is not running.",
|
|
29
|
+
linePid: (pid) => `PID: ${pid}`,
|
|
30
|
+
lineStartedAt: (startedAt) => `Started at: ${startedAt}`,
|
|
31
|
+
lineUptimeSec: (seconds) => `Uptime: ${seconds} sec`,
|
|
32
|
+
lineLogFile: (filePath) => `Log file: ${filePath}`,
|
|
33
|
+
lineAppHome: (appHome) => `App home: ${appHome}`,
|
|
34
|
+
errorPrefix: (message) => `CLI error: ${message}`,
|
|
35
|
+
};
|
|
8
36
|
function writeStdout(message) {
|
|
9
37
|
process.stdout.write(`${message}\n`);
|
|
10
38
|
}
|
|
@@ -12,18 +40,33 @@ function writeStderr(message) {
|
|
|
12
40
|
process.stderr.write(`${message}\n`);
|
|
13
41
|
}
|
|
14
42
|
function printUsage() {
|
|
15
|
-
writeStdout(
|
|
43
|
+
writeStdout(CLI_USAGE);
|
|
16
44
|
}
|
|
17
|
-
function
|
|
18
|
-
if (
|
|
19
|
-
return
|
|
45
|
+
function formatServiceCleanupMessage(cleanupReason) {
|
|
46
|
+
if (cleanupReason === "stale") {
|
|
47
|
+
return CLI_MESSAGES.cleanupStale;
|
|
20
48
|
}
|
|
21
|
-
if (
|
|
22
|
-
return
|
|
49
|
+
if (cleanupReason === "invalid") {
|
|
50
|
+
return CLI_MESSAGES.cleanupInvalid;
|
|
23
51
|
}
|
|
24
|
-
return
|
|
52
|
+
return null;
|
|
25
53
|
}
|
|
26
|
-
|
|
54
|
+
function formatServiceDetails(details) {
|
|
55
|
+
const uptimeSec = Math.max(0, Math.floor((Date.now() - Date.parse(details.startedAt)) / 1000));
|
|
56
|
+
return [
|
|
57
|
+
CLI_MESSAGES.linePid(details.pid),
|
|
58
|
+
CLI_MESSAGES.lineStartedAt(details.startedAt),
|
|
59
|
+
CLI_MESSAGES.lineUptimeSec(uptimeSec),
|
|
60
|
+
CLI_MESSAGES.lineLogFile(details.logFilePath),
|
|
61
|
+
CLI_MESSAGES.lineAppHome(details.appHome),
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|
|
64
|
+
function writeOptionalLine(message) {
|
|
65
|
+
if (message) {
|
|
66
|
+
writeStdout(message);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function runStartCommand(mode, daemon) {
|
|
27
70
|
const modeResult = resolveRuntimeMode({
|
|
28
71
|
defaultMode: "installed",
|
|
29
72
|
explicitMode: mode,
|
|
@@ -32,10 +75,46 @@ async function runStartCommand(mode) {
|
|
|
32
75
|
throw new Error(modeResult.error);
|
|
33
76
|
}
|
|
34
77
|
setRuntimeMode(modeResult.mode);
|
|
35
|
-
|
|
36
|
-
|
|
78
|
+
if (daemon && modeResult.mode !== "installed") {
|
|
79
|
+
throw new Error(CLI_MESSAGES.daemonRequiresInstalled);
|
|
80
|
+
}
|
|
37
81
|
const { ensureRuntimeConfigForStart } = await import("./runtime/bootstrap.js");
|
|
38
82
|
await ensureRuntimeConfigForStart();
|
|
83
|
+
if (daemon) {
|
|
84
|
+
const { startBotDaemon } = await import("./service/manager.js");
|
|
85
|
+
const result = await startBotDaemon(modeResult.mode);
|
|
86
|
+
const cleanupMessage = formatServiceCleanupMessage(result.cleanupReason);
|
|
87
|
+
const runtimePaths = getRuntimePaths();
|
|
88
|
+
writeOptionalLine(cleanupMessage);
|
|
89
|
+
if (!result.success) {
|
|
90
|
+
if (result.alreadyRunning && result.service) {
|
|
91
|
+
writeStdout(CLI_MESSAGES.startAlreadyRunning);
|
|
92
|
+
writeStdout("");
|
|
93
|
+
writeStdout(formatServiceDetails({
|
|
94
|
+
pid: result.service.pid,
|
|
95
|
+
startedAt: result.service.startedAt,
|
|
96
|
+
logFilePath: result.service.logFilePath,
|
|
97
|
+
appHome: runtimePaths.appHome,
|
|
98
|
+
}));
|
|
99
|
+
return EXIT_SUCCESS;
|
|
100
|
+
}
|
|
101
|
+
throw new Error(result.error || CLI_MESSAGES.unknownServiceError);
|
|
102
|
+
}
|
|
103
|
+
if (!result.service) {
|
|
104
|
+
throw new Error(CLI_MESSAGES.unknownServiceError);
|
|
105
|
+
}
|
|
106
|
+
writeStdout(CLI_MESSAGES.startSuccess);
|
|
107
|
+
writeStdout("");
|
|
108
|
+
writeStdout(formatServiceDetails({
|
|
109
|
+
pid: result.service.pid,
|
|
110
|
+
startedAt: result.service.startedAt,
|
|
111
|
+
logFilePath: result.service.logFilePath,
|
|
112
|
+
appHome: runtimePaths.appHome,
|
|
113
|
+
}));
|
|
114
|
+
return EXIT_SUCCESS;
|
|
115
|
+
}
|
|
116
|
+
const { initializeLogger } = await import("./utils/logger.js");
|
|
117
|
+
await initializeLogger();
|
|
39
118
|
const { startBotApp } = await import("./app/start-bot-app.js");
|
|
40
119
|
await startBotApp();
|
|
41
120
|
return EXIT_SUCCESS;
|
|
@@ -48,8 +127,40 @@ async function runConfigCommand() {
|
|
|
48
127
|
await runConfigWizardCommand();
|
|
49
128
|
return EXIT_SUCCESS;
|
|
50
129
|
}
|
|
51
|
-
async function
|
|
52
|
-
|
|
130
|
+
async function runStatusCommand() {
|
|
131
|
+
setRuntimeMode("installed");
|
|
132
|
+
const { getBotServiceStatus } = await import("./service/manager.js");
|
|
133
|
+
const runtimePaths = getRuntimePaths();
|
|
134
|
+
const status = await getBotServiceStatus();
|
|
135
|
+
writeOptionalLine(formatServiceCleanupMessage(status.cleanupReason));
|
|
136
|
+
if (status.status !== "running" || !status.service) {
|
|
137
|
+
writeStdout(CLI_MESSAGES.statusStopped);
|
|
138
|
+
writeStdout(CLI_MESSAGES.lineAppHome(runtimePaths.appHome));
|
|
139
|
+
return EXIT_SUCCESS;
|
|
140
|
+
}
|
|
141
|
+
writeStdout(CLI_MESSAGES.statusRunning);
|
|
142
|
+
writeStdout("");
|
|
143
|
+
writeStdout(formatServiceDetails({
|
|
144
|
+
pid: status.service.pid,
|
|
145
|
+
startedAt: status.service.startedAt,
|
|
146
|
+
logFilePath: status.service.logFilePath,
|
|
147
|
+
appHome: runtimePaths.appHome,
|
|
148
|
+
}));
|
|
149
|
+
return EXIT_SUCCESS;
|
|
150
|
+
}
|
|
151
|
+
async function runStopCommand() {
|
|
152
|
+
setRuntimeMode("installed");
|
|
153
|
+
const { stopBotDaemon } = await import("./service/manager.js");
|
|
154
|
+
const result = await stopBotDaemon();
|
|
155
|
+
writeOptionalLine(formatServiceCleanupMessage(result.cleanupReason));
|
|
156
|
+
if (!result.success) {
|
|
157
|
+
throw new Error(result.error || CLI_MESSAGES.unknownServiceError);
|
|
158
|
+
}
|
|
159
|
+
if (result.alreadyStopped) {
|
|
160
|
+
writeStdout(CLI_MESSAGES.stopAlreadyStopped);
|
|
161
|
+
return EXIT_SUCCESS;
|
|
162
|
+
}
|
|
163
|
+
writeStdout(CLI_MESSAGES.stopSuccess);
|
|
53
164
|
return EXIT_SUCCESS;
|
|
54
165
|
}
|
|
55
166
|
async function runCli(argv) {
|
|
@@ -62,12 +173,15 @@ async function runCli(argv) {
|
|
|
62
173
|
return parsedArgs.error ? EXIT_INVALID_ARGS : EXIT_SUCCESS;
|
|
63
174
|
}
|
|
64
175
|
if (parsedArgs.command === "start") {
|
|
65
|
-
return runStartCommand(parsedArgs.mode);
|
|
176
|
+
return runStartCommand(parsedArgs.mode, parsedArgs.daemon);
|
|
66
177
|
}
|
|
67
178
|
if (parsedArgs.command === "config") {
|
|
68
179
|
return runConfigCommand();
|
|
69
180
|
}
|
|
70
|
-
|
|
181
|
+
if (parsedArgs.command === "status") {
|
|
182
|
+
return runStatusCommand();
|
|
183
|
+
}
|
|
184
|
+
return runStopCommand();
|
|
71
185
|
}
|
|
72
186
|
void runCli(process.argv.slice(2))
|
|
73
187
|
.then((exitCode) => {
|
|
@@ -75,10 +189,10 @@ void runCli(process.argv.slice(2))
|
|
|
75
189
|
})
|
|
76
190
|
.catch((error) => {
|
|
77
191
|
if (error instanceof Error) {
|
|
78
|
-
writeStderr(
|
|
192
|
+
writeStderr(CLI_MESSAGES.errorPrefix(error.message));
|
|
79
193
|
}
|
|
80
194
|
else {
|
|
81
|
-
writeStderr(
|
|
195
|
+
writeStderr(CLI_MESSAGES.errorPrefix(String(error)));
|
|
82
196
|
}
|
|
83
197
|
process.exitCode = EXIT_RUNTIME_ERROR;
|
|
84
198
|
});
|