@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.
Files changed (66) hide show
  1. package/.env.example +34 -0
  2. package/LICENSE +21 -0
  3. package/README.md +72 -0
  4. package/dist/agent/manager.js +92 -0
  5. package/dist/agent/types.js +26 -0
  6. package/dist/app/start-bot-app.js +26 -0
  7. package/dist/bot/commands/agent.js +16 -0
  8. package/dist/bot/commands/definitions.js +20 -0
  9. package/dist/bot/commands/help.js +7 -0
  10. package/dist/bot/commands/model.js +16 -0
  11. package/dist/bot/commands/models.js +37 -0
  12. package/dist/bot/commands/new.js +58 -0
  13. package/dist/bot/commands/opencode-start.js +87 -0
  14. package/dist/bot/commands/opencode-stop.js +46 -0
  15. package/dist/bot/commands/projects.js +104 -0
  16. package/dist/bot/commands/server-restart.js +23 -0
  17. package/dist/bot/commands/server-start.js +23 -0
  18. package/dist/bot/commands/sessions.js +240 -0
  19. package/dist/bot/commands/start.js +40 -0
  20. package/dist/bot/commands/status.js +63 -0
  21. package/dist/bot/commands/stop.js +92 -0
  22. package/dist/bot/handlers/agent.js +96 -0
  23. package/dist/bot/handlers/context.js +112 -0
  24. package/dist/bot/handlers/model.js +115 -0
  25. package/dist/bot/handlers/permission.js +158 -0
  26. package/dist/bot/handlers/question.js +294 -0
  27. package/dist/bot/handlers/variant.js +126 -0
  28. package/dist/bot/index.js +573 -0
  29. package/dist/bot/middleware/auth.js +30 -0
  30. package/dist/bot/utils/keyboard.js +66 -0
  31. package/dist/cli/args.js +97 -0
  32. package/dist/cli.js +90 -0
  33. package/dist/config.js +46 -0
  34. package/dist/index.js +26 -0
  35. package/dist/keyboard/manager.js +171 -0
  36. package/dist/keyboard/types.js +1 -0
  37. package/dist/model/manager.js +123 -0
  38. package/dist/model/types.js +26 -0
  39. package/dist/opencode/client.js +13 -0
  40. package/dist/opencode/events.js +79 -0
  41. package/dist/opencode/server.js +104 -0
  42. package/dist/permission/manager.js +78 -0
  43. package/dist/permission/types.js +1 -0
  44. package/dist/pinned/manager.js +610 -0
  45. package/dist/pinned/types.js +1 -0
  46. package/dist/pinned-message/service.js +54 -0
  47. package/dist/process/manager.js +273 -0
  48. package/dist/process/types.js +1 -0
  49. package/dist/project/manager.js +28 -0
  50. package/dist/question/manager.js +143 -0
  51. package/dist/question/types.js +1 -0
  52. package/dist/runtime/bootstrap.js +278 -0
  53. package/dist/runtime/mode.js +74 -0
  54. package/dist/runtime/paths.js +37 -0
  55. package/dist/session/manager.js +10 -0
  56. package/dist/session/state.js +24 -0
  57. package/dist/settings/manager.js +99 -0
  58. package/dist/status/formatter.js +44 -0
  59. package/dist/summary/aggregator.js +427 -0
  60. package/dist/summary/formatter.js +226 -0
  61. package/dist/utils/formatting.js +237 -0
  62. package/dist/utils/logger.js +59 -0
  63. package/dist/utils/safe-background-task.js +33 -0
  64. package/dist/variant/manager.js +103 -0
  65. package/dist/variant/types.js +1 -0
  66. 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
+ }