@grinev/opencode-telegram-bot 0.1.0-rc.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.
- package/.env.example +34 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/agent/manager.js +92 -0
- package/dist/agent/types.js +26 -0
- package/dist/app/start-bot-app.js +26 -0
- package/dist/bot/commands/agent.js +16 -0
- package/dist/bot/commands/definitions.js +20 -0
- package/dist/bot/commands/help.js +7 -0
- package/dist/bot/commands/model.js +16 -0
- package/dist/bot/commands/models.js +37 -0
- package/dist/bot/commands/new.js +58 -0
- package/dist/bot/commands/opencode-start.js +87 -0
- package/dist/bot/commands/opencode-stop.js +46 -0
- package/dist/bot/commands/projects.js +104 -0
- package/dist/bot/commands/server-restart.js +23 -0
- package/dist/bot/commands/server-start.js +23 -0
- package/dist/bot/commands/sessions.js +240 -0
- package/dist/bot/commands/start.js +40 -0
- package/dist/bot/commands/status.js +63 -0
- package/dist/bot/commands/stop.js +92 -0
- package/dist/bot/handlers/agent.js +96 -0
- package/dist/bot/handlers/context.js +112 -0
- package/dist/bot/handlers/model.js +115 -0
- package/dist/bot/handlers/permission.js +158 -0
- package/dist/bot/handlers/question.js +294 -0
- package/dist/bot/handlers/variant.js +126 -0
- package/dist/bot/index.js +573 -0
- package/dist/bot/middleware/auth.js +30 -0
- package/dist/bot/utils/keyboard.js +66 -0
- package/dist/cli/args.js +97 -0
- package/dist/cli.js +90 -0
- package/dist/config.js +46 -0
- package/dist/index.js +26 -0
- package/dist/keyboard/manager.js +171 -0
- package/dist/keyboard/types.js +1 -0
- package/dist/model/manager.js +123 -0
- package/dist/model/types.js +26 -0
- package/dist/opencode/client.js +13 -0
- package/dist/opencode/events.js +79 -0
- package/dist/opencode/server.js +104 -0
- package/dist/permission/manager.js +78 -0
- package/dist/permission/types.js +1 -0
- package/dist/pinned/manager.js +610 -0
- package/dist/pinned/types.js +1 -0
- package/dist/pinned-message/service.js +54 -0
- package/dist/process/manager.js +273 -0
- package/dist/process/types.js +1 -0
- package/dist/project/manager.js +28 -0
- package/dist/question/manager.js +143 -0
- package/dist/question/types.js +1 -0
- package/dist/runtime/bootstrap.js +278 -0
- package/dist/runtime/mode.js +74 -0
- package/dist/runtime/paths.js +37 -0
- package/dist/session/manager.js +10 -0
- package/dist/session/state.js +24 -0
- package/dist/settings/manager.js +99 -0
- package/dist/status/formatter.js +44 -0
- package/dist/summary/aggregator.js +427 -0
- package/dist/summary/formatter.js +226 -0
- package/dist/utils/formatting.js +237 -0
- package/dist/utils/logger.js +59 -0
- package/dist/utils/safe-background-task.js +33 -0
- package/dist/variant/manager.js +103 -0
- package/dist/variant/types.js +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { prepareCodeFile } from "./formatter.js";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
import { getCurrentProject } from "../settings/manager.js";
|
|
4
|
+
class SummaryAggregator {
|
|
5
|
+
currentSessionId = null;
|
|
6
|
+
currentMessageParts = new Map();
|
|
7
|
+
pendingParts = new Map();
|
|
8
|
+
messages = new Map();
|
|
9
|
+
messageCount = 0;
|
|
10
|
+
lastUpdated = 0;
|
|
11
|
+
onCompleteCallback = null;
|
|
12
|
+
onToolCallback = null;
|
|
13
|
+
onToolFileCallback = null;
|
|
14
|
+
onQuestionCallback = null;
|
|
15
|
+
onQuestionErrorCallback = null;
|
|
16
|
+
onThinkingCallback = null;
|
|
17
|
+
onTokensCallback = null;
|
|
18
|
+
onSessionCompactedCallback = null;
|
|
19
|
+
onPermissionCallback = null;
|
|
20
|
+
onSessionDiffCallback = null;
|
|
21
|
+
onFileChangeCallback = null;
|
|
22
|
+
processedToolStates = new Set();
|
|
23
|
+
bot = null;
|
|
24
|
+
chatId = null;
|
|
25
|
+
typingTimer = null;
|
|
26
|
+
partHashes = new Map();
|
|
27
|
+
setBotAndChatId(bot, chatId) {
|
|
28
|
+
this.bot = bot;
|
|
29
|
+
this.chatId = chatId;
|
|
30
|
+
}
|
|
31
|
+
setOnComplete(callback) {
|
|
32
|
+
this.onCompleteCallback = callback;
|
|
33
|
+
}
|
|
34
|
+
setOnTool(callback) {
|
|
35
|
+
this.onToolCallback = callback;
|
|
36
|
+
}
|
|
37
|
+
setOnToolFile(callback) {
|
|
38
|
+
this.onToolFileCallback = callback;
|
|
39
|
+
}
|
|
40
|
+
setOnQuestion(callback) {
|
|
41
|
+
this.onQuestionCallback = callback;
|
|
42
|
+
}
|
|
43
|
+
setOnQuestionError(callback) {
|
|
44
|
+
this.onQuestionErrorCallback = callback;
|
|
45
|
+
}
|
|
46
|
+
setOnThinking(callback) {
|
|
47
|
+
this.onThinkingCallback = callback;
|
|
48
|
+
}
|
|
49
|
+
setOnTokens(callback) {
|
|
50
|
+
this.onTokensCallback = callback;
|
|
51
|
+
}
|
|
52
|
+
setOnSessionCompacted(callback) {
|
|
53
|
+
this.onSessionCompactedCallback = callback;
|
|
54
|
+
}
|
|
55
|
+
setOnPermission(callback) {
|
|
56
|
+
this.onPermissionCallback = callback;
|
|
57
|
+
}
|
|
58
|
+
setOnSessionDiff(callback) {
|
|
59
|
+
this.onSessionDiffCallback = callback;
|
|
60
|
+
}
|
|
61
|
+
setOnFileChange(callback) {
|
|
62
|
+
this.onFileChangeCallback = callback;
|
|
63
|
+
}
|
|
64
|
+
startTypingIndicator() {
|
|
65
|
+
if (this.typingTimer) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const sendTyping = () => {
|
|
69
|
+
if (this.bot && this.chatId) {
|
|
70
|
+
this.bot.api.sendChatAction(this.chatId, "typing").catch((err) => {
|
|
71
|
+
logger.error("Failed to send typing action:", err);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
sendTyping();
|
|
76
|
+
this.typingTimer = setInterval(sendTyping, 4000);
|
|
77
|
+
}
|
|
78
|
+
stopTypingIndicator() {
|
|
79
|
+
if (this.typingTimer) {
|
|
80
|
+
clearInterval(this.typingTimer);
|
|
81
|
+
this.typingTimer = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
processEvent(event) {
|
|
85
|
+
// Log all question-related events for debugging
|
|
86
|
+
if (event.type.startsWith("question.")) {
|
|
87
|
+
logger.info(`[Aggregator] Question event: ${event.type}`, JSON.stringify(event.properties, null, 2));
|
|
88
|
+
}
|
|
89
|
+
// Log all session-related events for debugging
|
|
90
|
+
if (event.type.startsWith("session.")) {
|
|
91
|
+
logger.debug(`[Aggregator] Session event: ${event.type}`, JSON.stringify(event.properties, null, 2));
|
|
92
|
+
}
|
|
93
|
+
switch (event.type) {
|
|
94
|
+
case "message.updated":
|
|
95
|
+
this.handleMessageUpdated(event);
|
|
96
|
+
break;
|
|
97
|
+
case "message.part.updated":
|
|
98
|
+
this.handleMessagePartUpdated(event);
|
|
99
|
+
break;
|
|
100
|
+
case "session.status":
|
|
101
|
+
this.handleSessionStatus(event);
|
|
102
|
+
break;
|
|
103
|
+
case "session.idle":
|
|
104
|
+
this.handleSessionIdle(event);
|
|
105
|
+
break;
|
|
106
|
+
case "session.compacted":
|
|
107
|
+
this.handleSessionCompacted(event);
|
|
108
|
+
break;
|
|
109
|
+
case "question.asked":
|
|
110
|
+
this.handleQuestionAsked(event);
|
|
111
|
+
break;
|
|
112
|
+
case "question.replied":
|
|
113
|
+
logger.info(`[Aggregator] Question replied: requestID=${event.properties.requestID}`);
|
|
114
|
+
break;
|
|
115
|
+
case "question.rejected":
|
|
116
|
+
logger.info(`[Aggregator] Question rejected: requestID=${event.properties.requestID}`);
|
|
117
|
+
break;
|
|
118
|
+
case "session.diff":
|
|
119
|
+
this.handleSessionDiff(event);
|
|
120
|
+
break;
|
|
121
|
+
case "permission.asked":
|
|
122
|
+
this.handlePermissionAsked(event);
|
|
123
|
+
break;
|
|
124
|
+
case "permission.replied":
|
|
125
|
+
logger.info(`[Aggregator] Permission replied: requestID=${event.properties.requestID}`);
|
|
126
|
+
break;
|
|
127
|
+
default:
|
|
128
|
+
logger.debug(`[Aggregator] Unhandled event type: ${event.type}`);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
setSession(sessionId) {
|
|
133
|
+
if (this.currentSessionId !== sessionId) {
|
|
134
|
+
this.clear();
|
|
135
|
+
this.currentSessionId = sessionId;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
clear() {
|
|
139
|
+
this.stopTypingIndicator();
|
|
140
|
+
this.currentSessionId = null;
|
|
141
|
+
this.currentMessageParts.clear();
|
|
142
|
+
this.pendingParts.clear();
|
|
143
|
+
this.messages.clear();
|
|
144
|
+
this.partHashes.clear();
|
|
145
|
+
this.processedToolStates.clear();
|
|
146
|
+
this.messageCount = 0;
|
|
147
|
+
this.lastUpdated = 0;
|
|
148
|
+
}
|
|
149
|
+
handleMessageUpdated(event) {
|
|
150
|
+
const { info } = event.properties;
|
|
151
|
+
if (info.sessionID !== this.currentSessionId) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const messageID = info.id;
|
|
155
|
+
this.messages.set(messageID, { role: info.role });
|
|
156
|
+
if (info.role === "assistant") {
|
|
157
|
+
if (!this.currentMessageParts.has(messageID)) {
|
|
158
|
+
this.currentMessageParts.set(messageID, []);
|
|
159
|
+
this.messageCount++;
|
|
160
|
+
this.startTypingIndicator();
|
|
161
|
+
// Notify that agent started thinking
|
|
162
|
+
if (this.onThinkingCallback) {
|
|
163
|
+
setImmediate(() => {
|
|
164
|
+
this.onThinkingCallback();
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const pending = this.pendingParts.get(messageID) || [];
|
|
169
|
+
const current = this.currentMessageParts.get(messageID) || [];
|
|
170
|
+
this.currentMessageParts.set(messageID, [...current, ...pending]);
|
|
171
|
+
this.pendingParts.delete(messageID);
|
|
172
|
+
const assistantMessage = info;
|
|
173
|
+
const time = assistantMessage.time;
|
|
174
|
+
if (time?.completed) {
|
|
175
|
+
const parts = this.currentMessageParts.get(messageID) || [];
|
|
176
|
+
const lastPart = parts[parts.length - 1] || "";
|
|
177
|
+
logger.debug(`[Aggregator] Message part completed: messageId=${messageID}, textLength=${lastPart.length}, totalParts=${parts.length}, session=${this.currentSessionId}`);
|
|
178
|
+
// Extract and report tokens BEFORE onComplete so keyboard context is updated
|
|
179
|
+
const assistantInfo = info;
|
|
180
|
+
if (this.onTokensCallback && assistantInfo.tokens) {
|
|
181
|
+
const tokens = {
|
|
182
|
+
input: assistantInfo.tokens.input,
|
|
183
|
+
output: assistantInfo.tokens.output,
|
|
184
|
+
reasoning: assistantInfo.tokens.reasoning,
|
|
185
|
+
cacheRead: assistantInfo.tokens.cache?.read || 0,
|
|
186
|
+
cacheWrite: assistantInfo.tokens.cache?.write || 0,
|
|
187
|
+
};
|
|
188
|
+
logger.debug(`[Aggregator] Tokens: input=${tokens.input}, output=${tokens.output}, reasoning=${tokens.reasoning}`);
|
|
189
|
+
// Call synchronously so keyboardManager is updated before onComplete sends the reply
|
|
190
|
+
this.onTokensCallback(tokens);
|
|
191
|
+
}
|
|
192
|
+
if (this.onCompleteCallback && lastPart.length > 0) {
|
|
193
|
+
this.onCompleteCallback(this.currentSessionId, lastPart);
|
|
194
|
+
}
|
|
195
|
+
this.currentMessageParts.delete(messageID);
|
|
196
|
+
this.messages.delete(messageID);
|
|
197
|
+
this.partHashes.delete(messageID);
|
|
198
|
+
logger.debug(`[Aggregator] Message completed cleanup: remaining messages=${this.currentMessageParts.size}`);
|
|
199
|
+
if (this.currentMessageParts.size === 0) {
|
|
200
|
+
logger.debug("[Aggregator] No more active messages, stopping typing indicator");
|
|
201
|
+
this.stopTypingIndicator();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
this.lastUpdated = Date.now();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
handleMessagePartUpdated(event) {
|
|
208
|
+
const { part } = event.properties;
|
|
209
|
+
if (part.sessionID !== this.currentSessionId) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const messageID = part.messageID;
|
|
213
|
+
const messageInfo = this.messages.get(messageID);
|
|
214
|
+
if (part.type === "text" && "text" in part && part.text) {
|
|
215
|
+
const partHash = this.hashString(part.text);
|
|
216
|
+
if (!this.partHashes.has(messageID)) {
|
|
217
|
+
this.partHashes.set(messageID, new Set());
|
|
218
|
+
}
|
|
219
|
+
const hashes = this.partHashes.get(messageID);
|
|
220
|
+
if (hashes.has(partHash)) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
hashes.add(partHash);
|
|
224
|
+
if (messageInfo && messageInfo.role === "assistant") {
|
|
225
|
+
if (!this.currentMessageParts.has(messageID)) {
|
|
226
|
+
this.currentMessageParts.set(messageID, []);
|
|
227
|
+
this.startTypingIndicator();
|
|
228
|
+
}
|
|
229
|
+
const parts = this.currentMessageParts.get(messageID);
|
|
230
|
+
parts.push(part.text);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
if (!this.pendingParts.has(messageID)) {
|
|
234
|
+
this.pendingParts.set(messageID, []);
|
|
235
|
+
}
|
|
236
|
+
const pending = this.pendingParts.get(messageID);
|
|
237
|
+
pending.push(part.text);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else if (part.type === "tool") {
|
|
241
|
+
const state = part.state;
|
|
242
|
+
const input = "input" in state ? state.input : undefined;
|
|
243
|
+
const title = "title" in state ? state.title : undefined;
|
|
244
|
+
logger.debug(`[Aggregator] Tool event: callID=${part.callID}, tool=${part.tool}, status=${"status" in state ? state.status : "unknown"}`);
|
|
245
|
+
if (part.tool === "question") {
|
|
246
|
+
logger.debug(`[Aggregator] Question tool part update:`, JSON.stringify(part, null, 2));
|
|
247
|
+
// ะัะปะธ question tool ะทะฐะฒะตััะธะปัั ั ะพัะธะฑะบะพะน, ะพัะธัะฐะตะผ ะฐะบัะธะฒะฝัะน ะพะฟัะพั
|
|
248
|
+
// ััะพะฑั ะฐะณะตะฝั ะผะพะณ ะฟะตัะตัะพะทะดะฐัั ะตะณะพ ั ะธัะฟัะฐะฒะปะตะฝะฝัะผะธ ะดะฐะฝะฝัะผะธ
|
|
249
|
+
if ("status" in state && state.status === "error") {
|
|
250
|
+
logger.info(`[Aggregator] Question tool failed with error, clearing active poll. callID=${part.callID}`);
|
|
251
|
+
if (this.onQuestionErrorCallback) {
|
|
252
|
+
setImmediate(() => {
|
|
253
|
+
this.onQuestionErrorCallback();
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// NOTE: Questions are now handled via "question.asked" event, not via tool part updates.
|
|
259
|
+
// This ensures we have access to the requestID needed for question.reply().
|
|
260
|
+
}
|
|
261
|
+
if ("status" in state && state.status === "completed") {
|
|
262
|
+
logger.debug(`[Aggregator] Tool completed: callID=${part.callID}, tool=${part.tool}`, JSON.stringify(state, null, 2));
|
|
263
|
+
const notifiedKey = `notified-${part.callID}`;
|
|
264
|
+
if (!this.processedToolStates.has(notifiedKey)) {
|
|
265
|
+
this.processedToolStates.add(notifiedKey);
|
|
266
|
+
const toolData = {
|
|
267
|
+
messageId: messageID,
|
|
268
|
+
callId: part.callID,
|
|
269
|
+
tool: part.tool,
|
|
270
|
+
state: part.state,
|
|
271
|
+
input,
|
|
272
|
+
title,
|
|
273
|
+
metadata: state.metadata,
|
|
274
|
+
};
|
|
275
|
+
logger.debug(`[Aggregator] Sending tool notification to Telegram: tool=${part.tool}, title=${title || "N/A"}`);
|
|
276
|
+
if (this.onToolCallback) {
|
|
277
|
+
this.onToolCallback(toolData);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if ("status" in state && state.status === "completed") {
|
|
282
|
+
const fileKey = `file-${part.callID}`;
|
|
283
|
+
if (!this.processedToolStates.has(fileKey)) {
|
|
284
|
+
this.processedToolStates.add(fileKey);
|
|
285
|
+
if (part.tool === "write" && input && "content" in input && "filePath" in input) {
|
|
286
|
+
const fileData = prepareCodeFile(input.content, input.filePath, "write");
|
|
287
|
+
if (fileData && this.onToolFileCallback) {
|
|
288
|
+
logger.debug(`[Aggregator] Sending write file: ${fileData.filename} (${fileData.buffer.length} bytes)`);
|
|
289
|
+
this.onToolFileCallback(fileData);
|
|
290
|
+
}
|
|
291
|
+
// Notify about file change for pinned message
|
|
292
|
+
if (this.onFileChangeCallback) {
|
|
293
|
+
const lines = input.content.split("\n").length;
|
|
294
|
+
this.onFileChangeCallback({
|
|
295
|
+
file: input.filePath,
|
|
296
|
+
additions: lines,
|
|
297
|
+
deletions: 0,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else if (part.tool === "edit" &&
|
|
302
|
+
state.metadata &&
|
|
303
|
+
"diff" in state.metadata &&
|
|
304
|
+
"filediff" in state.metadata) {
|
|
305
|
+
const filediff = state.metadata.filediff;
|
|
306
|
+
const filePath = filediff.file;
|
|
307
|
+
const diff = state.metadata.diff;
|
|
308
|
+
if (filePath && diff) {
|
|
309
|
+
const fileData = prepareCodeFile(diff, filePath, "edit");
|
|
310
|
+
if (fileData && this.onToolFileCallback) {
|
|
311
|
+
logger.debug(`[Aggregator] Sending edit file: ${fileData.filename} (${fileData.buffer.length} bytes)`);
|
|
312
|
+
this.onToolFileCallback(fileData);
|
|
313
|
+
}
|
|
314
|
+
// Notify about file change for pinned message
|
|
315
|
+
if (this.onFileChangeCallback) {
|
|
316
|
+
this.onFileChangeCallback({
|
|
317
|
+
file: filePath,
|
|
318
|
+
additions: filediff.additions || 0,
|
|
319
|
+
deletions: filediff.deletions || 0,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
this.lastUpdated = Date.now();
|
|
328
|
+
}
|
|
329
|
+
hashString(str) {
|
|
330
|
+
let hash = 0;
|
|
331
|
+
for (let i = 0; i < str.length; i++) {
|
|
332
|
+
const char = str.charCodeAt(i);
|
|
333
|
+
hash = (hash << 5) - hash + char;
|
|
334
|
+
hash = hash & hash;
|
|
335
|
+
}
|
|
336
|
+
return hash.toString(36);
|
|
337
|
+
}
|
|
338
|
+
handleSessionStatus(event) {
|
|
339
|
+
const { sessionID } = event.properties;
|
|
340
|
+
if (sessionID !== this.currentSessionId) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
handleSessionIdle(event) {
|
|
345
|
+
const { sessionID } = event.properties;
|
|
346
|
+
if (sessionID !== this.currentSessionId) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
logger.info(`[Aggregator] Session became idle: ${sessionID}`);
|
|
350
|
+
// Stop typing indicator when session goes idle
|
|
351
|
+
this.stopTypingIndicator();
|
|
352
|
+
}
|
|
353
|
+
handleSessionCompacted(event) {
|
|
354
|
+
const properties = event.properties;
|
|
355
|
+
const { sessionID } = properties;
|
|
356
|
+
if (sessionID !== this.currentSessionId) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
logger.info(`[Aggregator] Session compacted: ${sessionID}`);
|
|
360
|
+
// Reload context from history after compaction
|
|
361
|
+
if (this.onSessionCompactedCallback) {
|
|
362
|
+
setImmediate(() => {
|
|
363
|
+
const project = getCurrentProject();
|
|
364
|
+
if (project) {
|
|
365
|
+
this.onSessionCompactedCallback(sessionID, project.worktree);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
handleQuestionAsked(event) {
|
|
371
|
+
const { id, sessionID, questions } = event.properties;
|
|
372
|
+
if (sessionID !== this.currentSessionId) {
|
|
373
|
+
logger.debug(`[Aggregator] Ignoring question.asked for different session: ${sessionID} (current: ${this.currentSessionId})`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
logger.info(`[Aggregator] Question asked: requestID=${id}, questions=${questions.length}`);
|
|
377
|
+
if (this.onQuestionCallback) {
|
|
378
|
+
const callback = this.onQuestionCallback;
|
|
379
|
+
setImmediate(async () => {
|
|
380
|
+
try {
|
|
381
|
+
await callback(questions, id);
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
logger.error("[Aggregator] Error in question callback:", err);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
handleSessionDiff(event) {
|
|
390
|
+
const properties = event.properties;
|
|
391
|
+
if (properties.sessionID !== this.currentSessionId) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
logger.debug(`[Aggregator] Session diff: ${properties.diff.length} files changed`);
|
|
395
|
+
if (this.onSessionDiffCallback) {
|
|
396
|
+
const diffs = properties.diff.map((d) => ({
|
|
397
|
+
file: d.file,
|
|
398
|
+
additions: d.additions,
|
|
399
|
+
deletions: d.deletions,
|
|
400
|
+
}));
|
|
401
|
+
const callback = this.onSessionDiffCallback;
|
|
402
|
+
setImmediate(() => {
|
|
403
|
+
callback(properties.sessionID, diffs);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
handlePermissionAsked(event) {
|
|
408
|
+
const request = event.properties;
|
|
409
|
+
if (request.sessionID !== this.currentSessionId) {
|
|
410
|
+
logger.debug(`[Aggregator] Ignoring permission.asked for different session: ${request.sessionID} (current: ${this.currentSessionId})`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
logger.info(`[Aggregator] Permission asked: requestID=${request.id}, type=${request.permission}, patterns=${request.patterns.length}`);
|
|
414
|
+
if (this.onPermissionCallback) {
|
|
415
|
+
const callback = this.onPermissionCallback;
|
|
416
|
+
setImmediate(async () => {
|
|
417
|
+
try {
|
|
418
|
+
await callback(request);
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
logger.error("[Aggregator] Error in permission callback:", err);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
export const summaryAggregator = new SummaryAggregator();
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { config } from "../config.js";
|
|
3
|
+
import { logger } from "../utils/logger.js";
|
|
4
|
+
const TELEGRAM_MESSAGE_LIMIT = 4096;
|
|
5
|
+
function splitText(text, maxLength) {
|
|
6
|
+
const parts = [];
|
|
7
|
+
let currentIndex = 0;
|
|
8
|
+
while (currentIndex < text.length) {
|
|
9
|
+
let endIndex = currentIndex + maxLength;
|
|
10
|
+
if (endIndex >= text.length) {
|
|
11
|
+
parts.push(text.slice(currentIndex));
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
const breakPoint = text.lastIndexOf("\n", endIndex);
|
|
15
|
+
if (breakPoint > currentIndex) {
|
|
16
|
+
endIndex = breakPoint + 1;
|
|
17
|
+
}
|
|
18
|
+
parts.push(text.slice(currentIndex, endIndex));
|
|
19
|
+
currentIndex = endIndex;
|
|
20
|
+
}
|
|
21
|
+
return parts;
|
|
22
|
+
}
|
|
23
|
+
export function formatSummary(text) {
|
|
24
|
+
if (!text || text.trim().length === 0) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const parts = splitText(text, TELEGRAM_MESSAGE_LIMIT);
|
|
28
|
+
const formattedParts = [];
|
|
29
|
+
for (const part of parts) {
|
|
30
|
+
const trimmed = part.trim();
|
|
31
|
+
if (!trimmed) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (parts.length > 1) {
|
|
35
|
+
formattedParts.push(`\`\`\`\n${trimmed}\n\`\`\``);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
formattedParts.push(trimmed);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return formattedParts;
|
|
42
|
+
}
|
|
43
|
+
function getToolDetails(tool, input) {
|
|
44
|
+
if (!input) {
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
// ะกะฝะฐัะฐะปะฐ ะฟัะพะฒะตััะตะผ ัะฟะตัะธัะธัะฝัะต ะดะปั ะธะทะฒะตััะฝัั
ััะปะพะฒ ะฟะพะปั
|
|
48
|
+
switch (tool) {
|
|
49
|
+
case "read":
|
|
50
|
+
case "edit":
|
|
51
|
+
case "write":
|
|
52
|
+
const path = input.path || input.filePath;
|
|
53
|
+
if (typeof path === "string")
|
|
54
|
+
return path;
|
|
55
|
+
break;
|
|
56
|
+
case "bash":
|
|
57
|
+
if (typeof input.command === "string")
|
|
58
|
+
return input.command;
|
|
59
|
+
break;
|
|
60
|
+
case "grep":
|
|
61
|
+
case "glob":
|
|
62
|
+
if (typeof input.pattern === "string")
|
|
63
|
+
return input.pattern;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
// ะฃะฝะธะฒะตััะฐะปัะฝัะน ะฟะพะธัะบ ะดะปั MCP ะธ ะฟัะพัะธั
ะธะฝััััะผะตะฝัะพะฒ
|
|
67
|
+
// ะัะตะผ ัะธะฟะธัะฝัะต ะฟะพะปั: query, url, name, prompt
|
|
68
|
+
const commonFields = ["query", "url", "name", "prompt", "text"];
|
|
69
|
+
for (const field of commonFields) {
|
|
70
|
+
if (typeof input[field] === "string") {
|
|
71
|
+
return input[field];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// ะัะปะธ ะฝะธัะตะณะพ ะฝะต ะฝะฐัะปะธ, ะฝะพ ะตััั ัััะพะบะพะฒัะต ะฟะพะปั, ะฑะตัะตะผ ะฟะตัะฒะพะต ะฟะพะฟะฐะฒัะตะตัั (ะบัะพะผะต description)
|
|
75
|
+
for (const [key, value] of Object.entries(input)) {
|
|
76
|
+
if (key !== "description" && typeof value === "string" && value.length > 0) {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
function getToolIcon(tool) {
|
|
83
|
+
switch (tool) {
|
|
84
|
+
case "read":
|
|
85
|
+
return "๐";
|
|
86
|
+
case "write":
|
|
87
|
+
return "โ๏ธ";
|
|
88
|
+
case "edit":
|
|
89
|
+
return "โ๏ธ";
|
|
90
|
+
case "bash":
|
|
91
|
+
return "๐ป";
|
|
92
|
+
case "glob":
|
|
93
|
+
return "๐";
|
|
94
|
+
case "grep":
|
|
95
|
+
return "๐";
|
|
96
|
+
case "task":
|
|
97
|
+
return "๐ค";
|
|
98
|
+
case "question":
|
|
99
|
+
return "โ";
|
|
100
|
+
case "todoread":
|
|
101
|
+
return "๐";
|
|
102
|
+
case "todowrite":
|
|
103
|
+
return "๐";
|
|
104
|
+
case "webfetch":
|
|
105
|
+
return "๐";
|
|
106
|
+
case "web-search_tavily_search":
|
|
107
|
+
return "๐";
|
|
108
|
+
case "web-search_tavily_extract":
|
|
109
|
+
return "๐";
|
|
110
|
+
case "skill":
|
|
111
|
+
return "๐";
|
|
112
|
+
default:
|
|
113
|
+
return "๐ ๏ธ";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function formatTodos(todos) {
|
|
117
|
+
const MAX_TODOS = 20;
|
|
118
|
+
const statusToMarker = {
|
|
119
|
+
completed: "x",
|
|
120
|
+
in_progress: "~",
|
|
121
|
+
pending: " ",
|
|
122
|
+
};
|
|
123
|
+
const formattedTodos = [];
|
|
124
|
+
for (let i = 0; i < Math.min(todos.length, MAX_TODOS); i++) {
|
|
125
|
+
const todo = todos[i];
|
|
126
|
+
const marker = statusToMarker[todo.status] ?? " ";
|
|
127
|
+
formattedTodos.push(`[${marker}] ${todo.content}`);
|
|
128
|
+
}
|
|
129
|
+
let result = formattedTodos.join("\n");
|
|
130
|
+
if (todos.length > MAX_TODOS) {
|
|
131
|
+
result += `\n*(ะตัั ${todos.length - MAX_TODOS} ะทะฐะดะฐั)*`;
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
export function formatToolInfo(toolInfo) {
|
|
136
|
+
const { tool, input, title } = toolInfo;
|
|
137
|
+
logger.debug(`[Formatter] formatToolInfo: tool=${tool}, hasMetadata=${!!toolInfo.metadata}, hasFilediff=${!!toolInfo.metadata?.filediff}`);
|
|
138
|
+
if (tool === "todowrite" && toolInfo.metadata?.todos) {
|
|
139
|
+
const todos = toolInfo.metadata.todos;
|
|
140
|
+
const toolIcon = getToolIcon(tool);
|
|
141
|
+
const todosList = formatTodos(todos);
|
|
142
|
+
return `${toolIcon} ${tool} (${todos.length})\n${todosList}`;
|
|
143
|
+
}
|
|
144
|
+
let details = title || getToolDetails(tool, input);
|
|
145
|
+
const toolIcon = getToolIcon(tool);
|
|
146
|
+
let description = "";
|
|
147
|
+
if (input && typeof input.description === "string") {
|
|
148
|
+
description = `${input.description}\n`;
|
|
149
|
+
}
|
|
150
|
+
if (tool === "bash" && input && typeof input.command === "string") {
|
|
151
|
+
details = input.command;
|
|
152
|
+
}
|
|
153
|
+
const detailsStr = details ? ` ${details}` : "";
|
|
154
|
+
let lineInfo = "";
|
|
155
|
+
if (tool === "write" && input && "content" in input && typeof input.content === "string") {
|
|
156
|
+
const lines = countLines(input.content);
|
|
157
|
+
lineInfo = ` (+${lines})`;
|
|
158
|
+
}
|
|
159
|
+
if (tool === "edit" && toolInfo.metadata && "filediff" in toolInfo.metadata) {
|
|
160
|
+
const filediff = toolInfo.metadata.filediff;
|
|
161
|
+
logger.debug("[Formatter] Edit metadata:", JSON.stringify(toolInfo.metadata, null, 2));
|
|
162
|
+
const parts = [];
|
|
163
|
+
if (filediff.additions && filediff.additions > 0)
|
|
164
|
+
parts.push(`+${filediff.additions}`);
|
|
165
|
+
if (filediff.deletions && filediff.deletions > 0)
|
|
166
|
+
parts.push(`-${filediff.deletions}`);
|
|
167
|
+
if (parts.length > 0) {
|
|
168
|
+
lineInfo = ` (${parts.join(" ")})`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return `${toolIcon} ${description}${tool}${detailsStr}${lineInfo}`;
|
|
172
|
+
}
|
|
173
|
+
function countLines(text) {
|
|
174
|
+
return text.split("\n").length;
|
|
175
|
+
}
|
|
176
|
+
function formatDiff(diff) {
|
|
177
|
+
const lines = diff.split("\n");
|
|
178
|
+
const formattedLines = [];
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
if (line.startsWith("@@")) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (line.startsWith("---") || line.startsWith("+++")) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (line.startsWith("Index:")) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (line.startsWith("===") && line.includes("=")) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (line.startsWith("\\ No newline")) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (line.startsWith(" ")) {
|
|
196
|
+
formattedLines.push(" " + line.slice(1));
|
|
197
|
+
}
|
|
198
|
+
else if (line.startsWith("+")) {
|
|
199
|
+
formattedLines.push("+ " + line.slice(1));
|
|
200
|
+
}
|
|
201
|
+
else if (line.startsWith("-")) {
|
|
202
|
+
formattedLines.push("- " + line.slice(1));
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
formattedLines.push(line);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return formattedLines.join("\n");
|
|
209
|
+
}
|
|
210
|
+
export function prepareCodeFile(content, filePath, operation) {
|
|
211
|
+
let processedContent = content;
|
|
212
|
+
if (operation === "edit") {
|
|
213
|
+
processedContent = formatDiff(content);
|
|
214
|
+
}
|
|
215
|
+
const sizeKb = Buffer.byteLength(processedContent, "utf8") / 1024;
|
|
216
|
+
if (sizeKb > config.files.maxFileSizeKb) {
|
|
217
|
+
logger.debug(`[Formatter] File too large: ${filePath} (${sizeKb.toFixed(2)} KB > ${config.files.maxFileSizeKb} KB)`);
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
const header = `${operation === "write" ? "Write" : "Edit"} File/Path: ${filePath}\n${"=".repeat(60)}\n\n`;
|
|
221
|
+
const fullContent = header + processedContent;
|
|
222
|
+
const buffer = Buffer.from(fullContent, "utf8");
|
|
223
|
+
const basename = path.basename(filePath);
|
|
224
|
+
const filename = `${operation}_${basename}.txt`;
|
|
225
|
+
return { buffer, filename, caption: "" };
|
|
226
|
+
}
|