@grinev/opencode-telegram-bot 0.4.0 → 0.6.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 +11 -0
- package/README.md +26 -15
- package/dist/bot/commands/definitions.js +7 -4
- package/dist/bot/commands/help.js +7 -1
- package/dist/bot/commands/new.js +4 -0
- package/dist/bot/commands/projects.js +18 -9
- package/dist/bot/commands/rename.js +49 -1
- package/dist/bot/commands/sessions.js +15 -2
- package/dist/bot/commands/stop.js +3 -5
- package/dist/bot/handlers/agent.js +12 -1
- package/dist/bot/handlers/context.js +15 -25
- package/dist/bot/handlers/inline-menu.js +119 -0
- package/dist/bot/handlers/model.js +15 -2
- package/dist/bot/handlers/permission.js +81 -12
- package/dist/bot/handlers/question.js +97 -9
- package/dist/bot/handlers/variant.js +12 -1
- package/dist/bot/index.js +92 -16
- package/dist/bot/middleware/interaction-guard.js +80 -0
- package/dist/bot/middleware/unknown-command.js +22 -0
- package/dist/bot/utils/commands.js +21 -0
- package/dist/config.js +31 -0
- package/dist/i18n/en.js +23 -4
- package/dist/i18n/ru.js +23 -4
- package/dist/interaction/cleanup.js +24 -0
- package/dist/interaction/guard.js +87 -0
- package/dist/interaction/manager.js +106 -0
- package/dist/interaction/types.js +1 -0
- package/dist/permission/manager.js +60 -38
- package/dist/pinned/manager.js +7 -5
- package/dist/question/manager.js +33 -0
- package/dist/rename/manager.js +3 -0
- package/dist/settings/manager.js +6 -1
- package/dist/summary/aggregator.js +87 -6
- package/dist/summary/formatter.js +91 -15
- package/dist/summary/tool-message-batcher.js +182 -0
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import * as path from "path";
|
|
|
2
2
|
import { config } from "../config.js";
|
|
3
3
|
import { logger } from "../utils/logger.js";
|
|
4
4
|
import { t } from "../i18n/index.js";
|
|
5
|
+
import { getCurrentProject } from "../settings/manager.js";
|
|
5
6
|
const TELEGRAM_MESSAGE_LIMIT = 4096;
|
|
6
7
|
function splitText(text, maxLength) {
|
|
7
8
|
const parts = [];
|
|
@@ -21,6 +22,27 @@ function splitText(text, maxLength) {
|
|
|
21
22
|
}
|
|
22
23
|
return parts;
|
|
23
24
|
}
|
|
25
|
+
export function normalizePathForDisplay(filePath) {
|
|
26
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
27
|
+
const project = getCurrentProject();
|
|
28
|
+
if (!project?.worktree) {
|
|
29
|
+
return normalizedPath;
|
|
30
|
+
}
|
|
31
|
+
const normalizedWorktree = project.worktree.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
32
|
+
if (!normalizedWorktree) {
|
|
33
|
+
return normalizedPath;
|
|
34
|
+
}
|
|
35
|
+
const pathForCompare = process.platform === "win32" ? normalizedPath.toLowerCase() : normalizedPath;
|
|
36
|
+
const worktreeForCompare = process.platform === "win32" ? normalizedWorktree.toLowerCase() : normalizedWorktree;
|
|
37
|
+
if (pathForCompare === worktreeForCompare) {
|
|
38
|
+
return ".";
|
|
39
|
+
}
|
|
40
|
+
const worktreePrefix = `${worktreeForCompare}/`;
|
|
41
|
+
if (pathForCompare.startsWith(worktreePrefix)) {
|
|
42
|
+
return normalizedPath.slice(normalizedWorktree.length + 1);
|
|
43
|
+
}
|
|
44
|
+
return normalizedPath;
|
|
45
|
+
}
|
|
24
46
|
export function formatSummary(text) {
|
|
25
47
|
if (!text || text.trim().length === 0) {
|
|
26
48
|
return [];
|
|
@@ -50,9 +72,10 @@ function getToolDetails(tool, input) {
|
|
|
50
72
|
case "read":
|
|
51
73
|
case "edit":
|
|
52
74
|
case "write":
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
case "apply_patch":
|
|
76
|
+
const filePath = input.path || input.filePath;
|
|
77
|
+
if (typeof filePath === "string")
|
|
78
|
+
return normalizePathForDisplay(filePath);
|
|
56
79
|
break;
|
|
57
80
|
case "bash":
|
|
58
81
|
if (typeof input.command === "string")
|
|
@@ -88,6 +111,8 @@ function getToolIcon(tool) {
|
|
|
88
111
|
return "✍️";
|
|
89
112
|
case "edit":
|
|
90
113
|
return "✏️";
|
|
114
|
+
case "apply_patch":
|
|
115
|
+
return "🩹";
|
|
91
116
|
case "bash":
|
|
92
117
|
return "💻";
|
|
93
118
|
case "glob":
|
|
@@ -133,6 +158,37 @@ function formatTodos(todos) {
|
|
|
133
158
|
}
|
|
134
159
|
return result;
|
|
135
160
|
}
|
|
161
|
+
function formatDiffLineInfo(filediff) {
|
|
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
|
+
return parts.length > 0 ? ` (${parts.join(" ")})` : "";
|
|
168
|
+
}
|
|
169
|
+
function countDiffChangesFromText(text) {
|
|
170
|
+
let additions = 0;
|
|
171
|
+
let deletions = 0;
|
|
172
|
+
for (const line of text.split("\n")) {
|
|
173
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
174
|
+
additions++;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
178
|
+
deletions++;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { additions, deletions };
|
|
182
|
+
}
|
|
183
|
+
function extractFirstUpdatedFileFromTitle(title) {
|
|
184
|
+
for (const rawLine of title.split("\n")) {
|
|
185
|
+
const line = rawLine.trim();
|
|
186
|
+
if (line.length >= 3 && line[1] === " " && /[AMDURC]/.test(line[0])) {
|
|
187
|
+
return line.slice(2).trim();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return "";
|
|
191
|
+
}
|
|
136
192
|
export function formatToolInfo(toolInfo) {
|
|
137
193
|
const { tool, input, title } = toolInfo;
|
|
138
194
|
logger.debug(`[Formatter] formatToolInfo: tool=${tool}, hasMetadata=${!!toolInfo.metadata}, hasFilediff=${!!toolInfo.metadata?.filediff}`);
|
|
@@ -151,22 +207,41 @@ export function formatToolInfo(toolInfo) {
|
|
|
151
207
|
if (tool === "bash" && input && typeof input.command === "string") {
|
|
152
208
|
details = input.command;
|
|
153
209
|
}
|
|
210
|
+
if (tool === "apply_patch") {
|
|
211
|
+
const filediff = toolInfo.metadata && "filediff" in toolInfo.metadata
|
|
212
|
+
? toolInfo.metadata.filediff
|
|
213
|
+
: undefined;
|
|
214
|
+
if (filediff?.file) {
|
|
215
|
+
details = normalizePathForDisplay(filediff.file);
|
|
216
|
+
}
|
|
217
|
+
else if (title) {
|
|
218
|
+
const fileFromTitle = extractFirstUpdatedFileFromTitle(title);
|
|
219
|
+
if (fileFromTitle) {
|
|
220
|
+
details = normalizePathForDisplay(fileFromTitle);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
154
224
|
const detailsStr = details ? ` ${details}` : "";
|
|
155
225
|
let lineInfo = "";
|
|
156
226
|
if (tool === "write" && input && "content" in input && typeof input.content === "string") {
|
|
157
227
|
const lines = countLines(input.content);
|
|
158
228
|
lineInfo = ` (+${lines})`;
|
|
159
229
|
}
|
|
160
|
-
if (tool === "edit"
|
|
230
|
+
if ((tool === "edit" || tool === "apply_patch") &&
|
|
231
|
+
toolInfo.metadata &&
|
|
232
|
+
"filediff" in toolInfo.metadata) {
|
|
161
233
|
const filediff = toolInfo.metadata.filediff;
|
|
162
|
-
logger.debug("[Formatter]
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
234
|
+
logger.debug("[Formatter] Diff metadata:", JSON.stringify(toolInfo.metadata, null, 2));
|
|
235
|
+
lineInfo = formatDiffLineInfo(filediff);
|
|
236
|
+
}
|
|
237
|
+
if (tool === "apply_patch" && !lineInfo) {
|
|
238
|
+
const diffText = toolInfo.metadata && typeof toolInfo.metadata.diff === "string"
|
|
239
|
+
? toolInfo.metadata.diff
|
|
240
|
+
: input && typeof input.patchText === "string"
|
|
241
|
+
? input.patchText
|
|
242
|
+
: "";
|
|
243
|
+
if (diffText) {
|
|
244
|
+
lineInfo = formatDiffLineInfo(countDiffChangesFromText(diffText));
|
|
170
245
|
}
|
|
171
246
|
}
|
|
172
247
|
return `${toolIcon} ${description}${tool}${detailsStr}${lineInfo}`;
|
|
@@ -209,18 +284,19 @@ function formatDiff(diff) {
|
|
|
209
284
|
return formattedLines.join("\n");
|
|
210
285
|
}
|
|
211
286
|
export function prepareCodeFile(content, filePath, operation) {
|
|
287
|
+
const displayPath = normalizePathForDisplay(filePath);
|
|
212
288
|
let processedContent = content;
|
|
213
289
|
if (operation === "edit") {
|
|
214
290
|
processedContent = formatDiff(content);
|
|
215
291
|
}
|
|
216
292
|
const sizeKb = Buffer.byteLength(processedContent, "utf8") / 1024;
|
|
217
293
|
if (sizeKb > config.files.maxFileSizeKb) {
|
|
218
|
-
logger.debug(`[Formatter] File too large: ${
|
|
294
|
+
logger.debug(`[Formatter] File too large: ${displayPath} (${sizeKb.toFixed(2)} KB > ${config.files.maxFileSizeKb} KB)`);
|
|
219
295
|
return null;
|
|
220
296
|
}
|
|
221
297
|
const header = operation === "write"
|
|
222
|
-
? t("tool.file_header.write", { path:
|
|
223
|
-
: t("tool.file_header.edit", { path:
|
|
298
|
+
? t("tool.file_header.write", { path: displayPath })
|
|
299
|
+
: t("tool.file_header.edit", { path: displayPath });
|
|
224
300
|
const fullContent = header + processedContent;
|
|
225
301
|
const buffer = Buffer.from(fullContent, "utf8");
|
|
226
302
|
const basename = path.basename(filePath);
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { logger } from "../utils/logger.js";
|
|
2
|
+
const DEFAULT_INTERVAL_SECONDS = 5;
|
|
3
|
+
const TELEGRAM_MESSAGE_MAX_LENGTH = 4096;
|
|
4
|
+
function normalizeIntervalSeconds(value) {
|
|
5
|
+
if (!Number.isFinite(value)) {
|
|
6
|
+
return DEFAULT_INTERVAL_SECONDS;
|
|
7
|
+
}
|
|
8
|
+
const normalized = Math.floor(value);
|
|
9
|
+
if (normalized < 0) {
|
|
10
|
+
return DEFAULT_INTERVAL_SECONDS;
|
|
11
|
+
}
|
|
12
|
+
return normalized;
|
|
13
|
+
}
|
|
14
|
+
export class ToolMessageBatcher {
|
|
15
|
+
intervalSeconds;
|
|
16
|
+
sendMessage;
|
|
17
|
+
queues = new Map();
|
|
18
|
+
timers = new Map();
|
|
19
|
+
generation = 0;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.intervalSeconds = normalizeIntervalSeconds(options.intervalSeconds);
|
|
22
|
+
this.sendMessage = options.sendMessage;
|
|
23
|
+
}
|
|
24
|
+
setIntervalSeconds(nextIntervalSeconds) {
|
|
25
|
+
const normalized = normalizeIntervalSeconds(nextIntervalSeconds);
|
|
26
|
+
if (this.intervalSeconds === normalized) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
this.intervalSeconds = normalized;
|
|
30
|
+
logger.info(`[ToolBatcher] Interval updated: ${normalized}s`);
|
|
31
|
+
if (normalized === 0) {
|
|
32
|
+
void this.flushAll("interval_updated");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const sessionIds = Array.from(this.queues.keys());
|
|
36
|
+
for (const sessionId of sessionIds) {
|
|
37
|
+
this.restartTimer(sessionId);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
getIntervalSeconds() {
|
|
41
|
+
return this.intervalSeconds;
|
|
42
|
+
}
|
|
43
|
+
enqueue(sessionId, message) {
|
|
44
|
+
const normalizedMessage = message.trim();
|
|
45
|
+
if (!sessionId || normalizedMessage.length === 0) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (this.intervalSeconds === 0) {
|
|
49
|
+
const expectedGeneration = this.generation;
|
|
50
|
+
logger.debug(`[ToolBatcher] Sending immediate message: session=${sessionId}`);
|
|
51
|
+
void this.sendMessageSafe(sessionId, normalizedMessage, "immediate", expectedGeneration);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const queue = this.queues.get(sessionId) ?? [];
|
|
55
|
+
queue.push(normalizedMessage);
|
|
56
|
+
this.queues.set(sessionId, queue);
|
|
57
|
+
logger.debug(`[ToolBatcher] Queued message: session=${sessionId}, queueSize=${queue.length}, interval=${this.intervalSeconds}s`);
|
|
58
|
+
this.ensureTimer(sessionId);
|
|
59
|
+
}
|
|
60
|
+
async flushSession(sessionId, reason) {
|
|
61
|
+
const expectedGeneration = this.generation;
|
|
62
|
+
this.clearTimer(sessionId);
|
|
63
|
+
const queuedMessages = this.queues.get(sessionId);
|
|
64
|
+
if (!queuedMessages || queuedMessages.length === 0) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.queues.delete(sessionId);
|
|
68
|
+
const batches = this.packMessages(queuedMessages);
|
|
69
|
+
logger.debug(`[ToolBatcher] Flushing ${queuedMessages.length} tool messages as ${batches.length} Telegram messages (session=${sessionId}, reason=${reason})`);
|
|
70
|
+
for (const batchMessage of batches) {
|
|
71
|
+
await this.sendMessageSafe(sessionId, batchMessage, reason, expectedGeneration);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async flushAll(reason) {
|
|
75
|
+
for (const sessionId of Array.from(this.timers.keys())) {
|
|
76
|
+
this.clearTimer(sessionId);
|
|
77
|
+
}
|
|
78
|
+
const sessionIds = Array.from(this.queues.keys());
|
|
79
|
+
for (const sessionId of sessionIds) {
|
|
80
|
+
await this.flushSession(sessionId, reason);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
clearSession(sessionId, reason) {
|
|
84
|
+
this.generation++;
|
|
85
|
+
this.clearTimer(sessionId);
|
|
86
|
+
if (this.queues.delete(sessionId)) {
|
|
87
|
+
logger.debug(`[ToolBatcher] Cleared session queue: session=${sessionId}, reason=${reason}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
clearAll(reason) {
|
|
91
|
+
this.generation++;
|
|
92
|
+
for (const timer of this.timers.values()) {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
}
|
|
95
|
+
const queuedSessions = this.queues.size;
|
|
96
|
+
this.timers.clear();
|
|
97
|
+
this.queues.clear();
|
|
98
|
+
if (queuedSessions > 0) {
|
|
99
|
+
logger.debug(`[ToolBatcher] Cleared all queued tool messages: sessions=${queuedSessions}, reason=${reason}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
clearTimer(sessionId) {
|
|
103
|
+
const timer = this.timers.get(sessionId);
|
|
104
|
+
if (!timer) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
this.timers.delete(sessionId);
|
|
109
|
+
}
|
|
110
|
+
ensureTimer(sessionId) {
|
|
111
|
+
if (this.timers.has(sessionId)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.restartTimer(sessionId);
|
|
115
|
+
}
|
|
116
|
+
restartTimer(sessionId) {
|
|
117
|
+
this.clearTimer(sessionId);
|
|
118
|
+
const timer = setTimeout(() => {
|
|
119
|
+
this.timers.delete(sessionId);
|
|
120
|
+
void this.flushSession(sessionId, "interval_elapsed");
|
|
121
|
+
}, this.intervalSeconds * 1000);
|
|
122
|
+
this.timers.set(sessionId, timer);
|
|
123
|
+
}
|
|
124
|
+
async sendMessageSafe(sessionId, text, reason, expectedGeneration) {
|
|
125
|
+
if (this.generation !== expectedGeneration) {
|
|
126
|
+
logger.debug(`[ToolBatcher] Dropping stale tool batch message: session=${sessionId}, reason=${reason}`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
await this.sendMessage(sessionId, text);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
logger.error(`[ToolBatcher] Failed to send tool batch message: session=${sessionId}, reason=${reason}`, err);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
packMessages(messages) {
|
|
137
|
+
const normalizedEntries = messages
|
|
138
|
+
.flatMap((message) => this.splitLongText(message, TELEGRAM_MESSAGE_MAX_LENGTH))
|
|
139
|
+
.filter((entry) => entry.length > 0);
|
|
140
|
+
if (normalizedEntries.length === 0) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
const result = [];
|
|
144
|
+
let current = "";
|
|
145
|
+
for (const entry of normalizedEntries) {
|
|
146
|
+
if (!current) {
|
|
147
|
+
current = entry;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const candidate = `${current}\n\n${entry}`;
|
|
151
|
+
if (candidate.length <= TELEGRAM_MESSAGE_MAX_LENGTH) {
|
|
152
|
+
current = candidate;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
result.push(current);
|
|
156
|
+
current = entry;
|
|
157
|
+
}
|
|
158
|
+
if (current) {
|
|
159
|
+
result.push(current);
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
splitLongText(text, limit) {
|
|
164
|
+
if (text.length <= limit) {
|
|
165
|
+
return [text];
|
|
166
|
+
}
|
|
167
|
+
const chunks = [];
|
|
168
|
+
let remaining = text;
|
|
169
|
+
while (remaining.length > limit) {
|
|
170
|
+
let splitIndex = remaining.lastIndexOf("\n", limit);
|
|
171
|
+
if (splitIndex <= 0 || splitIndex < Math.floor(limit * 0.5)) {
|
|
172
|
+
splitIndex = limit;
|
|
173
|
+
}
|
|
174
|
+
chunks.push(remaining.slice(0, splitIndex));
|
|
175
|
+
remaining = remaining.slice(splitIndex).replace(/^\n+/, "");
|
|
176
|
+
}
|
|
177
|
+
if (remaining.length > 0) {
|
|
178
|
+
chunks.push(remaining);
|
|
179
|
+
}
|
|
180
|
+
return chunks;
|
|
181
|
+
}
|
|
182
|
+
}
|