ctb 1.0.0 → 1.2.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/README.md +42 -11
- package/package.json +4 -2
- package/src/__tests__/callback.test.ts +286 -0
- package/src/__tests__/cli.test.ts +377 -0
- package/src/__tests__/file-detection.test.ts +311 -0
- package/src/__tests__/session.test.ts +399 -0
- package/src/__tests__/shell-command.test.ts +310 -0
- package/src/bookmarks.ts +5 -1
- package/src/bot.ts +41 -0
- package/src/cli.ts +94 -0
- package/src/formatting.ts +289 -237
- package/src/handlers/callback.ts +46 -1
- package/src/handlers/commands.ts +417 -3
- package/src/handlers/index.ts +8 -0
- package/src/handlers/streaming.ts +185 -185
- package/src/handlers/text.ts +191 -113
- package/src/index.ts +19 -0
- package/src/session.ts +140 -6
package/src/formatting.ts
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* Escape HTML special characters.
|
|
9
9
|
*/
|
|
10
10
|
export function escapeHtml(text: string): string {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
return text
|
|
12
|
+
.replace(/&/g, "&")
|
|
13
|
+
.replace(/</g, "<")
|
|
14
|
+
.replace(/>/g, ">")
|
|
15
|
+
.replace(/"/g, """);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -22,288 +22,340 @@ export function escapeHtml(text: string): string {
|
|
|
22
22
|
* Telegram HTML supports: <b>, <i>, <code>, <pre>, <a href="">
|
|
23
23
|
*/
|
|
24
24
|
export function convertMarkdownToHtml(text: string): string {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
// Store code blocks temporarily to avoid processing their contents
|
|
26
|
+
const codeBlocks: string[] = [];
|
|
27
|
+
const inlineCodes: string[] = [];
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
// Save code blocks first (```code```)
|
|
30
|
+
text = text.replace(/```(?:\w+)?\n?([\s\S]*?)```/g, (_, code) => {
|
|
31
|
+
codeBlocks.push(code);
|
|
32
|
+
return `\x00CODEBLOCK${codeBlocks.length - 1}\x00`;
|
|
33
|
+
});
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
// Save inline code (`code`)
|
|
36
|
+
text = text.replace(/`([^`]+)`/g, (_, code) => {
|
|
37
|
+
inlineCodes.push(code);
|
|
38
|
+
return `\x00INLINECODE${inlineCodes.length - 1}\x00`;
|
|
39
|
+
});
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Escape HTML entities in the remaining text
|
|
42
|
+
text = escapeHtml(text);
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// Headers: ## Header -> <b>Header</b>
|
|
45
|
+
text = text.replace(/^#{1,6}\s+(.+)$/gm, "<b>$1</b>\n");
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
// Bold: **text** -> <b>text</b>
|
|
48
|
+
text = text.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
// Also handle *text* as bold (single asterisk)
|
|
51
|
+
text = text.replace(/(?<!\*)\*(.+?)\*(?!\*)/g, "<b>$1</b>");
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
// Double underscore: __text__ -> <b>text</b>
|
|
54
|
+
text = text.replace(/__([^_]+)__/g, "<b>$1</b>");
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
// Italic: _text_ -> <i>text</i> (but not __text__)
|
|
57
|
+
text = text.replace(/(?<!_)_([^_]+)_(?!_)/g, "<i>$1</i>");
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
// Blockquotes: > text -> <blockquote>text</blockquote>
|
|
60
|
+
text = convertBlockquotes(text);
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// Bullet lists: - item or * item -> • item
|
|
63
|
+
text = text.replace(/^[-*] /gm, "• ");
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// Horizontal rules: --- or *** -> blank line
|
|
66
|
+
text = text.replace(/^[-*]{3,}$/gm, "");
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// Links: [text](url) -> <a href="url">text</a>
|
|
69
|
+
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
// Restore code blocks
|
|
72
|
+
for (let i = 0; i < codeBlocks.length; i++) {
|
|
73
|
+
const escapedCode = escapeHtml(codeBlocks[i]!);
|
|
74
|
+
text = text.replace(`\x00CODEBLOCK${i}\x00`, `<pre>${escapedCode}</pre>`);
|
|
75
|
+
}
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
// Restore inline code
|
|
78
|
+
for (let i = 0; i < inlineCodes.length; i++) {
|
|
79
|
+
const escapedCode = escapeHtml(inlineCodes[i]!);
|
|
80
|
+
text = text.replace(
|
|
81
|
+
`\x00INLINECODE${i}\x00`,
|
|
82
|
+
`<code>${escapedCode}</code>`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
// Collapse multiple newlines
|
|
87
|
+
text = text.replace(/\n{3,}/g, "\n\n");
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
return text;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
93
|
* Convert blockquotes (handles multi-line).
|
|
94
94
|
*/
|
|
95
95
|
function convertBlockquotes(text: string): string {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return result.join("\n");
|
|
96
|
+
const lines = text.split("\n");
|
|
97
|
+
const result: string[] = [];
|
|
98
|
+
let inBlockquote = false;
|
|
99
|
+
const blockquoteLines: string[] = [];
|
|
100
|
+
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
if (line.startsWith("> ") || line === ">") {
|
|
103
|
+
if (line === ">") {
|
|
104
|
+
blockquoteLines.push("");
|
|
105
|
+
} else {
|
|
106
|
+
// Remove '> ' and strip # from hashtags (Telegram mobile bug workaround)
|
|
107
|
+
const content = line.slice(5).replace(/#/g, "");
|
|
108
|
+
blockquoteLines.push(content);
|
|
109
|
+
}
|
|
110
|
+
inBlockquote = true;
|
|
111
|
+
} else {
|
|
112
|
+
if (inBlockquote) {
|
|
113
|
+
result.push(`<blockquote>${blockquoteLines.join("\n")}</blockquote>`);
|
|
114
|
+
blockquoteLines.length = 0;
|
|
115
|
+
inBlockquote = false;
|
|
116
|
+
}
|
|
117
|
+
result.push(line);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Handle blockquote at end
|
|
122
|
+
if (inBlockquote) {
|
|
123
|
+
result.push(`<blockquote>${blockquoteLines.join("\n")}</blockquote>`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return result.join("\n");
|
|
129
127
|
}
|
|
130
128
|
|
|
131
129
|
// Legacy alias
|
|
132
130
|
export const convertMarkdownForTelegram = convertMarkdownToHtml;
|
|
133
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Detect file paths in text that could be sent as documents.
|
|
134
|
+
* Returns array of { path, display } objects.
|
|
135
|
+
* If workingDir is provided, relative paths are resolved and checked for existence.
|
|
136
|
+
*/
|
|
137
|
+
export function detectFilePaths(
|
|
138
|
+
text: string,
|
|
139
|
+
workingDir?: string,
|
|
140
|
+
): Array<{ path: string; display: string }> {
|
|
141
|
+
const paths: Array<{ path: string; display: string }> = [];
|
|
142
|
+
const seen = new Set<string>();
|
|
143
|
+
|
|
144
|
+
// Match paths in backticks, after prefixes, or standalone absolute paths
|
|
145
|
+
const patterns = [
|
|
146
|
+
// Absolute paths in backticks: `/path/to/file.ext`
|
|
147
|
+
/`(\/[^\s`]+\.[a-zA-Z0-9]+)`/g,
|
|
148
|
+
// Relative paths in backticks with extension: `name.txt`, `path/to/file.ext`
|
|
149
|
+
/`([a-zA-Z0-9_.-]+(?:\/[a-zA-Z0-9_.-]+)*\.[a-zA-Z0-9]+)`/g,
|
|
150
|
+
// Paths after common prefixes (absolute)
|
|
151
|
+
/(?:file|path|saved|created|wrote|output|generated):\s*(\/[^\s,)]+\.[a-zA-Z0-9]+)/gi,
|
|
152
|
+
// Paths after common prefixes (relative)
|
|
153
|
+
/(?:file|path|saved|created|wrote|output|generated):\s*([a-zA-Z0-9_.-]+(?:\/[a-zA-Z0-9_.-]+)*\.[a-zA-Z0-9]+)/gi,
|
|
154
|
+
// Standalone absolute paths with extensions
|
|
155
|
+
/(?:^|\s)(\/(?:Users|home|tmp|var|etc|opt)[^\s,)]+\.[a-zA-Z0-9]+)/gm,
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
for (const pattern of patterns) {
|
|
159
|
+
const matches = text.matchAll(pattern);
|
|
160
|
+
for (const match of matches) {
|
|
161
|
+
let filePath = match[1];
|
|
162
|
+
if (!filePath) continue;
|
|
163
|
+
|
|
164
|
+
// Resolve relative paths if workingDir is provided
|
|
165
|
+
if (!filePath.startsWith("/") && workingDir) {
|
|
166
|
+
filePath = `${workingDir}/${filePath}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Skip if we've already seen this path
|
|
170
|
+
if (seen.has(filePath)) continue;
|
|
171
|
+
seen.add(filePath);
|
|
172
|
+
|
|
173
|
+
// Get display name (last 2 path components)
|
|
174
|
+
const parts = filePath.split("/");
|
|
175
|
+
const display =
|
|
176
|
+
parts.length >= 2
|
|
177
|
+
? parts.slice(-2).join("/")
|
|
178
|
+
: parts[parts.length - 1] || filePath;
|
|
179
|
+
paths.push({ path: filePath, display });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return paths;
|
|
184
|
+
}
|
|
185
|
+
|
|
134
186
|
// ============== Tool Status Formatting ==============
|
|
135
187
|
|
|
136
188
|
/**
|
|
137
189
|
* Shorten a file path for display (last 2 components).
|
|
138
190
|
*/
|
|
139
191
|
function shortenPath(path: string): string {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
192
|
+
if (!path) return "file";
|
|
193
|
+
const parts = path.split("/");
|
|
194
|
+
if (parts.length >= 2) {
|
|
195
|
+
return parts.slice(-2).join("/");
|
|
196
|
+
}
|
|
197
|
+
return parts[parts.length - 1] || path;
|
|
146
198
|
}
|
|
147
199
|
|
|
148
200
|
/**
|
|
149
201
|
* Truncate text with ellipsis.
|
|
150
202
|
*/
|
|
151
203
|
function truncate(text: string, maxLen = 60): string {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
204
|
+
if (!text) return "";
|
|
205
|
+
// Clean up newlines for display
|
|
206
|
+
const cleaned = text.replace(/\n/g, " ").trim();
|
|
207
|
+
if (cleaned.length <= maxLen) return cleaned;
|
|
208
|
+
return `${cleaned.slice(0, maxLen)}...`;
|
|
157
209
|
}
|
|
158
210
|
|
|
159
211
|
/**
|
|
160
212
|
* Wrap text in HTML code tags, escaping special chars.
|
|
161
213
|
*/
|
|
162
214
|
function code(text: string): string {
|
|
163
|
-
|
|
215
|
+
return `<code>${escapeHtml(text)}</code>`;
|
|
164
216
|
}
|
|
165
217
|
|
|
166
218
|
/**
|
|
167
219
|
* Format tool use for display in Telegram with HTML formatting.
|
|
168
220
|
*/
|
|
169
221
|
export function formatToolStatus(
|
|
170
|
-
|
|
171
|
-
|
|
222
|
+
toolName: string,
|
|
223
|
+
toolInput: Record<string, unknown>,
|
|
172
224
|
): string {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
225
|
+
const emojiMap: Record<string, string> = {
|
|
226
|
+
Read: "📖",
|
|
227
|
+
Write: "📝",
|
|
228
|
+
Edit: "✏️",
|
|
229
|
+
Bash: "▶️",
|
|
230
|
+
Glob: "🔍",
|
|
231
|
+
Grep: "🔎",
|
|
232
|
+
WebSearch: "🔍",
|
|
233
|
+
WebFetch: "🌐",
|
|
234
|
+
Task: "🎯",
|
|
235
|
+
TodoWrite: "📋",
|
|
236
|
+
mcp__: "🔧",
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// Find matching emoji
|
|
240
|
+
let emoji = "🔧";
|
|
241
|
+
for (const [key, val] of Object.entries(emojiMap)) {
|
|
242
|
+
if (toolName.includes(key)) {
|
|
243
|
+
emoji = val;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Format based on tool type
|
|
249
|
+
if (toolName === "Read") {
|
|
250
|
+
const filePath = String(toolInput.file_path || "file");
|
|
251
|
+
const shortPath = shortenPath(filePath);
|
|
252
|
+
const imageExtensions = [
|
|
253
|
+
".jpg",
|
|
254
|
+
".jpeg",
|
|
255
|
+
".png",
|
|
256
|
+
".gif",
|
|
257
|
+
".webp",
|
|
258
|
+
".bmp",
|
|
259
|
+
".svg",
|
|
260
|
+
".ico",
|
|
261
|
+
];
|
|
262
|
+
if (imageExtensions.some((ext) => filePath.toLowerCase().endsWith(ext))) {
|
|
263
|
+
return "👀 Viewing";
|
|
264
|
+
}
|
|
265
|
+
return `${emoji} Reading ${code(shortPath)}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (toolName === "Write") {
|
|
269
|
+
const filePath = String(toolInput.file_path || "file");
|
|
270
|
+
return `${emoji} Writing ${code(shortenPath(filePath))}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (toolName === "Edit") {
|
|
274
|
+
const filePath = String(toolInput.file_path || "file");
|
|
275
|
+
return `${emoji} Editing ${code(shortenPath(filePath))}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (toolName === "Bash") {
|
|
279
|
+
const cmd = String(toolInput.command || "");
|
|
280
|
+
const desc = String(toolInput.description || "");
|
|
281
|
+
if (desc) {
|
|
282
|
+
return `${emoji} ${escapeHtml(desc)}`;
|
|
283
|
+
}
|
|
284
|
+
return `${emoji} ${code(truncate(cmd, 50))}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (toolName === "Grep") {
|
|
288
|
+
const pattern = String(toolInput.pattern || "");
|
|
289
|
+
const path = String(toolInput.path || "");
|
|
290
|
+
if (path) {
|
|
291
|
+
return `${emoji} Searching ${code(truncate(pattern, 30))} in ${code(
|
|
292
|
+
shortenPath(path),
|
|
293
|
+
)}`;
|
|
294
|
+
}
|
|
295
|
+
return `${emoji} Searching ${code(truncate(pattern, 40))}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (toolName === "Glob") {
|
|
299
|
+
const pattern = String(toolInput.pattern || "");
|
|
300
|
+
return `${emoji} Finding ${code(truncate(pattern, 50))}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (toolName === "WebSearch") {
|
|
304
|
+
const query = String(toolInput.query || "");
|
|
305
|
+
return `${emoji} Searching: ${escapeHtml(truncate(query, 50))}`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (toolName === "WebFetch") {
|
|
309
|
+
const url = String(toolInput.url || "");
|
|
310
|
+
return `${emoji} Fetching ${code(truncate(url, 50))}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (toolName === "Task") {
|
|
314
|
+
const desc = String(toolInput.description || "");
|
|
315
|
+
if (desc) {
|
|
316
|
+
return `${emoji} Agent: ${escapeHtml(desc)}`;
|
|
317
|
+
}
|
|
318
|
+
return `${emoji} Running agent...`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (toolName === "Skill") {
|
|
322
|
+
const skillName = String(toolInput.skill || "");
|
|
323
|
+
if (skillName) {
|
|
324
|
+
return `💭 Using skill: ${escapeHtml(skillName)}`;
|
|
325
|
+
}
|
|
326
|
+
return `💭 Using skill...`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (toolName.startsWith("mcp__")) {
|
|
330
|
+
// Generic MCP tool formatting
|
|
331
|
+
const parts = toolName.split("__");
|
|
332
|
+
if (parts.length >= 3) {
|
|
333
|
+
const server = parts[1]!;
|
|
334
|
+
let action = parts[2]!;
|
|
335
|
+
// Remove redundant server prefix from action
|
|
336
|
+
if (action.startsWith(`${server}_`)) {
|
|
337
|
+
action = action.slice(server.length + 1);
|
|
338
|
+
}
|
|
339
|
+
action = action.replace(/_/g, " ");
|
|
340
|
+
|
|
341
|
+
// Try to get meaningful summary
|
|
342
|
+
const summary =
|
|
343
|
+
toolInput.title ||
|
|
344
|
+
toolInput.query ||
|
|
345
|
+
toolInput.content ||
|
|
346
|
+
toolInput.text ||
|
|
347
|
+
toolInput.id ||
|
|
348
|
+
"";
|
|
349
|
+
|
|
350
|
+
if (summary) {
|
|
351
|
+
return `🔧 ${server} ${action}: ${escapeHtml(
|
|
352
|
+
truncate(String(summary), 40),
|
|
353
|
+
)}`;
|
|
354
|
+
}
|
|
355
|
+
return `🔧 ${server}: ${action}`;
|
|
356
|
+
}
|
|
357
|
+
return `🔧 ${escapeHtml(toolName)}`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return `${emoji} ${escapeHtml(toolName)}`;
|
|
309
361
|
}
|
package/src/handlers/callback.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Callback query handler for Claude Telegram Bot.
|
|
3
3
|
*
|
|
4
|
-
* Handles inline keyboard button presses (ask_user MCP integration, bookmarks).
|
|
4
|
+
* Handles inline keyboard button presses (ask_user MCP integration, bookmarks, file sending).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { unlinkSync } from "node:fs";
|
|
@@ -39,6 +39,12 @@ export async function handleCallback(ctx: Context): Promise<void> {
|
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// 2b. Handle file sending callbacks
|
|
43
|
+
if (callbackData.startsWith("sendfile:")) {
|
|
44
|
+
await handleSendFileCallback(ctx, callbackData);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
// 3. Parse callback data: askuser:{request_id}:{option_index}
|
|
43
49
|
if (!callbackData.startsWith("askuser:")) {
|
|
44
50
|
await ctx.answerCallbackQuery();
|
|
@@ -246,3 +252,42 @@ async function handleBookmarkCallback(
|
|
|
246
252
|
await ctx.answerCallbackQuery({ text: "Unknown action" });
|
|
247
253
|
}
|
|
248
254
|
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Handle file sending callbacks.
|
|
258
|
+
* Format: sendfile:base64encodedpath
|
|
259
|
+
*/
|
|
260
|
+
async function handleSendFileCallback(
|
|
261
|
+
ctx: Context,
|
|
262
|
+
callbackData: string,
|
|
263
|
+
): Promise<void> {
|
|
264
|
+
const { existsSync } = await import("node:fs");
|
|
265
|
+
const { basename } = await import("node:path");
|
|
266
|
+
const { InputFile } = await import("grammy");
|
|
267
|
+
|
|
268
|
+
// Decode the file path (base64 encoded to handle special chars)
|
|
269
|
+
const encodedPath = callbackData.slice("sendfile:".length);
|
|
270
|
+
let filePath: string;
|
|
271
|
+
try {
|
|
272
|
+
filePath = Buffer.from(encodedPath, "base64").toString("utf-8");
|
|
273
|
+
} catch {
|
|
274
|
+
await ctx.answerCallbackQuery({ text: "Invalid file path" });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Check file exists
|
|
279
|
+
if (!existsSync(filePath)) {
|
|
280
|
+
await ctx.answerCallbackQuery({ text: "File not found" });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Send the file
|
|
285
|
+
try {
|
|
286
|
+
await ctx.answerCallbackQuery({ text: "Sending file..." });
|
|
287
|
+
const fileName = basename(filePath);
|
|
288
|
+
await ctx.replyWithDocument(new InputFile(filePath, fileName));
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.error("Failed to send file:", error);
|
|
291
|
+
await ctx.reply(`❌ Failed to send file: ${String(error).slice(0, 100)}`);
|
|
292
|
+
}
|
|
293
|
+
}
|