@grinev/opencode-telegram-bot 0.16.1 → 0.18.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 +5 -0
- package/README.md +72 -43
- package/dist/app/start-bot-app.js +0 -2
- package/dist/attach/manager.js +66 -0
- package/dist/attach/service.js +166 -0
- package/dist/bot/commands/abort.js +0 -4
- package/dist/bot/commands/commands.js +13 -3
- package/dist/bot/commands/definitions.js +2 -0
- package/dist/bot/commands/new.js +10 -18
- 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/sessions.js +19 -20
- package/dist/bot/commands/skills.js +376 -0
- package/dist/bot/commands/start.js +2 -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/prompt.js +26 -31
- package/dist/bot/handlers/voice.js +8 -1
- package/dist/bot/index.js +90 -10
- package/dist/bot/middleware/interaction-guard.js +1 -1
- package/dist/bot/utils/busy-guard.js +3 -2
- package/dist/bot/utils/external-user-input.js +46 -0
- package/dist/bot/utils/switch-project.js +2 -0
- package/dist/config.js +2 -0
- package/dist/external-input/suppression.js +54 -0
- package/dist/git/worktree.js +158 -0
- package/dist/i18n/de.js +54 -1
- package/dist/i18n/en.js +54 -1
- package/dist/i18n/es.js +54 -1
- package/dist/i18n/fr.js +54 -1
- package/dist/i18n/ru.js +54 -1
- package/dist/i18n/zh.js +54 -1
- package/dist/interaction/guard.js +2 -1
- package/dist/opencode/events.js +13 -1
- package/dist/opencode/process.js +182 -0
- package/dist/pinned/manager.js +46 -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/settings/manager.js +9 -12
- package/dist/summary/aggregator.js +54 -9
- package/package.json +1 -1
- package/dist/process/manager.js +0 -273
- package/dist/process/types.js +0 -1
|
@@ -32,6 +32,7 @@ class SummaryAggregator {
|
|
|
32
32
|
lastUpdated = 0;
|
|
33
33
|
onCompleteCallback = null;
|
|
34
34
|
onPartialCallback = null;
|
|
35
|
+
onExternalUserInputCallback = null;
|
|
35
36
|
onToolCallback = null;
|
|
36
37
|
onToolFileCallback = null;
|
|
37
38
|
onQuestionCallback = null;
|
|
@@ -50,6 +51,7 @@ class SummaryAggregator {
|
|
|
50
51
|
onClearedCallback = null;
|
|
51
52
|
processedToolStates = new Set();
|
|
52
53
|
thinkingFiredForMessages = new Set();
|
|
54
|
+
deliveredExternalUserMessageIds = new Set();
|
|
53
55
|
knownTextPartIds = new Map();
|
|
54
56
|
bot = null;
|
|
55
57
|
chatId = null;
|
|
@@ -73,6 +75,9 @@ class SummaryAggregator {
|
|
|
73
75
|
setOnPartial(callback) {
|
|
74
76
|
this.onPartialCallback = callback;
|
|
75
77
|
}
|
|
78
|
+
setOnExternalUserInput(callback) {
|
|
79
|
+
this.onExternalUserInputCallback = callback;
|
|
80
|
+
}
|
|
76
81
|
setOnTool(callback) {
|
|
77
82
|
this.onToolCallback = callback;
|
|
78
83
|
}
|
|
@@ -226,6 +231,7 @@ class SummaryAggregator {
|
|
|
226
231
|
this.knownTextPartIds.clear();
|
|
227
232
|
this.processedToolStates.clear();
|
|
228
233
|
this.thinkingFiredForMessages.clear();
|
|
234
|
+
this.deliveredExternalUserMessageIds.clear();
|
|
229
235
|
this.trackedSessionParents.clear();
|
|
230
236
|
this.subagentStates.clear();
|
|
231
237
|
this.subagentOrder = [];
|
|
@@ -605,6 +611,10 @@ class SummaryAggregator {
|
|
|
605
611
|
}
|
|
606
612
|
const messageID = info.id;
|
|
607
613
|
this.messages.set(messageID, { role: info.role });
|
|
614
|
+
if (info.role === "user") {
|
|
615
|
+
this.emitExternalUserInputIfReady(info.sessionID, messageID);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
608
618
|
if (info.role === "assistant") {
|
|
609
619
|
if (!this.textMessageStates.has(messageID)) {
|
|
610
620
|
this.textMessageStates.set(messageID, {
|
|
@@ -655,15 +665,8 @@ class SummaryAggregator {
|
|
|
655
665
|
completedAt: time?.completed,
|
|
656
666
|
});
|
|
657
667
|
}
|
|
658
|
-
this.
|
|
659
|
-
this.messages.delete(messageID);
|
|
660
|
-
this.partHashes.delete(messageID);
|
|
661
|
-
this.knownTextPartIds.delete(messageID);
|
|
668
|
+
this.cleanupCompletedMessage(messageID);
|
|
662
669
|
logger.debug(`[Aggregator] Message completed cleanup: remaining messages=${this.textMessageStates.size}`);
|
|
663
|
-
if (this.textMessageStates.size === 0) {
|
|
664
|
-
logger.debug("[Aggregator] No more active messages, stopping typing indicator");
|
|
665
|
-
this.stopTypingIndicator();
|
|
666
|
-
}
|
|
667
670
|
}
|
|
668
671
|
this.lastUpdated = Date.now();
|
|
669
672
|
}
|
|
@@ -741,6 +744,9 @@ class SummaryAggregator {
|
|
|
741
744
|
this.startTypingIndicator();
|
|
742
745
|
this.emitPartialText(part.sessionID, messageID, fullText);
|
|
743
746
|
}
|
|
747
|
+
else if (messageInfo && messageInfo.role === "user") {
|
|
748
|
+
this.emitExternalUserInputIfReady(part.sessionID, messageID);
|
|
749
|
+
}
|
|
744
750
|
else {
|
|
745
751
|
const state = this.getOrCreateTextMessageState(messageID);
|
|
746
752
|
state.optimisticUpdateCount++;
|
|
@@ -857,9 +863,48 @@ class SummaryAggregator {
|
|
|
857
863
|
if (!combined.trim()) {
|
|
858
864
|
return;
|
|
859
865
|
}
|
|
866
|
+
const messageInfo = this.messages.get(messageID);
|
|
867
|
+
if (messageInfo?.role === "user") {
|
|
868
|
+
this.emitExternalUserInputIfReady(sessionID, messageID);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
860
871
|
this.startTypingIndicator();
|
|
861
872
|
this.emitPartialText(sessionID, messageID, combined);
|
|
862
873
|
}
|
|
874
|
+
emitExternalUserInputIfReady(sessionId, messageId) {
|
|
875
|
+
if (sessionId !== this.currentSessionId || this.deliveredExternalUserMessageIds.has(messageId)) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
const messageInfo = this.messages.get(messageId);
|
|
879
|
+
if (!messageInfo || messageInfo.role !== "user") {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const messageText = this.getCombinedMessageText(messageId).trim();
|
|
883
|
+
if (!messageText) {
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
this.deliveredExternalUserMessageIds.add(messageId);
|
|
887
|
+
this.cleanupCompletedMessage(messageId);
|
|
888
|
+
if (!this.onExternalUserInputCallback) {
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const callback = this.onExternalUserInputCallback;
|
|
892
|
+
setImmediate(() => {
|
|
893
|
+
Promise.resolve(callback(sessionId, messageId, messageText)).catch((err) => {
|
|
894
|
+
logger.error("[Aggregator] Error in external user input callback:", err);
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
cleanupCompletedMessage(messageId) {
|
|
899
|
+
this.textMessageStates.delete(messageId);
|
|
900
|
+
this.messages.delete(messageId);
|
|
901
|
+
this.partHashes.delete(messageId);
|
|
902
|
+
this.knownTextPartIds.delete(messageId);
|
|
903
|
+
if (this.textMessageStates.size === 0) {
|
|
904
|
+
logger.debug("[Aggregator] No more active messages, stopping typing indicator");
|
|
905
|
+
this.stopTypingIndicator();
|
|
906
|
+
}
|
|
907
|
+
}
|
|
863
908
|
emitPartialText(sessionId, messageId, messageText) {
|
|
864
909
|
if (!this.onPartialCallback || !messageText.trim()) {
|
|
865
910
|
return;
|
|
@@ -1103,7 +1148,7 @@ class SummaryAggregator {
|
|
|
1103
1148
|
const callback = this.onQuestionCallback;
|
|
1104
1149
|
setImmediate(async () => {
|
|
1105
1150
|
try {
|
|
1106
|
-
await callback(questions, id);
|
|
1151
|
+
await callback(questions, id, sessionID);
|
|
1107
1152
|
}
|
|
1108
1153
|
catch (err) {
|
|
1109
1154
|
logger.error("[Aggregator] Error in question callback:", err);
|
package/package.json
CHANGED
package/dist/process/manager.js
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import { spawn, exec } from "child_process";
|
|
2
|
-
import { promisify } from "util";
|
|
3
|
-
import { getServerProcess, setServerProcess, clearServerProcess } from "../settings/manager.js";
|
|
4
|
-
import { logger } from "../utils/logger.js";
|
|
5
|
-
const execAsync = promisify(exec);
|
|
6
|
-
/**
|
|
7
|
-
* Singleton manager for OpenCode server process
|
|
8
|
-
* Handles starting, stopping, and monitoring the server process
|
|
9
|
-
* Persists PID to settings.json for recovery after bot restart
|
|
10
|
-
*/
|
|
11
|
-
class ProcessManager {
|
|
12
|
-
state = {
|
|
13
|
-
process: null,
|
|
14
|
-
pid: null,
|
|
15
|
-
startTime: null,
|
|
16
|
-
isRunning: false,
|
|
17
|
-
};
|
|
18
|
-
/**
|
|
19
|
-
* Initialize the manager by restoring state from settings
|
|
20
|
-
* Checks if the stored process is still alive
|
|
21
|
-
*/
|
|
22
|
-
async initialize() {
|
|
23
|
-
const savedProcess = getServerProcess();
|
|
24
|
-
if (!savedProcess) {
|
|
25
|
-
logger.debug("[ProcessManager] No saved process found in settings");
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
logger.info(`[ProcessManager] Found saved process: PID=${savedProcess.pid}`);
|
|
29
|
-
// Check if the process is still alive
|
|
30
|
-
if (this.isProcessAlive(savedProcess.pid)) {
|
|
31
|
-
logger.info(`[ProcessManager] Process PID=${savedProcess.pid} is still alive, restoring state`);
|
|
32
|
-
this.state = {
|
|
33
|
-
process: null, // Cannot recover ChildProcess reference
|
|
34
|
-
pid: savedProcess.pid,
|
|
35
|
-
startTime: new Date(savedProcess.startTime),
|
|
36
|
-
isRunning: true,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
logger.warn(`[ProcessManager] Process PID=${savedProcess.pid} is dead, cleaning up`);
|
|
41
|
-
clearServerProcess();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Start the OpenCode server process
|
|
46
|
-
*/
|
|
47
|
-
async start() {
|
|
48
|
-
if (this.state.isRunning) {
|
|
49
|
-
return {
|
|
50
|
-
success: false,
|
|
51
|
-
error: "Process already running",
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
try {
|
|
55
|
-
logger.info("[ProcessManager] Starting OpenCode server process...");
|
|
56
|
-
const isWindows = process.platform === "win32";
|
|
57
|
-
const command = isWindows ? "cmd.exe" : "opencode";
|
|
58
|
-
const args = isWindows ? ["/c", "opencode", "serve"] : ["serve"];
|
|
59
|
-
// Spawn the process
|
|
60
|
-
// Windows: use cmd.exe to resolve npm-installed global commands
|
|
61
|
-
// Unix-like: run opencode directly
|
|
62
|
-
const childProcess = spawn(command, args, {
|
|
63
|
-
detached: false,
|
|
64
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
65
|
-
windowsHide: isWindows,
|
|
66
|
-
});
|
|
67
|
-
if (!childProcess.pid) {
|
|
68
|
-
throw new Error("Failed to start OpenCode server process. Ensure 'opencode' is installed and available in PATH.");
|
|
69
|
-
}
|
|
70
|
-
// Setup event handlers
|
|
71
|
-
childProcess.on("error", (err) => {
|
|
72
|
-
logger.error("[ProcessManager] Process error:", err);
|
|
73
|
-
this.cleanup();
|
|
74
|
-
});
|
|
75
|
-
childProcess.on("exit", (code, signal) => {
|
|
76
|
-
logger.info(`[ProcessManager] Process exited: code=${code}, signal=${signal}`);
|
|
77
|
-
this.cleanup();
|
|
78
|
-
});
|
|
79
|
-
// Log stdout/stderr
|
|
80
|
-
if (childProcess.stdout) {
|
|
81
|
-
childProcess.stdout.on("data", (data) => {
|
|
82
|
-
logger.debug(`[OpenCode Server] ${data.toString().trim()}`);
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
if (childProcess.stderr) {
|
|
86
|
-
childProcess.stderr.on("data", (data) => {
|
|
87
|
-
logger.warn(`[OpenCode Server Error] ${data.toString().trim()}`);
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
// Save state in memory
|
|
91
|
-
const startTime = new Date();
|
|
92
|
-
this.state = {
|
|
93
|
-
process: childProcess,
|
|
94
|
-
pid: childProcess.pid,
|
|
95
|
-
startTime,
|
|
96
|
-
isRunning: true,
|
|
97
|
-
};
|
|
98
|
-
// Persist to settings.json
|
|
99
|
-
setServerProcess({
|
|
100
|
-
pid: childProcess.pid,
|
|
101
|
-
startTime: startTime.toISOString(),
|
|
102
|
-
});
|
|
103
|
-
logger.info(`[ProcessManager] OpenCode server started with PID=${childProcess.pid}`);
|
|
104
|
-
return { success: true };
|
|
105
|
-
}
|
|
106
|
-
catch (err) {
|
|
107
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
108
|
-
logger.error("[ProcessManager] Failed to start process:", err);
|
|
109
|
-
this.cleanup();
|
|
110
|
-
return { success: false, error: errorMessage };
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Stop the OpenCode server process
|
|
115
|
-
* Sends SIGINT (Ctrl+C) and waits for graceful shutdown
|
|
116
|
-
* Falls back to SIGKILL if timeout is exceeded
|
|
117
|
-
*/
|
|
118
|
-
async stop(timeoutMs = 5000) {
|
|
119
|
-
if (!this.state.isRunning || !this.state.pid) {
|
|
120
|
-
return {
|
|
121
|
-
success: false,
|
|
122
|
-
error: "Process not running",
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
try {
|
|
126
|
-
const pid = this.state.pid;
|
|
127
|
-
logger.info(`[ProcessManager] Stopping process PID=${pid}...`);
|
|
128
|
-
// On Windows, use taskkill to kill the entire process tree
|
|
129
|
-
// This is necessary because cmd.exe spawns child processes
|
|
130
|
-
if (process.platform === "win32") {
|
|
131
|
-
try {
|
|
132
|
-
// /F = force terminate, /T = terminate tree, /PID = process id
|
|
133
|
-
logger.debug(`[ProcessManager] Using taskkill to terminate process tree for PID=${pid}`);
|
|
134
|
-
await execAsync(`taskkill /F /T /PID ${pid}`);
|
|
135
|
-
logger.info(`[ProcessManager] Process tree terminated successfully for PID=${pid}`);
|
|
136
|
-
}
|
|
137
|
-
catch (err) {
|
|
138
|
-
// taskkill returns error if process not found, which is ok
|
|
139
|
-
const error = err;
|
|
140
|
-
if (error.message?.includes("not found")) {
|
|
141
|
-
logger.debug(`[ProcessManager] Process PID=${pid} already terminated`);
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
logger.warn(`[ProcessManager] taskkill error for PID=${pid}:`, err);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
// Wait a bit for cleanup
|
|
148
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
// Unix-like systems: use SIGINT/SIGKILL
|
|
152
|
-
if (this.state.process) {
|
|
153
|
-
const childProcess = this.state.process;
|
|
154
|
-
// Send SIGINT (Ctrl+C)
|
|
155
|
-
logger.debug(`[ProcessManager] Sending SIGINT to PID=${pid}`);
|
|
156
|
-
childProcess.kill("SIGINT");
|
|
157
|
-
// Wait for graceful shutdown
|
|
158
|
-
const gracefulExit = await this.waitForProcessExit(childProcess, timeoutMs);
|
|
159
|
-
if (!gracefulExit && this.state.isRunning) {
|
|
160
|
-
logger.warn(`[ProcessManager] Graceful shutdown failed, sending SIGKILL to PID=${pid}`);
|
|
161
|
-
childProcess.kill("SIGKILL");
|
|
162
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
// No ChildProcess reference (recovered from settings)
|
|
167
|
-
logger.debug(`[ProcessManager] Sending SIGTERM to PID=${pid}`);
|
|
168
|
-
try {
|
|
169
|
-
process.kill(pid, "SIGTERM");
|
|
170
|
-
}
|
|
171
|
-
catch (err) {
|
|
172
|
-
logger.debug(`[ProcessManager] Failed to send SIGTERM to PID=${pid}:`, err);
|
|
173
|
-
}
|
|
174
|
-
// Wait for process to die
|
|
175
|
-
await new Promise((resolve) => setTimeout(resolve, timeoutMs));
|
|
176
|
-
// Check if still alive
|
|
177
|
-
if (this.isProcessAlive(pid)) {
|
|
178
|
-
logger.warn(`[ProcessManager] Graceful shutdown failed, sending SIGKILL to PID=${pid}`);
|
|
179
|
-
try {
|
|
180
|
-
process.kill(pid, "SIGKILL");
|
|
181
|
-
}
|
|
182
|
-
catch (err) {
|
|
183
|
-
logger.error(`[ProcessManager] Failed to send SIGKILL to PID=${pid}:`, err);
|
|
184
|
-
}
|
|
185
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
this.cleanup();
|
|
190
|
-
logger.info(`[ProcessManager] Process PID=${pid} stopped successfully`);
|
|
191
|
-
return { success: true };
|
|
192
|
-
}
|
|
193
|
-
catch (err) {
|
|
194
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
195
|
-
logger.error("[ProcessManager] Failed to stop process:", err);
|
|
196
|
-
return { success: false, error: errorMessage };
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Check if the process is running
|
|
201
|
-
* Validates that the process with stored PID is actually alive
|
|
202
|
-
*/
|
|
203
|
-
isRunning() {
|
|
204
|
-
if (!this.state.isRunning || !this.state.pid) {
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
// Verify that the process is actually alive
|
|
208
|
-
if (!this.isProcessAlive(this.state.pid)) {
|
|
209
|
-
logger.warn(`[ProcessManager] Process PID=${this.state.pid} appears dead, cleaning up`);
|
|
210
|
-
this.cleanup();
|
|
211
|
-
return false;
|
|
212
|
-
}
|
|
213
|
-
return true;
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* Get the process ID of the running server
|
|
217
|
-
*/
|
|
218
|
-
getPID() {
|
|
219
|
-
return this.state.pid;
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Get the uptime of the server in milliseconds
|
|
223
|
-
*/
|
|
224
|
-
getUptime() {
|
|
225
|
-
if (!this.state.startTime || !this.state.isRunning) {
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
return Date.now() - this.state.startTime.getTime();
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Check if a process with given PID is alive
|
|
232
|
-
* Uses process.kill(pid, 0) which checks existence without killing
|
|
233
|
-
*/
|
|
234
|
-
isProcessAlive(pid) {
|
|
235
|
-
try {
|
|
236
|
-
process.kill(pid, 0);
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
catch {
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Wait for process to exit
|
|
245
|
-
*/
|
|
246
|
-
async waitForProcessExit(childProcess, timeoutMs) {
|
|
247
|
-
return new Promise((resolve) => {
|
|
248
|
-
const exitHandler = () => {
|
|
249
|
-
logger.debug("[ProcessManager] Process exited gracefully");
|
|
250
|
-
resolve(true);
|
|
251
|
-
};
|
|
252
|
-
childProcess.once("exit", exitHandler);
|
|
253
|
-
setTimeout(() => {
|
|
254
|
-
childProcess.removeListener("exit", exitHandler);
|
|
255
|
-
resolve(false);
|
|
256
|
-
}, timeoutMs);
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Clean up state and settings
|
|
261
|
-
*/
|
|
262
|
-
cleanup() {
|
|
263
|
-
this.state = {
|
|
264
|
-
process: null,
|
|
265
|
-
pid: null,
|
|
266
|
-
startTime: null,
|
|
267
|
-
isRunning: false,
|
|
268
|
-
};
|
|
269
|
-
clearServerProcess();
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
// Export singleton instance
|
|
273
|
-
export const processManager = new ProcessManager();
|
package/dist/process/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|