virtualcode 2.0.0 → 2.0.2
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/dist/index.d.ts.map +1 -1
- package/dist/index.js +422 -139
- package/dist/index.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +159 -33
- package/dist/tui.js.map +1 -1
- package/install.js +69 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
2
|
import { Telegraf } from "telegraf";
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from "node:fs";
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, chmodSync, statSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
const LINK_FILE = join(homedir(), ".config", "opencode", "telegram-links.json");
|
|
@@ -8,51 +8,136 @@ const LINK_TMP = LINK_FILE + ".tmp";
|
|
|
8
8
|
const TOKEN_FILE = join(homedir(), ".config", "opencode", "telegram-token.json");
|
|
9
9
|
const TOKEN_TMP = TOKEN_FILE + ".tmp";
|
|
10
10
|
const CONFIG_DIR = join(homedir(), ".config", "opencode");
|
|
11
|
-
const
|
|
11
|
+
const LOG_FILE = join(CONFIG_DIR, "telegram-plugin.log");
|
|
12
|
+
const LOG_FILE_OLD = LOG_FILE + ".1";
|
|
13
|
+
const MAX_LOG_SIZE = 5 * 1024 * 1024;
|
|
14
|
+
const TOKEN_REGEX = /^\d{8,12}:[\w-]{30,50}$/;
|
|
12
15
|
const MAX_LRU_SIZE = 100;
|
|
13
16
|
const PENDING_TIMEOUT_MS = 30_000;
|
|
14
17
|
const RECONNECT_DELAYS = [5_000, 10_000, 20_000, 30_000];
|
|
15
|
-
|
|
18
|
+
const MAX_TELEGRAM_INPUT = 4096;
|
|
19
|
+
const MAX_TUI_TEXT = 400;
|
|
20
|
+
const MIN_PREFIX_LEN = 4;
|
|
21
|
+
const MAX_TOKENS_PER_CHUNK = 25;
|
|
22
|
+
const MIN_HISTORY_LIMIT = 1;
|
|
23
|
+
const FILE_MODE = 0o600;
|
|
24
|
+
let logDirEnsured = false;
|
|
25
|
+
function ensureLogDir() {
|
|
26
|
+
if (logDirEnsured)
|
|
27
|
+
return;
|
|
16
28
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
logDirEnsured = true;
|
|
33
|
+
}
|
|
34
|
+
function rotateLogIfNeeded() {
|
|
35
|
+
try {
|
|
36
|
+
if (!existsSync(LOG_FILE))
|
|
37
|
+
return;
|
|
38
|
+
if (statSync(LOG_FILE).size < MAX_LOG_SIZE)
|
|
39
|
+
return;
|
|
40
|
+
if (existsSync(LOG_FILE_OLD)) {
|
|
41
|
+
try {
|
|
42
|
+
writeFileSync(LOG_FILE_OLD, "", { flag: "w" });
|
|
43
|
+
}
|
|
44
|
+
catch { }
|
|
19
45
|
}
|
|
46
|
+
try {
|
|
47
|
+
renameSync(LOG_FILE, LOG_FILE_OLD);
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
20
50
|
}
|
|
21
51
|
catch { }
|
|
22
|
-
return {};
|
|
23
52
|
}
|
|
24
|
-
|
|
53
|
+
let logWriteQueue = Promise.resolve();
|
|
54
|
+
function fileLog(level, ...args) {
|
|
55
|
+
logWriteQueue = logWriteQueue.then(() => {
|
|
56
|
+
try {
|
|
57
|
+
ensureLogDir();
|
|
58
|
+
rotateLogIfNeeded();
|
|
59
|
+
const ts = new Date().toISOString();
|
|
60
|
+
const parts = args.map((a) => {
|
|
61
|
+
if (a instanceof Error)
|
|
62
|
+
return (a.stack || a.message || String(a)).replace(/[\r\n]+/g, " ");
|
|
63
|
+
if (typeof a === "string")
|
|
64
|
+
return a.replace(/[\r\n]+/g, " ");
|
|
65
|
+
try {
|
|
66
|
+
return JSON.stringify(a).replace(/[\r\n]+/g, " ");
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return String(a);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
writeFileSync(LOG_FILE, `[${ts}] [${level}] ${parts.join(" ")}\n`, { flag: "a" });
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
}).catch(() => { });
|
|
76
|
+
}
|
|
77
|
+
function safeChmod(p) {
|
|
78
|
+
try {
|
|
79
|
+
chmodSync(p, FILE_MODE);
|
|
80
|
+
}
|
|
81
|
+
catch { }
|
|
82
|
+
}
|
|
83
|
+
function atomicWrite(path, tmp, data) {
|
|
25
84
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
26
|
-
writeFileSync(
|
|
85
|
+
writeFileSync(tmp, data);
|
|
86
|
+
safeChmod(tmp);
|
|
27
87
|
try {
|
|
28
|
-
renameSync(
|
|
88
|
+
renameSync(tmp, path);
|
|
29
89
|
}
|
|
30
90
|
catch {
|
|
31
|
-
writeFileSync(
|
|
91
|
+
writeFileSync(path, data);
|
|
92
|
+
}
|
|
93
|
+
safeChmod(path);
|
|
94
|
+
}
|
|
95
|
+
function loadLinks() {
|
|
96
|
+
try {
|
|
97
|
+
if (existsSync(LINK_FILE)) {
|
|
98
|
+
const raw = readFileSync(LINK_FILE, "utf-8");
|
|
99
|
+
const parsed = JSON.parse(raw);
|
|
100
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
101
|
+
return parsed;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
debugLog("loadLinks failed:", userError(err));
|
|
32
107
|
}
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
function persistLinks(links) {
|
|
111
|
+
atomicWrite(LINK_FILE, LINK_TMP, JSON.stringify(links, null, 2));
|
|
33
112
|
}
|
|
34
113
|
function loadSavedToken() {
|
|
35
114
|
try {
|
|
36
115
|
if (existsSync(TOKEN_FILE)) {
|
|
37
116
|
const data = JSON.parse(readFileSync(TOKEN_FILE, "utf-8"));
|
|
38
|
-
if (data?.token)
|
|
117
|
+
if (typeof data?.token === "string" && data.token.length > 0)
|
|
39
118
|
return data.token;
|
|
40
119
|
}
|
|
41
120
|
}
|
|
42
|
-
catch {
|
|
121
|
+
catch (err) {
|
|
122
|
+
debugLog("loadSavedToken failed:", userError(err));
|
|
123
|
+
}
|
|
43
124
|
return null;
|
|
44
125
|
}
|
|
45
|
-
function
|
|
46
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
47
|
-
writeFileSync(TOKEN_TMP, JSON.stringify({ token }, null, 2));
|
|
126
|
+
function clearSavedToken() {
|
|
48
127
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
catch {
|
|
52
|
-
writeFileSync(TOKEN_FILE, JSON.stringify({ token }, null, 2));
|
|
128
|
+
if (existsSync(TOKEN_FILE))
|
|
129
|
+
renameSync(TOKEN_FILE, TOKEN_FILE + ".bak");
|
|
53
130
|
}
|
|
131
|
+
catch { }
|
|
132
|
+
}
|
|
133
|
+
function saveToken(token) {
|
|
134
|
+
atomicWrite(TOKEN_FILE, TOKEN_TMP, JSON.stringify({ token }, null, 2));
|
|
54
135
|
}
|
|
55
136
|
function isValidToken(token) {
|
|
137
|
+
if (typeof token !== "string")
|
|
138
|
+
return false;
|
|
139
|
+
if (token.length > 100)
|
|
140
|
+
return false;
|
|
56
141
|
return TOKEN_REGEX.test(token);
|
|
57
142
|
}
|
|
58
143
|
function userError(err) {
|
|
@@ -67,28 +152,34 @@ function userError(err) {
|
|
|
67
152
|
}
|
|
68
153
|
const DEBUG = !!process.env.DEBUG_TELEGRAM;
|
|
69
154
|
function debugLog(...args) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
155
|
+
if (DEBUG)
|
|
156
|
+
fileLog("DEBUG", ...args);
|
|
157
|
+
}
|
|
158
|
+
function errorLog(context, err) {
|
|
159
|
+
fileLog("ERROR", context, userError(err));
|
|
160
|
+
if (DEBUG && err instanceof Error)
|
|
161
|
+
fileLog("DEBUG", err.stack);
|
|
77
162
|
}
|
|
78
163
|
function sanitizeUI(text, maxLen = 200) {
|
|
79
164
|
if (!text)
|
|
80
165
|
return "";
|
|
81
|
-
let cleaned = text
|
|
166
|
+
let cleaned = String(text)
|
|
82
167
|
.split("\n")
|
|
83
168
|
.filter((line) => !/^\s*at\s/.test(line))
|
|
84
169
|
.join(" ")
|
|
85
170
|
.trim();
|
|
86
171
|
cleaned = cleaned.replace(/\s+/g, " ");
|
|
87
172
|
if (cleaned.length > maxLen) {
|
|
88
|
-
cleaned = cleaned.slice(0, maxLen - 3).trim() + "...";
|
|
173
|
+
cleaned = cleaned.slice(0, Math.max(0, maxLen - 3)).trim() + "...";
|
|
89
174
|
}
|
|
90
175
|
return cleaned;
|
|
91
176
|
}
|
|
177
|
+
function sanitizeTUI(text, maxLen = MAX_TUI_TEXT) {
|
|
178
|
+
const s = sanitizeUI(text, maxLen);
|
|
179
|
+
if (!s)
|
|
180
|
+
return "";
|
|
181
|
+
return s.length > maxLen ? s.slice(0, maxLen - 3) + "..." : s;
|
|
182
|
+
}
|
|
92
183
|
const KNOWN_ERRORS = [
|
|
93
184
|
[/Expected a string starting with "ses"/, "Invalid session ID."],
|
|
94
185
|
[/ECONNRESET/, "Connection lost."],
|
|
@@ -99,7 +190,7 @@ const KNOWN_ERRORS = [
|
|
|
99
190
|
];
|
|
100
191
|
function handlePluginError(err, context) {
|
|
101
192
|
const msg = userError(err);
|
|
102
|
-
|
|
193
|
+
errorLog(context, err);
|
|
103
194
|
for (const [pattern, friendly] of KNOWN_ERRORS) {
|
|
104
195
|
if (pattern.test(msg))
|
|
105
196
|
return friendly;
|
|
@@ -107,6 +198,8 @@ function handlePluginError(err, context) {
|
|
|
107
198
|
return "Something went wrong.";
|
|
108
199
|
}
|
|
109
200
|
function chunkText(text, maxLen = 4000) {
|
|
201
|
+
if (!text)
|
|
202
|
+
return [""];
|
|
110
203
|
if (text.length <= maxLen)
|
|
111
204
|
return [text];
|
|
112
205
|
const chunks = [];
|
|
@@ -116,19 +209,33 @@ function chunkText(text, maxLen = 4000) {
|
|
|
116
209
|
return chunks;
|
|
117
210
|
}
|
|
118
211
|
function fmtId(id) {
|
|
212
|
+
if (typeof id !== "string")
|
|
213
|
+
return "(invalid)";
|
|
119
214
|
return id.length > 12 ? id.slice(0, 12) + "..." : id;
|
|
120
215
|
}
|
|
121
216
|
function findSessionById(id, list) {
|
|
122
|
-
|
|
217
|
+
if (typeof id !== "string" || !Array.isArray(list))
|
|
218
|
+
return { status: "not_found" };
|
|
219
|
+
const exact = list.find((s) => s && typeof s.id === "string" && s.id === id);
|
|
123
220
|
if (exact)
|
|
124
221
|
return { status: "found", session: exact };
|
|
125
|
-
|
|
222
|
+
if (id.length < MIN_PREFIX_LEN)
|
|
223
|
+
return { status: "not_found" };
|
|
224
|
+
const prefix = list.filter((s) => s && typeof s.id === "string" && s.id.startsWith(id));
|
|
126
225
|
if (prefix.length === 1)
|
|
127
226
|
return { status: "found", session: prefix[0] };
|
|
128
227
|
if (prefix.length > 1)
|
|
129
228
|
return { status: "ambiguous" };
|
|
130
229
|
return { status: "not_found" };
|
|
131
230
|
}
|
|
231
|
+
function safeList(arr) {
|
|
232
|
+
if (!Array.isArray(arr))
|
|
233
|
+
return [];
|
|
234
|
+
return arr.filter((s) => s && typeof s === "object" && typeof s.id === "string");
|
|
235
|
+
}
|
|
236
|
+
function delay(ms) {
|
|
237
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
238
|
+
}
|
|
132
239
|
const TelegramPlugin = async ({ client, directory }, options) => {
|
|
133
240
|
const config = options;
|
|
134
241
|
const allowedSet = config?.allowed_users?.length ? new Set(config.allowed_users) : null;
|
|
@@ -217,40 +324,67 @@ const TelegramPlugin = async ({ client, directory }, options) => {
|
|
|
217
324
|
return;
|
|
218
325
|
if (reconnectTimer)
|
|
219
326
|
return;
|
|
220
|
-
|
|
327
|
+
if (reconnectAttempts >= 10) {
|
|
328
|
+
debugLog("reconnect attempts exhausted; giving up until next event");
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const delayMs = RECONNECT_DELAYS[Math.min(reconnectAttempts, RECONNECT_DELAYS.length - 1)];
|
|
221
332
|
reconnectAttempts++;
|
|
222
|
-
debugLog("scheduling reconnect in",
|
|
333
|
+
debugLog("scheduling reconnect in", delayMs, "ms (attempt", reconnectAttempts + ")");
|
|
223
334
|
reconnectTimer = setTimeout(async () => {
|
|
224
335
|
reconnectTimer = null;
|
|
225
336
|
if (savedToken && !userStopped) {
|
|
226
337
|
await startBot(savedToken);
|
|
227
338
|
}
|
|
228
|
-
},
|
|
339
|
+
}, delayMs);
|
|
229
340
|
}
|
|
230
341
|
async function sendToChats(chats, text) {
|
|
231
|
-
if (!botReady)
|
|
232
|
-
return;
|
|
233
|
-
const
|
|
342
|
+
if (!botReady || !bot)
|
|
343
|
+
return false;
|
|
344
|
+
const safeText = sanitizeUI(text, 4000);
|
|
345
|
+
const chunks = chunkText(safeText, 4000);
|
|
346
|
+
let sentAny = false;
|
|
234
347
|
for (const chatId of chats) {
|
|
235
348
|
for (const chunk of chunks) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
botReady = false;
|
|
243
|
-
scheduleReconnect();
|
|
349
|
+
let attempt = 0;
|
|
350
|
+
while (attempt < 2) {
|
|
351
|
+
try {
|
|
352
|
+
await bot.telegram.sendMessage(chatId, chunk);
|
|
353
|
+
sentAny = true;
|
|
354
|
+
break;
|
|
244
355
|
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
attempt++;
|
|
358
|
+
const msg = handlePluginError(err, "sendMessage");
|
|
359
|
+
if (msg.includes("blocked")) {
|
|
360
|
+
removeLink(chatId);
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
if (msg.includes("Invalid token")) {
|
|
364
|
+
botReady = false;
|
|
365
|
+
scheduleReconnect();
|
|
366
|
+
return sentAny;
|
|
367
|
+
}
|
|
368
|
+
if (attempt >= 2) {
|
|
369
|
+
debugLog("sendMessage gave up for chat", chatId, ":", msg);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
await delay(500);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (chunks.length > MAX_TOKENS_PER_CHUNK) {
|
|
377
|
+
await delay(50);
|
|
245
378
|
}
|
|
246
379
|
}
|
|
247
380
|
}
|
|
381
|
+
return sentAny;
|
|
248
382
|
}
|
|
249
383
|
async function sendToSession(sessionId, text) {
|
|
250
384
|
const chats = sessionToChats.get(sessionId);
|
|
251
385
|
if (!chats)
|
|
252
|
-
return;
|
|
253
|
-
await sendToChats(chats, text);
|
|
386
|
+
return false;
|
|
387
|
+
return await sendToChats(chats, text) ?? false;
|
|
254
388
|
}
|
|
255
389
|
async function startBot(token) {
|
|
256
390
|
if (botStarting)
|
|
@@ -274,161 +408,250 @@ const TelegramPlugin = async ({ client, directory }, options) => {
|
|
|
274
408
|
handlePluginError(err, "bot.catch");
|
|
275
409
|
});
|
|
276
410
|
bot.command("start", async (ctx) => {
|
|
277
|
-
if (allowedSet && !allowedSet.has(ctx.from.id))
|
|
411
|
+
if (allowedSet && (!ctx.from || !allowedSet.has(ctx.from.id)))
|
|
278
412
|
return;
|
|
279
|
-
|
|
280
|
-
"
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
413
|
+
try {
|
|
414
|
+
await ctx.reply("virtualcode - OpenCode Telegram bridge\n\n" +
|
|
415
|
+
"Quick setup:\n" +
|
|
416
|
+
"1. /ls - list your sessions\n" +
|
|
417
|
+
"2. /link <ID> - bind this chat\n" +
|
|
418
|
+
"3. Send any message to talk to OpenCode\n\n" +
|
|
419
|
+
"Type /help for all commands.");
|
|
420
|
+
}
|
|
421
|
+
catch { }
|
|
285
422
|
});
|
|
286
423
|
bot.command("link", async (ctx) => {
|
|
287
|
-
if (allowedSet && !allowedSet.has(ctx.from.id))
|
|
424
|
+
if (allowedSet && (!ctx.from || !allowedSet.has(ctx.from.id)))
|
|
288
425
|
return;
|
|
289
|
-
const arg = ctx.payload.trim();
|
|
426
|
+
const arg = ctx.payload.trim().slice(0, 200);
|
|
290
427
|
if (!arg) {
|
|
291
|
-
|
|
428
|
+
try {
|
|
429
|
+
await ctx.reply("Usage: /link <sessionID>");
|
|
430
|
+
}
|
|
431
|
+
catch { }
|
|
292
432
|
return;
|
|
293
433
|
}
|
|
294
434
|
try {
|
|
295
435
|
const list = await client.session.list();
|
|
296
436
|
if (list.error || !list.data) {
|
|
297
|
-
|
|
437
|
+
try {
|
|
438
|
+
await ctx.reply("Could not load sessions.");
|
|
439
|
+
}
|
|
440
|
+
catch { }
|
|
298
441
|
return;
|
|
299
442
|
}
|
|
300
|
-
const
|
|
443
|
+
const sessions = safeList(list.data);
|
|
444
|
+
const result = findSessionById(arg, sessions);
|
|
301
445
|
if (result.status === "not_found") {
|
|
302
|
-
|
|
446
|
+
try {
|
|
447
|
+
await ctx.reply(arg.length < MIN_PREFIX_LEN ? "Prefix too short (min 4 chars)." : "Session not found.");
|
|
448
|
+
}
|
|
449
|
+
catch { }
|
|
303
450
|
return;
|
|
304
451
|
}
|
|
305
452
|
if (result.status === "ambiguous") {
|
|
306
|
-
|
|
453
|
+
try {
|
|
454
|
+
await ctx.reply("Multiple sessions match. Use the full ID from /ls.");
|
|
455
|
+
}
|
|
456
|
+
catch { }
|
|
307
457
|
return;
|
|
308
458
|
}
|
|
309
459
|
const old = links[ctx.chat.id];
|
|
310
460
|
addLink(ctx.chat.id, result.session.id);
|
|
311
461
|
if (old) {
|
|
312
|
-
|
|
462
|
+
try {
|
|
463
|
+
await ctx.reply("Switched to " + fmtId(result.session.id) + " (from " + fmtId(old) + ")");
|
|
464
|
+
}
|
|
465
|
+
catch { }
|
|
313
466
|
}
|
|
314
467
|
else {
|
|
315
|
-
|
|
468
|
+
try {
|
|
469
|
+
await ctx.reply("Linked to " + (result.session.title || fmtId(result.session.id)));
|
|
470
|
+
}
|
|
471
|
+
catch { }
|
|
316
472
|
}
|
|
317
473
|
}
|
|
318
474
|
catch (err) {
|
|
319
475
|
const msg = handlePluginError(err, "/link");
|
|
320
|
-
|
|
476
|
+
try {
|
|
477
|
+
await ctx.reply(sanitizeUI(msg));
|
|
478
|
+
}
|
|
479
|
+
catch { }
|
|
321
480
|
}
|
|
322
481
|
});
|
|
323
482
|
bot.command("unlink", async (ctx) => {
|
|
324
|
-
if (allowedSet && !allowedSet.has(ctx.from.id))
|
|
483
|
+
if (allowedSet && (!ctx.from || !allowedSet.has(ctx.from.id)))
|
|
325
484
|
return;
|
|
326
485
|
if (!links[ctx.chat.id]) {
|
|
327
|
-
|
|
486
|
+
try {
|
|
487
|
+
await ctx.reply("Not linked.");
|
|
488
|
+
}
|
|
489
|
+
catch { }
|
|
328
490
|
return;
|
|
329
491
|
}
|
|
330
492
|
removeLink(ctx.chat.id);
|
|
331
|
-
|
|
493
|
+
try {
|
|
494
|
+
await ctx.reply("Unlinked.");
|
|
495
|
+
}
|
|
496
|
+
catch { }
|
|
332
497
|
});
|
|
333
498
|
bot.command("status", async (ctx) => {
|
|
334
|
-
if (allowedSet && !allowedSet.has(ctx.from.id))
|
|
499
|
+
if (allowedSet && (!ctx.from || !allowedSet.has(ctx.from.id)))
|
|
335
500
|
return;
|
|
336
501
|
const sessionId = links[ctx.chat.id];
|
|
337
502
|
if (!sessionId) {
|
|
338
|
-
|
|
503
|
+
try {
|
|
504
|
+
await ctx.reply("Not linked. Use /link <ID>");
|
|
505
|
+
}
|
|
506
|
+
catch { }
|
|
339
507
|
return;
|
|
340
508
|
}
|
|
341
|
-
|
|
509
|
+
try {
|
|
510
|
+
await ctx.reply("Connected | Session: " + fmtId(sessionId) + " | Project: " + (directory || "none"));
|
|
511
|
+
}
|
|
512
|
+
catch { }
|
|
342
513
|
});
|
|
343
514
|
bot.command(["ls", "sessions"], async (ctx) => {
|
|
344
|
-
if (allowedSet && !allowedSet.has(ctx.from.id))
|
|
515
|
+
if (allowedSet && (!ctx.from || !allowedSet.has(ctx.from.id)))
|
|
345
516
|
return;
|
|
346
517
|
try {
|
|
347
518
|
const res = await client.session.list();
|
|
348
519
|
if (res.error || !res.data) {
|
|
349
|
-
|
|
520
|
+
try {
|
|
521
|
+
await ctx.reply("Could not load sessions.");
|
|
522
|
+
}
|
|
523
|
+
catch { }
|
|
350
524
|
return;
|
|
351
525
|
}
|
|
352
526
|
const current = links[ctx.chat.id];
|
|
353
|
-
const
|
|
354
|
-
|
|
527
|
+
const sessions = safeList(res.data);
|
|
528
|
+
const recent = sessions.slice(-20);
|
|
529
|
+
const lines = recent.map((s, i) => {
|
|
530
|
+
const num = recent.length - i;
|
|
355
531
|
const marker = s.id === current ? " *" : "";
|
|
356
|
-
const label = s.title || s.id.slice(0, 16);
|
|
532
|
+
const label = (s.title || s.id.slice(0, 16)).slice(0, 60);
|
|
357
533
|
return num + ". " + label + " -- " + s.id + marker;
|
|
358
534
|
});
|
|
359
|
-
|
|
535
|
+
try {
|
|
536
|
+
await ctx.reply("Sessions:\n" + (lines.length ? lines.join("\n") : "None"));
|
|
537
|
+
}
|
|
538
|
+
catch { }
|
|
360
539
|
}
|
|
361
540
|
catch (err) {
|
|
362
541
|
const msg = handlePluginError(err, "/ls");
|
|
363
|
-
|
|
542
|
+
try {
|
|
543
|
+
await ctx.reply(sanitizeUI(msg));
|
|
544
|
+
}
|
|
545
|
+
catch { }
|
|
364
546
|
}
|
|
365
547
|
});
|
|
366
548
|
bot.command("use", async (ctx) => {
|
|
367
|
-
if (allowedSet && !allowedSet.has(ctx.from.id))
|
|
549
|
+
if (allowedSet && (!ctx.from || !allowedSet.has(ctx.from.id)))
|
|
368
550
|
return;
|
|
369
|
-
const arg = ctx.payload.trim();
|
|
551
|
+
const arg = ctx.payload.trim().slice(0, 200);
|
|
370
552
|
if (!arg) {
|
|
371
|
-
|
|
553
|
+
try {
|
|
554
|
+
await ctx.reply("Usage: /use <number|ID>");
|
|
555
|
+
}
|
|
556
|
+
catch { }
|
|
372
557
|
return;
|
|
373
558
|
}
|
|
374
559
|
try {
|
|
375
560
|
const res = await client.session.list();
|
|
376
561
|
if (res.error || !res.data) {
|
|
377
|
-
|
|
562
|
+
try {
|
|
563
|
+
await ctx.reply("Could not load sessions.");
|
|
564
|
+
}
|
|
565
|
+
catch { }
|
|
378
566
|
return;
|
|
379
567
|
}
|
|
568
|
+
const sessions = safeList(res.data);
|
|
380
569
|
const num = parseInt(arg);
|
|
570
|
+
if (!isNaN(num) && num > 0 && num <= sessions.length) {
|
|
571
|
+
const s = sessions[sessions.length - num];
|
|
572
|
+
addLink(ctx.chat.id, s.id);
|
|
573
|
+
try {
|
|
574
|
+
await ctx.reply("Switched to " + (s.title || fmtId(s.id)));
|
|
575
|
+
}
|
|
576
|
+
catch { }
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
381
579
|
if (!isNaN(num) && num > 0) {
|
|
382
|
-
|
|
383
|
-
await ctx.reply("Only " +
|
|
384
|
-
return;
|
|
580
|
+
try {
|
|
581
|
+
await ctx.reply("Only " + sessions.length + " sessions available.");
|
|
385
582
|
}
|
|
386
|
-
|
|
387
|
-
if (!s)
|
|
388
|
-
return;
|
|
389
|
-
addLink(ctx.chat.id, s.id);
|
|
390
|
-
await ctx.reply("Switched to " + (s.title || fmtId(s.id)));
|
|
583
|
+
catch { }
|
|
391
584
|
return;
|
|
392
585
|
}
|
|
393
|
-
const result = findSessionById(arg,
|
|
586
|
+
const result = findSessionById(arg, sessions);
|
|
394
587
|
if (result.status === "not_found") {
|
|
395
|
-
|
|
588
|
+
try {
|
|
589
|
+
await ctx.reply(arg.length < MIN_PREFIX_LEN ? "Prefix too short (min 4 chars)." : "Session not found.");
|
|
590
|
+
}
|
|
591
|
+
catch { }
|
|
396
592
|
return;
|
|
397
593
|
}
|
|
398
594
|
if (result.status === "ambiguous") {
|
|
399
|
-
|
|
595
|
+
try {
|
|
596
|
+
await ctx.reply("Multiple sessions match. Use the full ID from /ls.");
|
|
597
|
+
}
|
|
598
|
+
catch { }
|
|
400
599
|
return;
|
|
401
600
|
}
|
|
402
601
|
addLink(ctx.chat.id, result.session.id);
|
|
403
|
-
|
|
602
|
+
try {
|
|
603
|
+
await ctx.reply("Switched to " + (result.session.title || fmtId(result.session.id)));
|
|
604
|
+
}
|
|
605
|
+
catch { }
|
|
404
606
|
}
|
|
405
607
|
catch (err) {
|
|
406
608
|
const msg = handlePluginError(err, "/use");
|
|
407
|
-
|
|
609
|
+
try {
|
|
610
|
+
await ctx.reply(sanitizeUI(msg));
|
|
611
|
+
}
|
|
612
|
+
catch { }
|
|
408
613
|
}
|
|
409
614
|
});
|
|
410
615
|
bot.command("history", async (ctx) => {
|
|
411
|
-
if (allowedSet && !allowedSet.has(ctx.from.id))
|
|
616
|
+
if (allowedSet && (!ctx.from || !allowedSet.has(ctx.from.id)))
|
|
412
617
|
return;
|
|
413
618
|
const sessionId = links[ctx.chat.id];
|
|
414
619
|
if (!sessionId) {
|
|
415
|
-
|
|
620
|
+
try {
|
|
621
|
+
await ctx.reply("Not linked. Use /link <ID>");
|
|
622
|
+
}
|
|
623
|
+
catch { }
|
|
416
624
|
return;
|
|
417
625
|
}
|
|
418
626
|
try {
|
|
419
|
-
const limitText = ctx.payload.trim();
|
|
420
|
-
|
|
627
|
+
const limitText = ctx.payload.trim().slice(0, 10);
|
|
628
|
+
let limit = MIN_HISTORY_LIMIT;
|
|
629
|
+
if (limitText) {
|
|
630
|
+
const parsed = parseInt(limitText);
|
|
631
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
632
|
+
limit = Math.min(parsed, 100);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
limit = 20;
|
|
637
|
+
}
|
|
421
638
|
const res = await client.session.messages({ path: { id: sessionId }, query: { directory, limit } });
|
|
422
639
|
if (res.error || !res.data || res.data.length === 0) {
|
|
423
|
-
|
|
640
|
+
try {
|
|
641
|
+
await ctx.reply("No messages found.");
|
|
642
|
+
}
|
|
643
|
+
catch { }
|
|
424
644
|
return;
|
|
425
645
|
}
|
|
426
646
|
const lines = [];
|
|
427
647
|
for (const msg of res.data) {
|
|
648
|
+
if (!msg || !msg.info)
|
|
649
|
+
continue;
|
|
428
650
|
const role = msg.info.role === "user" ? "[User]" : "[AI]";
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
.
|
|
651
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : [];
|
|
652
|
+
const text = parts
|
|
653
|
+
.filter((p) => p && p.type === "text" && !p.synthetic)
|
|
654
|
+
.map((p) => String(p.text || ""))
|
|
432
655
|
.join("\n")
|
|
433
656
|
.trim();
|
|
434
657
|
if (!text)
|
|
@@ -437,45 +660,69 @@ const TelegramPlugin = async ({ client, directory }, options) => {
|
|
|
437
660
|
lines.push(role + " " + truncated);
|
|
438
661
|
}
|
|
439
662
|
if (lines.length === 0) {
|
|
440
|
-
|
|
663
|
+
try {
|
|
664
|
+
await ctx.reply("No text messages found.");
|
|
665
|
+
}
|
|
666
|
+
catch { }
|
|
441
667
|
return;
|
|
442
668
|
}
|
|
443
669
|
const text = lines.join("\n\n");
|
|
444
670
|
for (const chunk of chunkText(text)) {
|
|
445
|
-
|
|
671
|
+
try {
|
|
672
|
+
await ctx.reply(chunk);
|
|
673
|
+
}
|
|
674
|
+
catch { }
|
|
446
675
|
}
|
|
447
676
|
}
|
|
448
677
|
catch (err) {
|
|
449
678
|
const msg = handlePluginError(err, "/history");
|
|
450
|
-
|
|
679
|
+
try {
|
|
680
|
+
await ctx.reply(sanitizeUI(msg));
|
|
681
|
+
}
|
|
682
|
+
catch { }
|
|
451
683
|
}
|
|
452
684
|
});
|
|
453
685
|
bot.command("help", async (ctx) => {
|
|
454
|
-
if (allowedSet && !allowedSet.has(ctx.from.id))
|
|
686
|
+
if (allowedSet && (!ctx.from || !allowedSet.has(ctx.from.id)))
|
|
455
687
|
return;
|
|
456
|
-
|
|
457
|
-
"/
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
688
|
+
try {
|
|
689
|
+
await ctx.reply("/link <sessionId> - Bind this chat to a session\n" +
|
|
690
|
+
"/unlink - Remove binding\n" +
|
|
691
|
+
"/status - Show connection state\n" +
|
|
692
|
+
"/ls - List recent sessions\n" +
|
|
693
|
+
"/use <number|ID> - Switch active session\n" +
|
|
694
|
+
"/history [N] - View last N messages\n" +
|
|
695
|
+
"/help - Show this help\n\n" +
|
|
696
|
+
"Any other message will be sent to the linked session.");
|
|
697
|
+
}
|
|
698
|
+
catch { }
|
|
464
699
|
});
|
|
465
700
|
bot.on(["photo", "sticker", "document", "video", "audio", "voice"], async (ctx) => {
|
|
466
|
-
if (allowedSet && !allowedSet.has(ctx.from.id))
|
|
701
|
+
if (allowedSet && (!ctx.from || !allowedSet.has(ctx.from.id)))
|
|
467
702
|
return;
|
|
468
|
-
|
|
703
|
+
try {
|
|
704
|
+
await ctx.reply("Only text messages are supported.");
|
|
705
|
+
}
|
|
706
|
+
catch { }
|
|
469
707
|
});
|
|
470
708
|
bot.on("text", async (ctx) => {
|
|
471
|
-
if (allowedSet && !allowedSet.has(ctx.from.id))
|
|
709
|
+
if (allowedSet && (!ctx.from || !allowedSet.has(ctx.from.id)))
|
|
472
710
|
return;
|
|
473
711
|
const sessionId = links[ctx.chat.id];
|
|
474
712
|
if (!sessionId) {
|
|
475
|
-
|
|
713
|
+
try {
|
|
714
|
+
await ctx.reply("Not linked. Use /link <ID>");
|
|
715
|
+
}
|
|
716
|
+
catch { }
|
|
476
717
|
return;
|
|
477
718
|
}
|
|
478
|
-
|
|
719
|
+
let working = null;
|
|
720
|
+
try {
|
|
721
|
+
working = await ctx.reply("...");
|
|
722
|
+
}
|
|
723
|
+
catch (err) {
|
|
724
|
+
debugLog("working reply failed:", userError(err));
|
|
725
|
+
}
|
|
479
726
|
setPending(sessionId, ctx.chat.id);
|
|
480
727
|
try {
|
|
481
728
|
const res = await client.session.prompt({
|
|
@@ -485,20 +732,28 @@ const TelegramPlugin = async ({ client, directory }, options) => {
|
|
|
485
732
|
},
|
|
486
733
|
query: { directory },
|
|
487
734
|
});
|
|
488
|
-
if (res
|
|
735
|
+
if (res?.error) {
|
|
489
736
|
const msg = handlePluginError(res.error, "prompt");
|
|
490
|
-
|
|
737
|
+
try {
|
|
738
|
+
await ctx.reply(sanitizeUI(msg));
|
|
739
|
+
}
|
|
740
|
+
catch { }
|
|
491
741
|
}
|
|
492
742
|
}
|
|
493
743
|
catch (err) {
|
|
494
744
|
const msg = handlePluginError(err, "prompt");
|
|
495
|
-
|
|
745
|
+
try {
|
|
746
|
+
await ctx.reply(sanitizeUI(msg));
|
|
747
|
+
}
|
|
748
|
+
catch { }
|
|
496
749
|
}
|
|
497
750
|
clearPending(sessionId);
|
|
498
|
-
|
|
499
|
-
|
|
751
|
+
if (working?.message_id) {
|
|
752
|
+
try {
|
|
753
|
+
await ctx.deleteMessage(working.message_id);
|
|
754
|
+
}
|
|
755
|
+
catch { }
|
|
500
756
|
}
|
|
501
|
-
catch { }
|
|
502
757
|
});
|
|
503
758
|
if (notifyOnReconnect) {
|
|
504
759
|
for (const chatId of Object.keys(links)) {
|
|
@@ -518,9 +773,13 @@ const TelegramPlugin = async ({ client, directory }, options) => {
|
|
|
518
773
|
}
|
|
519
774
|
botReady = true;
|
|
520
775
|
botStarting = false;
|
|
521
|
-
bot.launch().catch((err) => {
|
|
776
|
+
bot.launch().catch(async (err) => {
|
|
522
777
|
handlePluginError(err, "bot.launch");
|
|
523
778
|
botReady = false;
|
|
779
|
+
try {
|
|
780
|
+
await bot?.stop();
|
|
781
|
+
}
|
|
782
|
+
catch { }
|
|
524
783
|
scheduleReconnect();
|
|
525
784
|
});
|
|
526
785
|
}
|
|
@@ -594,13 +853,19 @@ const TelegramPlugin = async ({ client, directory }, options) => {
|
|
|
594
853
|
await startBot(saved);
|
|
595
854
|
}
|
|
596
855
|
}
|
|
856
|
+
if (!output || !Array.isArray(output.parts))
|
|
857
|
+
return;
|
|
597
858
|
for (const part of output.parts) {
|
|
598
|
-
if (part.type !== "text")
|
|
859
|
+
if (!part || part.type !== "text" || typeof part.text !== "string")
|
|
599
860
|
continue;
|
|
600
|
-
|
|
861
|
+
let text = part.text;
|
|
862
|
+
if (text.length > MAX_TELEGRAM_INPUT) {
|
|
863
|
+
text = text.slice(0, MAX_TELEGRAM_INPUT);
|
|
864
|
+
}
|
|
601
865
|
if (text.startsWith("/telegram")) {
|
|
602
|
-
const
|
|
603
|
-
|
|
866
|
+
const rawArgs = text.slice("/telegram".length).trim().slice(0, 200);
|
|
867
|
+
const cmd = rawArgs.toLowerCase();
|
|
868
|
+
if (cmd === "disconnect" || cmd === "stop") {
|
|
604
869
|
userStopped = true;
|
|
605
870
|
if (reconnectTimer) {
|
|
606
871
|
clearTimeout(reconnectTimer);
|
|
@@ -614,12 +879,11 @@ const TelegramPlugin = async ({ client, directory }, options) => {
|
|
|
614
879
|
bot = null;
|
|
615
880
|
botStarting = false;
|
|
616
881
|
savedToken = null;
|
|
617
|
-
|
|
618
|
-
saveToken("");
|
|
882
|
+
clearSavedToken();
|
|
619
883
|
part.text = "Telegram bot disconnected and token removed.";
|
|
620
884
|
return;
|
|
621
885
|
}
|
|
622
|
-
if (
|
|
886
|
+
if (cmd === "status" || cmd === "") {
|
|
623
887
|
if (botReady) {
|
|
624
888
|
part.text =
|
|
625
889
|
"Telegram bot is connected.\n" +
|
|
@@ -633,16 +897,22 @@ const TelegramPlugin = async ({ client, directory }, options) => {
|
|
|
633
897
|
"- /telegram <your_bot_token> - Connect with a token\n" +
|
|
634
898
|
"- /telegram - Open setup dialog (type /telegram in command palette)";
|
|
635
899
|
}
|
|
900
|
+
part.text = sanitizeTUI(part.text, MAX_TUI_TEXT);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const tokenMatch = rawArgs.match(/^\s*["'`]*([^\s"'`]+)["'`]*\s*$/);
|
|
904
|
+
if (!tokenMatch) {
|
|
905
|
+
part.text = sanitizeTUI("Invalid token.", MAX_TUI_TEXT);
|
|
636
906
|
return;
|
|
637
907
|
}
|
|
638
|
-
const token =
|
|
908
|
+
const token = tokenMatch[1];
|
|
639
909
|
if (!isValidToken(token)) {
|
|
640
|
-
part.text = "Invalid token.";
|
|
910
|
+
part.text = sanitizeTUI("Invalid token.", MAX_TUI_TEXT);
|
|
641
911
|
return;
|
|
642
912
|
}
|
|
643
913
|
saveToken(token);
|
|
644
914
|
await startBot(token);
|
|
645
|
-
part.text = botReady ? "Connected." : "Invalid token.";
|
|
915
|
+
part.text = sanitizeTUI(botReady ? "Connected." : "Invalid token.", MAX_TUI_TEXT);
|
|
646
916
|
return;
|
|
647
917
|
}
|
|
648
918
|
}
|
|
@@ -659,9 +929,22 @@ const TelegramPlugin = async ({ client, directory }, options) => {
|
|
|
659
929
|
sessionId: tool.schema.string().optional().describe("Target session ID (defaults to current)"),
|
|
660
930
|
},
|
|
661
931
|
async execute({ text, sessionId }, ctx) {
|
|
932
|
+
if (typeof text !== "string" || text.length === 0) {
|
|
933
|
+
return { output: "No text to send." };
|
|
934
|
+
}
|
|
935
|
+
if (!botReady) {
|
|
936
|
+
return { output: "Telegram bot is not connected." };
|
|
937
|
+
}
|
|
938
|
+
if (!ctx?.sessionID && !sessionId) {
|
|
939
|
+
return { output: "No session context." };
|
|
940
|
+
}
|
|
662
941
|
const targetId = sessionId || ctx.sessionID;
|
|
663
|
-
|
|
664
|
-
|
|
942
|
+
if (typeof targetId !== "string") {
|
|
943
|
+
return { output: "Invalid session ID." };
|
|
944
|
+
}
|
|
945
|
+
const safeText = text.length > 4000 ? text.slice(0, 4000) + "..." : text;
|
|
946
|
+
const ok = await sendToSession(targetId, safeText);
|
|
947
|
+
return { output: ok ? "Message sent to Telegram" : "No linked chats." };
|
|
665
948
|
},
|
|
666
949
|
}),
|
|
667
950
|
},
|
|
@@ -677,7 +960,7 @@ const TelegramPlugin = async ({ client, directory }, options) => {
|
|
|
677
960
|
pendingTelegram.clear();
|
|
678
961
|
lastForwardedBySession.clear();
|
|
679
962
|
try {
|
|
680
|
-
bot?.stop();
|
|
963
|
+
await bot?.stop();
|
|
681
964
|
}
|
|
682
965
|
catch { }
|
|
683
966
|
botReady = false;
|