agentp 0.7.0 → 0.9.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/AGENTS.md +45 -16
- package/CHANGELOG.md +10 -0
- package/README.md +65 -10
- package/bin/agentp +142 -28
- package/bin/ocmux +24 -6
- package/bin/tgagentp +364 -125
- package/debugging_tg_integration.md +5594 -0
- package/lib/opencode.js +55 -12
- package/package.json +1 -1
- package/tgagentp.log +2156 -0
- package/status_report.md +0 -85
package/bin/tgagentp
CHANGED
|
@@ -1,26 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
const http = require('http');
|
|
3
4
|
const https = require('https');
|
|
4
5
|
const fs = require('fs');
|
|
5
6
|
const path = require('path');
|
|
6
7
|
const { version } = require('../package.json');
|
|
7
8
|
const { sendText, listenForFinalAnswer, listSessions, createSession, updateSession, selectSession, sendToSession, sendToSessionAsync, respondToPermission, listenForSessionEvents, listAgents, listProviders } = require('../lib/opencode.js');
|
|
8
9
|
|
|
9
|
-
//
|
|
10
|
-
{
|
|
11
|
-
|
|
12
|
-
process.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const prefixed = lines.map(l => l ? `[${ts}] ${l}` : l).join('\n');
|
|
17
|
-
return origWrite(prefixed, encoding, cb);
|
|
18
|
-
};
|
|
19
|
-
}
|
|
10
|
+
// Logger: info→stdout, error→stderr (always), debug→stderr (--verbose only)
|
|
11
|
+
const log = {
|
|
12
|
+
verbose: false,
|
|
13
|
+
info(msg) { process.stdout.write(`${new Date().toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, '')} ${msg}\n`); },
|
|
14
|
+
error(msg) { process.stderr.write(`${msg}\n`); },
|
|
15
|
+
debug(msg) { if (this.verbose) process.stderr.write(`${msg}\n`); },
|
|
16
|
+
};
|
|
20
17
|
|
|
21
18
|
const STARTUP_CHAT_FILE = '/tmp/tgagentp-startup-chat';
|
|
19
|
+
const TGAGENTP_PORT = parseInt(process.env.TGAGENTP_PORT || '0', 10);
|
|
20
|
+
const PORT_FILE = '/tmp/tgagentp-port';
|
|
21
|
+
const TGAGENTP_DEBOUNCE_MS = parseInt(process.env.TGAGENTP_DEBOUNCE_MS || '5000', 10);
|
|
22
22
|
const { listServers, activateServer } = require('../lib/ocmux.js');
|
|
23
23
|
|
|
24
|
+
// Agentp gateway message queue, keyed by server URL
|
|
25
|
+
const agentpQueues = {};
|
|
26
|
+
|
|
27
|
+
function validateAgentpAuth(req) {
|
|
28
|
+
const password = process.env.OPENCODE_SERVER_PASSWORD;
|
|
29
|
+
if (!password) return true;
|
|
30
|
+
const username = process.env.OPENCODE_SERVER_USERNAME || 'opencode';
|
|
31
|
+
const expected = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64');
|
|
32
|
+
return req.headers.authorization === expected;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function flushAgentpQueue(server, token, chatId) {
|
|
36
|
+
if (!chatId) return;
|
|
37
|
+
const q = agentpQueues[server];
|
|
38
|
+
if (!q || q.messages.length === 0) return;
|
|
39
|
+
const msgs = q.messages.splice(0);
|
|
40
|
+
q.notified = false;
|
|
41
|
+
if (q.notifyTimer) { clearTimeout(q.notifyTimer); q.notifyTimer = null; }
|
|
42
|
+
sendLongMessage(token, chatId,
|
|
43
|
+
`📨 Delivering ${msgs.length} queued agentp message(s):`)
|
|
44
|
+
.then(() => {
|
|
45
|
+
for (const msg of msgs) {
|
|
46
|
+
sendLongMessage(token, chatId, msg.trim()).catch(() => {});
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.catch(() => {});
|
|
50
|
+
}
|
|
51
|
+
|
|
24
52
|
// ---------------------------------------------------------------------------
|
|
25
53
|
// Telegram Bot API helpers
|
|
26
54
|
// ---------------------------------------------------------------------------
|
|
@@ -63,9 +91,10 @@ function getUpdates(token, offset, timeout) {
|
|
|
63
91
|
});
|
|
64
92
|
}
|
|
65
93
|
|
|
66
|
-
function sendTelegramMessage(token, chatId, text, parseMode) {
|
|
94
|
+
function sendTelegramMessage(token, chatId, text, parseMode, replyTo) {
|
|
67
95
|
const params = { chat_id: chatId, text };
|
|
68
96
|
if (parseMode) params.parse_mode = parseMode;
|
|
97
|
+
if (replyTo) params.reply_to_message_id = replyTo;
|
|
69
98
|
return telegramRequest(token, 'sendMessage', params);
|
|
70
99
|
}
|
|
71
100
|
|
|
@@ -75,7 +104,7 @@ function convertMarkdownToHtml(text) {
|
|
|
75
104
|
|
|
76
105
|
return parts.map((part, i) => {
|
|
77
106
|
if (i % 2 === 1) {
|
|
78
|
-
// Code block
|
|
107
|
+
// Code block ─ strip language tag, escape entities, wrap in <pre>
|
|
79
108
|
const nl = part.indexOf('\n');
|
|
80
109
|
const inner = nl >= 0 ? part.slice(nl + 1) : part;
|
|
81
110
|
return '<pre>' + inner
|
|
@@ -173,12 +202,32 @@ function splitMessage(text, maxLength = 4096) {
|
|
|
173
202
|
// Converts Markdown to Telegram HTML formatting.
|
|
174
203
|
// Splits the raw text first, then converts each chunk independently
|
|
175
204
|
// to avoid broken HTML tags across messages.
|
|
176
|
-
async function sendLongMessage(token, chatId, text) {
|
|
205
|
+
async function sendLongMessage(token, chatId, text, replyTo) {
|
|
177
206
|
const chunks = splitMessage(text);
|
|
178
|
-
for (
|
|
179
|
-
const html = convertMarkdownToHtml(
|
|
180
|
-
await sendTelegramMessage(token, chatId, html, 'HTML');
|
|
207
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
208
|
+
const html = convertMarkdownToHtml(chunks[i]);
|
|
209
|
+
await sendTelegramMessage(token, chatId, html, 'HTML', i === 0 ? replyTo : undefined);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Build a quoted reply context from a Telegram reply_to_message.
|
|
214
|
+
// Returns empty string if the message isn't a reply or has no text.
|
|
215
|
+
// Uses a dynamic code-fence length (max consecutive backticks + 1)
|
|
216
|
+
// so nested backticks never break the structure.
|
|
217
|
+
function quoteReply(msg) {
|
|
218
|
+
const rm = msg.reply_to_message;
|
|
219
|
+
if (!rm || !rm.text) return '';
|
|
220
|
+
const replyFrom = rm.from?.username
|
|
221
|
+
? `@${rm.from.username}`
|
|
222
|
+
: [rm.from?.first_name, rm.from?.last_name].filter(Boolean).join(' ') || 'unknown';
|
|
223
|
+
const senderType = rm.from?.is_bot ? 'assistant' : 'user';
|
|
224
|
+
let maxBackticks = 0;
|
|
225
|
+
for (let i = 0, cur = 0; i < rm.text.length; i++) {
|
|
226
|
+
if (rm.text[i] === '`') { cur++; maxBackticks = Math.max(maxBackticks, cur); }
|
|
227
|
+
else cur = 0;
|
|
181
228
|
}
|
|
229
|
+
const fence = '`'.repeat(maxBackticks + 1);
|
|
230
|
+
return `In response to ${replyFrom} (${senderType}):\n\n${fence}\n${rm.text}\n${fence}\n\n`;
|
|
182
231
|
}
|
|
183
232
|
|
|
184
233
|
// ---------------------------------------------------------------------------
|
|
@@ -190,24 +239,25 @@ async function cmdHelp(token, chatId, args) {
|
|
|
190
239
|
const topic = args[0].toLowerCase();
|
|
191
240
|
if (topic === 'servers') {
|
|
192
241
|
await sendLongMessage(token, chatId,
|
|
193
|
-
'`/servers`
|
|
194
|
-
'`/servers switch <name>`
|
|
242
|
+
'`/servers` ─ list all opencode servers (ocmux sessions)\n' +
|
|
243
|
+
'`/servers switch <name>` ─ switch target server\n\n' +
|
|
195
244
|
'`<name>` can be a full path or basename of the project directory.');
|
|
196
245
|
return;
|
|
197
246
|
}
|
|
198
247
|
if (topic === 'sessions') {
|
|
199
248
|
await sendLongMessage(token, chatId,
|
|
200
|
-
'`/sessions`
|
|
201
|
-
'`/sessions switch <number-or-name>`
|
|
202
|
-
'`/sessions new [name]`
|
|
203
|
-
'`/sessions rename <name>`
|
|
249
|
+
'`/sessions` ─ list active sessions (numbered, newest first, max 50)\n' +
|
|
250
|
+
'`/sessions switch <number-or-name>` ─ send messages to a specific session\n' +
|
|
251
|
+
'`/sessions new [name]` ─ create a new session\n' +
|
|
252
|
+
'`/sessions rename <name>` ─ rename the active session\n\n' +
|
|
204
253
|
'Switch by number (from the list) or by case-insensitive partial name match.\n' +
|
|
205
254
|
'Use `/sessions` to find session numbers.');
|
|
206
255
|
return;
|
|
207
256
|
}
|
|
208
257
|
if (topic === 'shutdown') {
|
|
209
258
|
await sendLongMessage(token, chatId,
|
|
210
|
-
'`/shutdown`
|
|
259
|
+
'`/shutdown` ─ stop tgagentp process\n' +
|
|
260
|
+
'`/shutdown force` ─ force shutdown even if busy (in-flight work will be lost)\n\n' +
|
|
211
261
|
'Only available when tgagentp is started with `--dev` flag.\n' +
|
|
212
262
|
'Useful when running tgagentp in a restart loop:\n' +
|
|
213
263
|
'`while true; do tgagentp --dev; done`');
|
|
@@ -215,62 +265,74 @@ async function cmdHelp(token, chatId, args) {
|
|
|
215
265
|
}
|
|
216
266
|
if (topic === 'agents') {
|
|
217
267
|
await sendLongMessage(token, chatId,
|
|
218
|
-
'`/agents`
|
|
219
|
-
'`/agents switch <name>`
|
|
268
|
+
'`/agents` ─ list primary agents (with ▶ marker for the active one).\n' +
|
|
269
|
+
'`/agents switch <name>` ─ switch the active agent. Subsequent messages will use this agent.\n\n' +
|
|
220
270
|
'Agent switching requires an active session (`/sessions switch` or `/sessions new`).');
|
|
221
271
|
return;
|
|
222
272
|
}
|
|
223
273
|
if (topic === 'session') {
|
|
224
274
|
await sendLongMessage(token, chatId,
|
|
225
275
|
'`/session` is an alias for `/sessions`.\n\n' +
|
|
226
|
-
'`/sessions`
|
|
227
|
-
'`/sessions switch <number-or-name>`
|
|
228
|
-
'`/sessions new [name]`
|
|
229
|
-
'`/sessions rename <name>`
|
|
276
|
+
'`/sessions` ─ list active sessions\n' +
|
|
277
|
+
'`/sessions switch <number-or-name>` ─ switch to a session\n' +
|
|
278
|
+
'`/sessions new [name]` ─ create a new session\n' +
|
|
279
|
+
'`/sessions rename <name>` ─ rename the active session');
|
|
230
280
|
return;
|
|
231
281
|
}
|
|
232
282
|
if (topic === 'models') {
|
|
233
283
|
await sendLongMessage(token, chatId,
|
|
234
|
-
'`/models`
|
|
284
|
+
'`/models` ─ list all providers and their models.\n' +
|
|
235
285
|
'Shows provider name, connection status, default model, and up to 5 models per provider\n' +
|
|
236
286
|
'with status, context limit, and input/output costs.');
|
|
237
287
|
return;
|
|
238
288
|
}
|
|
239
289
|
if (topic === 'allow' || topic === 'reject' || topic === 'always') {
|
|
240
290
|
await sendLongMessage(token, chatId,
|
|
241
|
-
'`/allow`
|
|
242
|
-
'`/reject`
|
|
243
|
-
'`/always`
|
|
291
|
+
'`/allow` ─ approve a permission request once\n' +
|
|
292
|
+
'`/reject` ─ deny a permission request\n' +
|
|
293
|
+
'`/always` ─ approve and remember for the session\n\n' +
|
|
244
294
|
'These commands respond to OpenCode permission prompts forwarded by tgagentp.\n' +
|
|
245
295
|
'When OpenCode needs approval (e.g. to run a shell command), tgagentp sends a\n' +
|
|
246
296
|
'🔐 notification to the chat. Use one of these commands to answer.');
|
|
247
297
|
return;
|
|
248
298
|
}
|
|
299
|
+
if (topic === 'think') {
|
|
300
|
+
await sendLongMessage(token, chatId,
|
|
301
|
+
'`/think` ─ toggle thinking message forwarding\n' +
|
|
302
|
+
'`/think on` ─ forward model thinking to the chat in real-time\n' +
|
|
303
|
+
'`/think off` ─ stop forwarding thinking messages (default)\n' +
|
|
304
|
+
'`/think switch` ─ toggle current state\n\n' +
|
|
305
|
+
'When enabled, the model\'s internal reasoning steps are sent to the chat\n' +
|
|
306
|
+
'as they are generated, prefixed with 💭.\n' +
|
|
307
|
+
'Can also be enabled at startup with `--think` flag.');
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
249
310
|
await sendLongMessage(token, chatId,
|
|
250
|
-
'Unknown topic. Try `/help servers`, `/help sessions`, `/help session`, `/help agents`, `/help models`, `/help allow`, or just `/help`.');
|
|
311
|
+
'Unknown topic. Try `/help servers`, `/help sessions`, `/help session`, `/help agents`, `/help models`, `/help allow`, `/help think`, or just `/help`.');
|
|
251
312
|
return;
|
|
252
313
|
}
|
|
253
314
|
await sendLongMessage(token, chatId,
|
|
254
|
-
'`/help`
|
|
255
|
-
'`/help <topic>`
|
|
256
|
-
'`/status`
|
|
257
|
-
'`/cancel`
|
|
258
|
-
'`/servers`
|
|
259
|
-
'`/servers switch <name>`
|
|
260
|
-
'`/sessions`
|
|
261
|
-
'`/sessions switch <number-or-name>`
|
|
262
|
-
'`/sessions new [name]`
|
|
263
|
-
'`/sessions rename <name>`
|
|
264
|
-
'`/session`
|
|
265
|
-
'`/agents`
|
|
266
|
-
'`/agents switch <name>`
|
|
267
|
-
'`/models`
|
|
268
|
-
'`/
|
|
269
|
-
'`/
|
|
315
|
+
'`/help` ─ show this message\n' +
|
|
316
|
+
'`/help <topic>` ─ detailed help\n' +
|
|
317
|
+
'`/status` ─ show current server and session\n' +
|
|
318
|
+
'`/cancel` ─ cancel the running prompt\n' +
|
|
319
|
+
'`/servers` ─ list running opencode servers\n' +
|
|
320
|
+
'`/servers switch <name>` ─ switch to a different server\n' +
|
|
321
|
+
'`/sessions` ─ list/create/switch/rename sessions\n' +
|
|
322
|
+
'`/sessions switch <number-or-name>` ─ switch to a session\n' +
|
|
323
|
+
'`/sessions new [name]` ─ create a new session\n' +
|
|
324
|
+
'`/sessions rename <name>` ─ rename the active session\n' +
|
|
325
|
+
'`/session` ─ alias for `/sessions`\n' +
|
|
326
|
+
'`/agents` ─ list available agents\n' +
|
|
327
|
+
'`/agents switch <name>` ─ switch active agent\n' +
|
|
328
|
+
'`/models` ─ list providers and models\n' +
|
|
329
|
+
'`/think` ─ toggle thinking message forwarding\n' +
|
|
330
|
+
'`/allow` / `/reject` / `/always` ─ respond to permission requests\n' +
|
|
331
|
+
'`/shutdown` ─ stop tgagentp (only with `--dev` flag)\n\n' +
|
|
270
332
|
'Everything else is forwarded to the active server as `[Telegram] <text>`.');
|
|
271
333
|
}
|
|
272
334
|
|
|
273
|
-
async function cmdServers(token, chatId, args, setServer) {
|
|
335
|
+
async function cmdServers(token, chatId, args, setServer, setActiveSession, serverBase) {
|
|
274
336
|
if (args.length > 0 && args[0].toLowerCase() === 'switch') {
|
|
275
337
|
if (args.length < 2) {
|
|
276
338
|
await sendLongMessage(token, chatId,
|
|
@@ -463,10 +525,10 @@ async function cmdSessions(token, chatId, args, setServer, setActiveSession, ser
|
|
|
463
525
|
await sendLongMessage(token, chatId,
|
|
464
526
|
`Unknown subcommand: /sessions ${sub}\n\n` +
|
|
465
527
|
'Usage:\n' +
|
|
466
|
-
' `/sessions`
|
|
467
|
-
' `/sessions switch <number-or-name>`
|
|
468
|
-
' `/sessions new [name]`
|
|
469
|
-
' `/sessions rename <name>`
|
|
528
|
+
' `/sessions` ─ list sessions\n' +
|
|
529
|
+
' `/sessions switch <number-or-name>` ─ switch to a session\n' +
|
|
530
|
+
' `/sessions new [name]` ─ create a new session\n' +
|
|
531
|
+
' `/sessions rename <name>` ─ rename the active session');
|
|
470
532
|
return;
|
|
471
533
|
}
|
|
472
534
|
|
|
@@ -566,10 +628,14 @@ async function cmdAgents(token, chatId, args, setServer, setActiveSession, serve
|
|
|
566
628
|
}
|
|
567
629
|
const target = matches[0];
|
|
568
630
|
getState(server).activeAgentName = target.name;
|
|
569
|
-
const
|
|
570
|
-
|
|
631
|
+
const st = getState(server);
|
|
632
|
+
if (st.activeSessionId) {
|
|
633
|
+
updateSession(server, st.activeSessionId, undefined, target.name);
|
|
634
|
+
selectSession(server, st.activeSessionId);
|
|
635
|
+
}
|
|
636
|
+
const model = target.model ? ` ─ \`${target.model.providerID}/${target.model.modelID}\`` : '';
|
|
571
637
|
await sendLongMessage(token, chatId,
|
|
572
|
-
`Switched to agent: **${target.name}**${
|
|
638
|
+
`Switched to agent: **${target.name}**${model}\nSubsequent messages will use this agent.`);
|
|
573
639
|
} catch (err) {
|
|
574
640
|
await sendLongMessage(token, chatId, `Error switching agent: ${err.message}`);
|
|
575
641
|
}
|
|
@@ -582,13 +648,19 @@ async function cmdAgents(token, chatId, args, setServer, setActiveSession, serve
|
|
|
582
648
|
await sendLongMessage(token, chatId, 'No agents available.');
|
|
583
649
|
return;
|
|
584
650
|
}
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
651
|
+
const primary = agents.filter(a => a.mode === 'primary');
|
|
652
|
+
if (primary.length === 0) {
|
|
653
|
+
await sendLongMessage(token, chatId, 'No primary agents available.');
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const activeAgentName = getState(server).activeAgentName;
|
|
657
|
+
const lines = primary.map(a => {
|
|
658
|
+
const bullet = a.name === activeAgentName ? '▶ ' : ' · ';
|
|
659
|
+
const model = a.model ? ` ─ \`${a.model.providerID}/${a.model.modelID}\`` : '';
|
|
588
660
|
const desc = a.description ? `. ${a.description}` : '';
|
|
589
|
-
return '**' + a.name + '**' +
|
|
661
|
+
return bullet + '**' + a.name + '**' + model + desc;
|
|
590
662
|
});
|
|
591
|
-
await sendLongMessage(token, chatId, '
|
|
663
|
+
await sendLongMessage(token, chatId, 'Primary agents:\n' + lines.join('\n'));
|
|
592
664
|
} catch (err) {
|
|
593
665
|
await sendLongMessage(token, chatId, `Error listing agents: ${err.message}`);
|
|
594
666
|
}
|
|
@@ -634,6 +706,88 @@ async function cmdModels(token, chatId, args, setServer, setActiveSession, serve
|
|
|
634
706
|
|
|
635
707
|
const PERM_RESPONSE_HELP = ' No pending permission. Use `/status` to check the current state.';
|
|
636
708
|
|
|
709
|
+
// ---------------------------------------------------------------------------
|
|
710
|
+
// Agentp gateway ─ tiny HTTP server for agentp --tg
|
|
711
|
+
// ---------------------------------------------------------------------------
|
|
712
|
+
|
|
713
|
+
function startAgentpGateway(token, chatIdResolver, serverBaseResolver) {
|
|
714
|
+
return new Promise((resolve) => {
|
|
715
|
+
const server = http.createServer((req, res) => {
|
|
716
|
+
try {
|
|
717
|
+
if (req.method !== 'POST' || req.url !== '/send') {
|
|
718
|
+
res.writeHead(405);
|
|
719
|
+
res.end('Method Not Allowed');
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (!validateAgentpAuth(req)) {
|
|
723
|
+
res.writeHead(401);
|
|
724
|
+
res.end('Unauthorized');
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
let body = '';
|
|
728
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
729
|
+
req.on('end', () => {
|
|
730
|
+
try {
|
|
731
|
+
let data;
|
|
732
|
+
try { data = JSON.parse(body); } catch {
|
|
733
|
+
res.writeHead(400);
|
|
734
|
+
res.end('Bad Request');
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const { text, server } = data;
|
|
738
|
+
if (typeof text !== 'string' || typeof server !== 'string') {
|
|
739
|
+
res.writeHead(400);
|
|
740
|
+
res.end('Bad Request: text and server are required');
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
res.writeHead(200);
|
|
744
|
+
res.end('OK');
|
|
745
|
+
|
|
746
|
+
const chatId = chatIdResolver();
|
|
747
|
+
if (!chatId) return;
|
|
748
|
+
const activeUrl = normalizeUrl(serverBaseResolver());
|
|
749
|
+
const targetUrl = normalizeUrl(server);
|
|
750
|
+
|
|
751
|
+
if (targetUrl === activeUrl) {
|
|
752
|
+
sendLongMessage(token, chatId, text.trim()).catch(() => {});
|
|
753
|
+
} else {
|
|
754
|
+
if (!agentpQueues[server]) {
|
|
755
|
+
agentpQueues[server] = { messages: [], notifyTimer: null, notified: false };
|
|
756
|
+
}
|
|
757
|
+
const q = agentpQueues[server];
|
|
758
|
+
q.messages.push(text);
|
|
759
|
+
if (!q.notifyTimer) {
|
|
760
|
+
q.notifyTimer = setTimeout(() => {
|
|
761
|
+
q.notifyTimer = null;
|
|
762
|
+
if (q.messages.length > 0 && !q.notified) {
|
|
763
|
+
q.notified = true;
|
|
764
|
+
sendLongMessage(token, chatId,
|
|
765
|
+
`📥 ${q.messages.length} agentp message(s) queued for server \`${server}\`. Switch servers to view them.`)
|
|
766
|
+
.catch(() => {});
|
|
767
|
+
}
|
|
768
|
+
}, TGAGENTP_DEBOUNCE_MS);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
} catch (err) {
|
|
772
|
+
log.error(`Agentp gateway handler error: ${err.message}`);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
} catch (err) {
|
|
776
|
+
log.error(`Agentp gateway handler error: ${err.message}`);
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
server.on('error', (err) => {
|
|
780
|
+
log.error(`Agentp gateway server error: ${err.message}`);
|
|
781
|
+
});
|
|
782
|
+
server.listen(TGAGENTP_PORT, '127.0.0.1', () => {
|
|
783
|
+
const port = server.address().port;
|
|
784
|
+
try { fs.writeFileSync(PORT_FILE, String(port)); } catch (_) {}
|
|
785
|
+
log.info(`Agentp gateway listening on port ${port}`);
|
|
786
|
+
resolve(port);
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
637
791
|
async function cmdAllow(token, chatId, args, setServer, setActiveSession, server) {
|
|
638
792
|
const st = getState(server);
|
|
639
793
|
if (!st.pendingPermission) {
|
|
@@ -644,7 +798,7 @@ async function cmdAllow(token, chatId, args, setServer, setActiveSession, server
|
|
|
644
798
|
try {
|
|
645
799
|
await respondToPermission(p.server, p.sessionId, p.permissionId, 'once');
|
|
646
800
|
await sendLongMessage(token, chatId, '✅ Permission approved (once).');
|
|
647
|
-
|
|
801
|
+
log.debug(` → /allow: ${p.type} ${p.pattern || ''}`);
|
|
648
802
|
} catch (err) {
|
|
649
803
|
await sendLongMessage(token, chatId, `Error: ${err.message}`);
|
|
650
804
|
}
|
|
@@ -660,7 +814,7 @@ async function cmdReject(token, chatId, args, setServer, setActiveSession, serve
|
|
|
660
814
|
try {
|
|
661
815
|
await respondToPermission(p.server, p.sessionId, p.permissionId, 'reject');
|
|
662
816
|
await sendLongMessage(token, chatId, '⛔ Permission denied.');
|
|
663
|
-
|
|
817
|
+
log.debug(` → /reject: ${p.type} ${p.pattern || ''}`);
|
|
664
818
|
} catch (err) {
|
|
665
819
|
await sendLongMessage(token, chatId, `Error: ${err.message}`);
|
|
666
820
|
}
|
|
@@ -676,12 +830,30 @@ async function cmdAlways(token, chatId, args, setServer, setActiveSession, serve
|
|
|
676
830
|
try {
|
|
677
831
|
await respondToPermission(p.server, p.sessionId, p.permissionId, 'always');
|
|
678
832
|
await sendLongMessage(token, chatId, '✅ Permission approved (always).');
|
|
679
|
-
|
|
833
|
+
log.debug(` → /always: ${p.type} ${p.pattern || ''}`);
|
|
680
834
|
} catch (err) {
|
|
681
835
|
await sendLongMessage(token, chatId, `Error: ${err.message}`);
|
|
682
836
|
}
|
|
683
837
|
}
|
|
684
838
|
|
|
839
|
+
async function cmdThink(token, chatId, args, setServer, setActiveSession, server) {
|
|
840
|
+
const st = getState(server);
|
|
841
|
+
const sub = args && args.length > 0 ? args[0].toLowerCase() : 'switch';
|
|
842
|
+
if (sub === 'on') {
|
|
843
|
+
st.thinkingEnabled = true;
|
|
844
|
+
await sendLongMessage(token, chatId, '💭 Thinking messages **on** (forwarded to chat).');
|
|
845
|
+
} else if (sub === 'off') {
|
|
846
|
+
st.thinkingEnabled = false;
|
|
847
|
+
await sendLongMessage(token, chatId, '💭 Thinking messages **off**.');
|
|
848
|
+
} else if (sub === 'switch') {
|
|
849
|
+
st.thinkingEnabled = !st.thinkingEnabled;
|
|
850
|
+
await sendLongMessage(token, chatId,
|
|
851
|
+
`💭 Thinking messages ${st.thinkingEnabled ? '**on**' : '**off**'}.`);
|
|
852
|
+
} else {
|
|
853
|
+
await sendLongMessage(token, chatId, 'Usage: `/think [on|off|switch]` (default: switch).');
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
685
857
|
const COMMANDS = {
|
|
686
858
|
'help': cmdHelp,
|
|
687
859
|
'servers': cmdServers,
|
|
@@ -689,6 +861,7 @@ const COMMANDS = {
|
|
|
689
861
|
'session': cmdSessions,
|
|
690
862
|
'agents': cmdAgents,
|
|
691
863
|
'models': cmdModels,
|
|
864
|
+
'think': cmdThink,
|
|
692
865
|
'allow': cmdAllow,
|
|
693
866
|
'reject': cmdReject,
|
|
694
867
|
'always': cmdAlways,
|
|
@@ -700,7 +873,7 @@ const COMMANDS = {
|
|
|
700
873
|
|
|
701
874
|
const serverState = {};
|
|
702
875
|
function getState(url) {
|
|
703
|
-
return serverState[url] = serverState[url] || { busy: false, pendingResponse: null, activeSessionId: null, activeSessionName: null, activeAgentName: null, taskId: 0, pendingPermission: null };
|
|
876
|
+
return serverState[url] = serverState[url] || { busy: false, pendingResponse: null, activeSessionId: null, activeSessionName: null, activeAgentName: null, taskId: 0, pendingPermission: null, thinkingEnabled: false };
|
|
704
877
|
}
|
|
705
878
|
|
|
706
879
|
function sessionLabel(st) {
|
|
@@ -725,6 +898,10 @@ async function main() {
|
|
|
725
898
|
process.exit(0);
|
|
726
899
|
} else if (args[i] === '--dev') {
|
|
727
900
|
DEV_MODE = true;
|
|
901
|
+
} else if (args[i] === '--verbose') {
|
|
902
|
+
log.verbose = true;
|
|
903
|
+
} else if (args[i] === '--think') {
|
|
904
|
+
getState(serverBase).thinkingEnabled = true;
|
|
728
905
|
} else if (args[i] === '--help') {
|
|
729
906
|
console.error('Usage: tgagentp [options] [url]');
|
|
730
907
|
console.error('');
|
|
@@ -732,9 +909,11 @@ async function main() {
|
|
|
732
909
|
console.error('Incoming Telegram messages are forwarded to OpenCode; answers are sent back to the chat.');
|
|
733
910
|
console.error('');
|
|
734
911
|
console.error('Options:');
|
|
735
|
-
console.error(' --dev
|
|
736
|
-
console.error(' --version
|
|
737
|
-
console.error(' --
|
|
912
|
+
console.error(' --dev Enable development mode (adds /shutdown command)');
|
|
913
|
+
console.error(' --version Show version');
|
|
914
|
+
console.error(' --verbose Show detailed logs (errors, trace, debug) on stderr');
|
|
915
|
+
console.error(' --think Start with thinking messages enabled (default: off)');
|
|
916
|
+
console.error(' --help Show this help message');
|
|
738
917
|
console.error('');
|
|
739
918
|
console.error('Arguments:');
|
|
740
919
|
console.error(' url OpenCode TUI server URL or port number (default: 4096)');
|
|
@@ -742,10 +921,15 @@ async function main() {
|
|
|
742
921
|
console.error('');
|
|
743
922
|
console.error('Environment variables:');
|
|
744
923
|
console.error(' TELEGRAM_BOT_TOKEN Telegram bot token (required)');
|
|
745
|
-
console.error(' OPENCODE_SERVER_PASSWORD OpenCode server password (optional)');
|
|
924
|
+
console.error(' OPENCODE_SERVER_PASSWORD OpenCode server password (optional, also used for agentp gateway auth)');
|
|
746
925
|
console.error(' OPENCODE_SERVER_USERNAME OpenCode server username (optional, default: opencode)');
|
|
747
926
|
console.error(' TGAGENTP_ALLOWED_CHAT_IDS Comma-separated Telegram chat IDs (optional)');
|
|
748
927
|
console.error(' Example: "123456,-789012"');
|
|
928
|
+
console.error(' TGAGENTP_PORT Agentp gateway listen port (optional, default: random)');
|
|
929
|
+
console.error(' TGAGENTP_DEBOUNCE_MS Debounce interval for queued agentp messages (optional, default: 5000)');
|
|
930
|
+
console.error('');
|
|
931
|
+
console.error('Also starts an agentp gateway (tiny HTTP server on localhost) that');
|
|
932
|
+
console.error('accepts POST /send from agentp --tg and forwards answers to Telegram.');
|
|
749
933
|
console.error('');
|
|
750
934
|
console.error('See also: agentp, ocmux');
|
|
751
935
|
console.error('');
|
|
@@ -769,24 +953,24 @@ async function main() {
|
|
|
769
953
|
process.exit(1);
|
|
770
954
|
}
|
|
771
955
|
|
|
772
|
-
|
|
956
|
+
log.info(`tgagentp v${version} started. OpenCode server: ${serverBase}`);
|
|
773
957
|
|
|
774
958
|
if (!process.env.OPENCODE_SERVER_PASSWORD) {
|
|
775
|
-
|
|
959
|
+
log.error('🔓 Server is not password-protected! Set OPENCODE_SERVER_PASSWORD for better security.');
|
|
776
960
|
}
|
|
777
961
|
|
|
778
962
|
const allowedChatIds = parseAllowlist(process.env.TGAGENTP_ALLOWED_CHAT_IDS);
|
|
779
963
|
if (allowedChatIds) {
|
|
780
|
-
|
|
964
|
+
log.info(`Chat allowlist: ${allowedChatIds.length} chat(s) configured`);
|
|
781
965
|
} else {
|
|
782
|
-
|
|
966
|
+
log.info('Chat allowlist: not configured (all chats accepted)');
|
|
783
967
|
}
|
|
784
968
|
|
|
785
969
|
let lockedChatId = null;
|
|
786
970
|
|
|
787
971
|
getState(serverBase);
|
|
788
972
|
|
|
789
|
-
|
|
973
|
+
log.info('Discovering server state...');
|
|
790
974
|
const [discovered, agents] = await Promise.all([
|
|
791
975
|
discoverActiveSession(serverBase),
|
|
792
976
|
listAgents(serverBase).catch(() => []),
|
|
@@ -794,20 +978,29 @@ async function main() {
|
|
|
794
978
|
if (discovered) {
|
|
795
979
|
getState(serverBase).activeSessionId = discovered.id;
|
|
796
980
|
getState(serverBase).activeSessionName = discovered.name;
|
|
797
|
-
|
|
981
|
+
log.info(` → session: ${discovered.name || discovered.id}`);
|
|
798
982
|
}
|
|
799
983
|
const defaultAgent = agents.find(a => a.mode === 'primary') || agents[0];
|
|
800
984
|
if (defaultAgent) {
|
|
801
985
|
getState(serverBase).activeAgentName = defaultAgent.name;
|
|
802
|
-
|
|
986
|
+
log.info(` → agent: ${defaultAgent.name}`);
|
|
803
987
|
}
|
|
804
988
|
|
|
805
|
-
|
|
989
|
+
log.info('Waiting for Telegram messages... (Ctrl+C to stop)');
|
|
990
|
+
|
|
991
|
+
// Start agentp gateway (tiny HTTP server for agentp --tg)
|
|
992
|
+
const chatIdResolver = () => lockedChatId;
|
|
993
|
+
const serverBaseResolver = () => serverBase;
|
|
994
|
+
startAgentpGateway(token, chatIdResolver, serverBaseResolver);
|
|
806
995
|
|
|
807
996
|
const setServer = async (url) => {
|
|
808
997
|
serverBase = url;
|
|
809
998
|
getState(url);
|
|
810
|
-
|
|
999
|
+
log.info(`Switched to server: ${url}`);
|
|
1000
|
+
|
|
1001
|
+
// Flush agentp queue for the new active server
|
|
1002
|
+
flushAgentpQueue(url, token, lockedChatId);
|
|
1003
|
+
|
|
811
1004
|
const [discovered, agents] = await Promise.all([
|
|
812
1005
|
discoverActiveSession(url),
|
|
813
1006
|
listAgents(url).catch(() => []),
|
|
@@ -815,12 +1008,12 @@ async function main() {
|
|
|
815
1008
|
if (discovered) {
|
|
816
1009
|
getState(url).activeSessionId = discovered.id;
|
|
817
1010
|
getState(url).activeSessionName = discovered.name;
|
|
818
|
-
|
|
1011
|
+
log.info(` → session: ${discovered.name || discovered.id}`);
|
|
819
1012
|
}
|
|
820
1013
|
const defaultAgent = agents.find(a => a.mode === 'primary') || agents[0];
|
|
821
1014
|
if (defaultAgent) {
|
|
822
1015
|
getState(url).activeAgentName = defaultAgent.name;
|
|
823
|
-
|
|
1016
|
+
log.info(` → agent: ${defaultAgent.name}`);
|
|
824
1017
|
}
|
|
825
1018
|
const p = getState(url).pendingResponse;
|
|
826
1019
|
if (p) {
|
|
@@ -832,24 +1025,47 @@ async function main() {
|
|
|
832
1025
|
|
|
833
1026
|
const setActiveSession = (id) => {
|
|
834
1027
|
getState(serverBase).activeSessionId = id;
|
|
835
|
-
|
|
1028
|
+
log.info(`Switched to session: ${id}`);
|
|
836
1029
|
};
|
|
837
1030
|
|
|
838
|
-
async function processMessageAsync(chatId, text, url, useSession, sessionId, myTaskId) {
|
|
1031
|
+
async function processMessageAsync(chatId, text, url, useSession, sessionId, myTaskId, replyTo) {
|
|
839
1032
|
const st = getState(url);
|
|
840
1033
|
const isCancelled = () => getState(url).taskId !== myTaskId;
|
|
841
1034
|
const cancelRef = { current: null };
|
|
1035
|
+
const sessionCancelRef = { current: null };
|
|
842
1036
|
st._cancelRef = cancelRef;
|
|
1037
|
+
st._sessionReq = sessionCancelRef;
|
|
843
1038
|
try {
|
|
844
1039
|
let answer;
|
|
845
1040
|
if (useSession) {
|
|
846
|
-
//
|
|
1041
|
+
// Dual approach:
|
|
1042
|
+
// 1. SSE for permissions/thinking events (connected before sending)
|
|
1043
|
+
// 2. Synchronous sendToSession (POST /session/:id/message) for reliable answer
|
|
847
1044
|
const notifiedPerms = new Set();
|
|
848
|
-
|
|
1045
|
+
let thinkingBuf = '';
|
|
1046
|
+
let sseConnected;
|
|
1047
|
+
const onConnected = Promise.race([
|
|
1048
|
+
new Promise((resolve) => { sseConnected = resolve; }),
|
|
1049
|
+
new Promise((resolve) => setTimeout(resolve, 5000)),
|
|
1050
|
+
]);
|
|
1051
|
+
listenForSessionEvents(url, sessionId, {
|
|
1052
|
+
onText: null,
|
|
1053
|
+
onConnected: sseConnected,
|
|
1054
|
+
onThinking: (chunk) => {
|
|
1055
|
+
if (!getState(url).thinkingEnabled) { thinkingBuf = ''; return; }
|
|
1056
|
+
thinkingBuf += chunk;
|
|
1057
|
+
if (thinkingBuf.length >= 50 || chunk.endsWith('\n')) {
|
|
1058
|
+
const html = '💭 ' + thinkingBuf.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1059
|
+
sendLongMessage(token, chatId, html, replyTo)
|
|
1060
|
+
.then(() => {})
|
|
1061
|
+
.catch(() => {});
|
|
1062
|
+
thinkingBuf = '';
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
849
1065
|
onPermission: (perm) => {
|
|
850
1066
|
const st2 = getState(url);
|
|
851
1067
|
const permDump = JSON.stringify(perm);
|
|
852
|
-
|
|
1068
|
+
log.debug(` → permission_asked callback: ${permDump}`);
|
|
853
1069
|
st2.pendingPermission = { server: url, sessionId, permissionId: perm.id, type: perm.permission || 'unspecified', pattern: perm.patterns || perm.pattern, title: perm.title };
|
|
854
1070
|
if (!notifiedPerms.has(perm.id)) {
|
|
855
1071
|
notifiedPerms.add(perm.id);
|
|
@@ -861,60 +1077,64 @@ async function main() {
|
|
|
861
1077
|
+ (patternStr ? '**Target:** `' + patternStr + '`\n' : '')
|
|
862
1078
|
+ (perm.title ? '**Detail:** ' + perm.title + '\n' : '')
|
|
863
1079
|
+ 'Use `/allow` to approve once, `/always` to always allow, or `/reject` to deny.';
|
|
864
|
-
|
|
865
|
-
sendLongMessage(token, chatId, msg)
|
|
866
|
-
.then(() =>
|
|
867
|
-
.catch((err) =>
|
|
1080
|
+
log.debug(` → sending permission notification to chat ${chatId}`);
|
|
1081
|
+
sendLongMessage(token, chatId, msg, replyTo)
|
|
1082
|
+
.then(() => log.debug(` → permission notification sent OK`))
|
|
1083
|
+
.catch((err) => log.debug(` → permission notification failed: ${err.message}`));
|
|
868
1084
|
} else {
|
|
869
|
-
|
|
1085
|
+
log.debug(` → permission ${perm.id} already notified, updating pendingPermission only`);
|
|
870
1086
|
}
|
|
871
1087
|
},
|
|
872
1088
|
onPermissionReplied: () => {
|
|
873
|
-
|
|
1089
|
+
log.debug(` → permission_replied: clearing pendingPermission`);
|
|
874
1090
|
getState(url).pendingPermission = null;
|
|
875
1091
|
},
|
|
876
|
-
}, cancelRef);
|
|
877
|
-
//
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1092
|
+
}, cancelRef).catch(() => {});
|
|
1093
|
+
// Wait for SSE to connect, then send synchronously
|
|
1094
|
+
await onConnected;
|
|
1095
|
+
answer = await sendToSession(url, sessionId, `[Telegram] ${text}`, st.activeAgentName, sessionCancelRef);
|
|
1096
|
+
// Flush any remaining thinking buffer
|
|
1097
|
+
if (thinkingBuf) {
|
|
1098
|
+
const html = '💭 ' + thinkingBuf.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1099
|
+
sendLongMessage(token, chatId, html, replyTo).catch(() => {});
|
|
1100
|
+
thinkingBuf = '';
|
|
884
1101
|
}
|
|
885
|
-
//
|
|
886
|
-
|
|
1102
|
+
// Clean up SSE listener
|
|
1103
|
+
if (cancelRef.current) { cancelRef.current.destroy(); cancelRef.current = null; }
|
|
887
1104
|
} else {
|
|
888
1105
|
await sendText(url, `[Telegram] ${text}`);
|
|
889
1106
|
answer = await listenForFinalAnswer(url, null, cancelRef);
|
|
890
1107
|
}
|
|
891
1108
|
if (isCancelled()) {
|
|
892
|
-
|
|
1109
|
+
log.debug(` → cancelled, discarding response from ${url}`);
|
|
893
1110
|
st.pendingPermission = null;
|
|
894
1111
|
return;
|
|
895
1112
|
}
|
|
896
1113
|
st.pendingPermission = null;
|
|
897
1114
|
if (url === serverBase) {
|
|
898
1115
|
if (answer && answer.trim()) {
|
|
899
|
-
await sendLongMessage(token, chatId, answer.trim());
|
|
1116
|
+
await sendLongMessage(token, chatId, answer.trim(), replyTo);
|
|
900
1117
|
}
|
|
901
1118
|
} else {
|
|
902
1119
|
getState(url).pendingResponse = answer;
|
|
903
|
-
await sendLongMessage(token, chatId, `✅ Server \`${url}\` finished processing. Switch back to see the response
|
|
1120
|
+
await sendLongMessage(token, chatId, `✅ Server \`${url}\` finished processing. Switch back to see the response.`, replyTo);
|
|
904
1121
|
}
|
|
905
1122
|
} catch (err) {
|
|
1123
|
+
// Clean up SSE listener if it's still active
|
|
1124
|
+
if (cancelRef.current) { cancelRef.current.destroy(); cancelRef.current = null; }
|
|
906
1125
|
if (isCancelled()) {
|
|
907
|
-
|
|
1126
|
+
log.debug(` → cancelled, ignoring error from ${url}: ${err.message}`);
|
|
908
1127
|
st.pendingPermission = null;
|
|
909
1128
|
return;
|
|
910
1129
|
}
|
|
911
|
-
|
|
1130
|
+
log.error(`Error processing message (${url}): ${err.message}`);
|
|
912
1131
|
if (url === serverBase) {
|
|
913
|
-
try { await sendLongMessage(token, chatId, `Error: ${err.message}
|
|
1132
|
+
try { await sendLongMessage(token, chatId, `Error: ${err.message}`, replyTo); } catch (_) {}
|
|
914
1133
|
}
|
|
915
1134
|
} finally {
|
|
916
1135
|
st.pendingPermission = null;
|
|
917
1136
|
if (st._cancelRef === cancelRef) st._cancelRef = null;
|
|
1137
|
+
if (st._sessionReq === sessionCancelRef) st._sessionReq = null;
|
|
918
1138
|
if (!isCancelled()) {
|
|
919
1139
|
getState(url).busy = false;
|
|
920
1140
|
}
|
|
@@ -934,8 +1154,13 @@ async function main() {
|
|
|
934
1154
|
st._cancelRef.current.destroy();
|
|
935
1155
|
st._cancelRef.current = null;
|
|
936
1156
|
}
|
|
1157
|
+
// Abort the synchronous session request if in progress
|
|
1158
|
+
if (st._sessionReq && st._sessionReq.current) {
|
|
1159
|
+
st._sessionReq.current.destroy();
|
|
1160
|
+
st._sessionReq.current = null;
|
|
1161
|
+
}
|
|
937
1162
|
await sendLongMessage(token, chatId, '✅ Cancelled.');
|
|
938
|
-
|
|
1163
|
+
log.debug(` → /cancel on ${serverBase}`);
|
|
939
1164
|
}
|
|
940
1165
|
|
|
941
1166
|
const cmdStatus = async (token, chatId) => {
|
|
@@ -951,13 +1176,22 @@ async function main() {
|
|
|
951
1176
|
await sendLongMessage(token, chatId, lines.join('\n'));
|
|
952
1177
|
};
|
|
953
1178
|
|
|
954
|
-
const cmdShutdown = async (token, chatId) => {
|
|
955
|
-
|
|
1179
|
+
const cmdShutdown = async (token, chatId, args) => {
|
|
1180
|
+
const st = getState(serverBase);
|
|
1181
|
+
const isForce = args && args.length > 0 && args[0].toLowerCase() === 'force';
|
|
1182
|
+
if (st.busy && !isForce) {
|
|
1183
|
+
await sendLongMessage(token, chatId,
|
|
1184
|
+
'⏳ Server is busy processing a message.\n' +
|
|
1185
|
+
'Use `/shutdown force` to shut down anyway (in-flight response will be lost).');
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
const status = st.busy ? '⏳ busy (forced)' : '✅ idle';
|
|
1189
|
+
await sendLongMessage(token, chatId, `👋 Shutting down (state: ${status})...`);
|
|
956
1190
|
try {
|
|
957
1191
|
fs.writeFileSync(STARTUP_CHAT_FILE, String(chatId));
|
|
958
1192
|
} catch (_) {}
|
|
959
1193
|
try { await getUpdates(token, offset, 1); } catch (_) {}
|
|
960
|
-
|
|
1194
|
+
log.debug('/shutdown received, exiting...');
|
|
961
1195
|
process.exit(0);
|
|
962
1196
|
};
|
|
963
1197
|
|
|
@@ -978,6 +1212,7 @@ async function main() {
|
|
|
978
1212
|
lines.push('**Session:** ' + sessionLabel(st));
|
|
979
1213
|
lines.push('**Agent:** ' + (st.activeAgentName ? '`' + st.activeAgentName + '`' : '*default*'));
|
|
980
1214
|
try { await sendLongMessage(token, savedChatId, lines.join('\n')); } catch (_) {}
|
|
1215
|
+
lockedChatId = savedChatId;
|
|
981
1216
|
try { fs.unlinkSync(STARTUP_CHAT_FILE); } catch (_) {}
|
|
982
1217
|
}
|
|
983
1218
|
|
|
@@ -987,13 +1222,13 @@ async function main() {
|
|
|
987
1222
|
try {
|
|
988
1223
|
result = await getUpdates(token, offset, 30);
|
|
989
1224
|
} catch (err) {
|
|
990
|
-
|
|
1225
|
+
log.error(`Polling error: ${err.message}`);
|
|
991
1226
|
await new Promise((r) => setTimeout(r, 5000));
|
|
992
1227
|
continue;
|
|
993
1228
|
}
|
|
994
1229
|
|
|
995
1230
|
if (!result.ok) {
|
|
996
|
-
|
|
1231
|
+
log.error(`Telegram API error: ${result.description || JSON.stringify(result)}`);
|
|
997
1232
|
await new Promise((r) => setTimeout(r, 5000));
|
|
998
1233
|
continue;
|
|
999
1234
|
}
|
|
@@ -1008,6 +1243,7 @@ async function main() {
|
|
|
1008
1243
|
}
|
|
1009
1244
|
|
|
1010
1245
|
const chatId = String(msg.chat.id);
|
|
1246
|
+
const replyTo = msg.message_id;
|
|
1011
1247
|
const text = msg.text;
|
|
1012
1248
|
const from = msg.from?.username
|
|
1013
1249
|
? `@${msg.from.username}`
|
|
@@ -1015,22 +1251,25 @@ async function main() {
|
|
|
1015
1251
|
|
|
1016
1252
|
// Enforce chat allowlist if configured
|
|
1017
1253
|
if (allowedChatIds && !allowedChatIds.includes(chatId)) {
|
|
1018
|
-
|
|
1254
|
+
log.debug(`[chat ${chatId}] ${from}: blocked (not in allowlist)`);
|
|
1019
1255
|
continue;
|
|
1020
1256
|
}
|
|
1021
1257
|
|
|
1022
1258
|
// Lock to the first chat that connects (single-user guard)
|
|
1023
1259
|
if (lockedChatId === null) {
|
|
1024
1260
|
lockedChatId = chatId;
|
|
1025
|
-
|
|
1261
|
+
log.debug(`[chat ${chatId}] ${from}: locked to this chat`);
|
|
1026
1262
|
} else if (lockedChatId !== chatId) {
|
|
1027
|
-
|
|
1263
|
+
log.debug(`[chat ${chatId}] ${from}: blocked (locked to ${lockedChatId})`);
|
|
1028
1264
|
continue;
|
|
1029
1265
|
}
|
|
1030
1266
|
|
|
1031
|
-
|
|
1267
|
+
log.debug(`[chat ${chatId}] ${from}: ${text}`);
|
|
1032
1268
|
try { fs.writeFileSync(STARTUP_CHAT_FILE, String(chatId)); } catch (_) {}
|
|
1033
1269
|
|
|
1270
|
+
// Build reply context if this message is a Telegram reply
|
|
1271
|
+
const replyContext = quoteReply(msg);
|
|
1272
|
+
|
|
1034
1273
|
// Intercept /commands
|
|
1035
1274
|
if (text.startsWith('/')) {
|
|
1036
1275
|
const parts = text.slice(1).split(' ');
|
|
@@ -1041,7 +1280,7 @@ async function main() {
|
|
|
1041
1280
|
try {
|
|
1042
1281
|
await handler(token, chatId, cmdArgs, setServer, setActiveSession, serverBase);
|
|
1043
1282
|
} catch (err) {
|
|
1044
|
-
|
|
1283
|
+
log.error(`Error handling /${cmd}: ${err.message}`);
|
|
1045
1284
|
try { await sendLongMessage(token, chatId, `Error: ${err.message}`); } catch (_) {}
|
|
1046
1285
|
}
|
|
1047
1286
|
} else {
|
|
@@ -1051,7 +1290,7 @@ async function main() {
|
|
|
1051
1290
|
continue;
|
|
1052
1291
|
}
|
|
1053
1292
|
|
|
1054
|
-
// Non-command message
|
|
1293
|
+
// Non-command message ─ process in background
|
|
1055
1294
|
const st = getState(serverBase);
|
|
1056
1295
|
if (st.busy) {
|
|
1057
1296
|
await sendLongMessage(token, chatId, '⏳ Server is busy processing a previous message. Please wait for it to finish, or use /cancel to cancel it.');
|
|
@@ -1059,14 +1298,14 @@ async function main() {
|
|
|
1059
1298
|
}
|
|
1060
1299
|
st.busy = true;
|
|
1061
1300
|
const myTaskId = ++st.taskId;
|
|
1062
|
-
|
|
1063
|
-
processMessageAsync(chatId, text, serverBase, !!st.activeSessionId, st.activeSessionId, myTaskId);
|
|
1301
|
+
log.debug(` → started background processing (task ${myTaskId})`);
|
|
1302
|
+
processMessageAsync(chatId, replyContext + text, serverBase, !!st.activeSessionId, st.activeSessionId, myTaskId, replyTo);
|
|
1064
1303
|
}
|
|
1065
1304
|
}
|
|
1066
1305
|
}
|
|
1067
1306
|
|
|
1068
1307
|
process.on('SIGINT', () => {
|
|
1069
|
-
|
|
1308
|
+
log.info('tgagentp stopped.');
|
|
1070
1309
|
process.exit(0);
|
|
1071
1310
|
});
|
|
1072
1311
|
|