agentp 0.4.0 → 0.5.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 +100 -8
- package/CHANGELOG.md +2 -0
- package/README.md +67 -2
- package/bin/agentp +12 -150
- package/bin/ocmux +14 -81
- package/bin/tgagentp +899 -0
- package/lib/ocmux.js +156 -0
- package/lib/opencode.js +270 -0
- package/package.json +3 -2
package/bin/tgagentp
ADDED
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { version } = require('../package.json');
|
|
7
|
+
const { sendText, listenForFinalAnswer, listSessions, createSession, updateSession, selectSession, sendToSession, listAgents, listProviders } = require('../lib/opencode.js');
|
|
8
|
+
|
|
9
|
+
const STARTUP_CHAT_FILE = '/tmp/tgagentp-startup-chat';
|
|
10
|
+
const { listServers, activateServer } = require('../lib/ocmux.js');
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Telegram Bot API helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
function telegramRequest(token, method, params) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const body = JSON.stringify(params || {});
|
|
19
|
+
const options = {
|
|
20
|
+
hostname: 'api.telegram.org',
|
|
21
|
+
port: 443,
|
|
22
|
+
path: `/bot${token}/${method}`,
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: {
|
|
25
|
+
'Content-Type': 'application/json',
|
|
26
|
+
'Content-Length': Buffer.byteLength(body),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const req = https.request(options, (res) => {
|
|
30
|
+
let data = '';
|
|
31
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
32
|
+
res.on('end', () => {
|
|
33
|
+
try {
|
|
34
|
+
resolve(JSON.parse(data));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
reject(new Error(`Failed to parse Telegram response: ${data}`));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
req.on('error', reject);
|
|
41
|
+
req.write(body);
|
|
42
|
+
req.end();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getUpdates(token, offset, timeout) {
|
|
47
|
+
return telegramRequest(token, 'getUpdates', {
|
|
48
|
+
offset,
|
|
49
|
+
timeout: timeout != null ? timeout : 30,
|
|
50
|
+
allowed_updates: ['message'],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sendTelegramMessage(token, chatId, text, parseMode) {
|
|
55
|
+
const params = { chat_id: chatId, text };
|
|
56
|
+
if (parseMode) params.parse_mode = parseMode;
|
|
57
|
+
return telegramRequest(token, 'sendMessage', params);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Lightweight Markdown to Telegram HTML conversion.
|
|
61
|
+
function convertMarkdownToHtml(text) {
|
|
62
|
+
const parts = text.split('```');
|
|
63
|
+
|
|
64
|
+
return parts.map((part, i) => {
|
|
65
|
+
if (i % 2 === 1) {
|
|
66
|
+
// Code block — strip language tag, escape entities, wrap in <pre>
|
|
67
|
+
const nl = part.indexOf('\n');
|
|
68
|
+
const inner = nl >= 0 ? part.slice(nl + 1) : part;
|
|
69
|
+
return '<pre>' + inner
|
|
70
|
+
.replace(/&/g, '&')
|
|
71
|
+
.replace(/</g, '<')
|
|
72
|
+
.replace(/>/g, '>') + '</pre>';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Text segment
|
|
76
|
+
|
|
77
|
+
// Phase 1: extract table blocks into placeholders
|
|
78
|
+
const placeholders = [];
|
|
79
|
+
let processed = '';
|
|
80
|
+
const lines = part.split('\n');
|
|
81
|
+
let li = 0;
|
|
82
|
+
while (li < lines.length) {
|
|
83
|
+
if (/^\|/.test(lines[li])) {
|
|
84
|
+
const table = [];
|
|
85
|
+
while (li < lines.length && /^\|/.test(lines[li])) {
|
|
86
|
+
table.push(lines[li]);
|
|
87
|
+
li++;
|
|
88
|
+
}
|
|
89
|
+
const hasSep = table.some(l => /^\|[-:| ]+\|$/.test(l));
|
|
90
|
+
if (table.length >= 2 && hasSep) {
|
|
91
|
+
const cleaned = table
|
|
92
|
+
.map(l => l.replace(/^\|/, '').replace(/\|$/, ''))
|
|
93
|
+
.filter(l => !/^\s*[|:\- ]+\s*$/.test(l));
|
|
94
|
+
if (cleaned.length > 0) {
|
|
95
|
+
const escaped = cleaned.map(l =>
|
|
96
|
+
l.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
97
|
+
);
|
|
98
|
+
const idx = placeholders.length;
|
|
99
|
+
placeholders.push('<pre>' + escaped.join('\n') + '</pre>');
|
|
100
|
+
processed += '\0T' + idx + '\0\n';
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const l of table) { processed += l + '\n'; }
|
|
105
|
+
} else {
|
|
106
|
+
processed += lines[li] + '\n';
|
|
107
|
+
li++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Phase 2: escape entities and apply markdown on non-table text
|
|
112
|
+
let html = processed
|
|
113
|
+
.replace(/&/g, '&')
|
|
114
|
+
.replace(/</g, '<')
|
|
115
|
+
.replace(/>/g, '>');
|
|
116
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
117
|
+
html = html.replace(/^#{1,6}\s+(.+)$/gm, '<b>$1</b>');
|
|
118
|
+
html = html.replace(/^\s*-{3,}\s*$/gm, '────────────────────');
|
|
119
|
+
html = html.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
|
|
120
|
+
html = html.replace(/\*(.+?)\*/g, '<i>$1</i>');
|
|
121
|
+
|
|
122
|
+
// Phase 3: restore table placeholders
|
|
123
|
+
for (let j = 0; j < placeholders.length; j++) {
|
|
124
|
+
html = html.split('\0T' + j + '\0').join(placeholders[j]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return html;
|
|
128
|
+
}).join('');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Parse TGAGENTP_ALLOWED_CHAT_IDS env var (comma-separated chat IDs).
|
|
132
|
+
// Returns null when unset (all chats allowed), or an array of IDs to filter.
|
|
133
|
+
function parseAllowlist(raw) {
|
|
134
|
+
if (!raw || !raw.trim()) return null;
|
|
135
|
+
const ids = raw.split(',').map(s => s.trim()).filter(Boolean);
|
|
136
|
+
return ids.length > 0 ? ids : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Split text into chunks of at most maxLength characters,
|
|
140
|
+
// breaking at newlines when possible.
|
|
141
|
+
function splitMessage(text, maxLength = 4096) {
|
|
142
|
+
if (text.length <= maxLength) return [text];
|
|
143
|
+
const chunks = [];
|
|
144
|
+
let start = 0;
|
|
145
|
+
while (start < text.length) {
|
|
146
|
+
let end = Math.min(start + maxLength, text.length);
|
|
147
|
+
if (end < text.length) {
|
|
148
|
+
const newlinePos = text.lastIndexOf('\n', end);
|
|
149
|
+
if (newlinePos > start) {
|
|
150
|
+
end = newlinePos + 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
chunks.push(text.slice(start, end));
|
|
154
|
+
start = end;
|
|
155
|
+
}
|
|
156
|
+
return chunks;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Send a text message to a Telegram chat, splitting into multiple
|
|
160
|
+
// messages if it exceeds the 4096-character limit.
|
|
161
|
+
// Converts Markdown to Telegram HTML formatting.
|
|
162
|
+
// Splits the raw text first, then converts each chunk independently
|
|
163
|
+
// to avoid broken HTML tags across messages.
|
|
164
|
+
async function sendLongMessage(token, chatId, text) {
|
|
165
|
+
const chunks = splitMessage(text);
|
|
166
|
+
for (const chunk of chunks) {
|
|
167
|
+
const html = convertMarkdownToHtml(chunk);
|
|
168
|
+
await sendTelegramMessage(token, chatId, html, 'HTML');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Telegram command handlers
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
async function cmdHelp(token, chatId, args) {
|
|
177
|
+
if (args.length > 0) {
|
|
178
|
+
const topic = args[0].toLowerCase();
|
|
179
|
+
if (topic === 'servers') {
|
|
180
|
+
await sendLongMessage(token, chatId,
|
|
181
|
+
'`/servers` — list all opencode servers (ocmux sessions)\n' +
|
|
182
|
+
'`/servers switch <name>` — switch target server\n\n' +
|
|
183
|
+
'`<name>` can be a full path or basename of the project directory.');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (topic === 'sessions') {
|
|
187
|
+
await sendLongMessage(token, chatId,
|
|
188
|
+
'`/sessions` — list active sessions (numbered, newest first, max 50)\n' +
|
|
189
|
+
'`/sessions switch <number-or-name>` — send messages to a specific session\n' +
|
|
190
|
+
'`/sessions new [name]` — create a new session\n' +
|
|
191
|
+
'`/sessions rename <name>` — rename the active session\n\n' +
|
|
192
|
+
'Switch by number (from the list) or by case-insensitive partial name match.\n' +
|
|
193
|
+
'Use `/sessions` to find session numbers.');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (topic === 'shutdown') {
|
|
197
|
+
await sendLongMessage(token, chatId,
|
|
198
|
+
'`/shutdown` — stop tgagentp process\n\n' +
|
|
199
|
+
'Only available when tgagentp is started with `--dev` flag.\n' +
|
|
200
|
+
'Useful when running tgagentp in a restart loop:\n' +
|
|
201
|
+
'`while true; do tgagentp --dev; done`');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (topic === 'agents') {
|
|
205
|
+
await sendLongMessage(token, chatId,
|
|
206
|
+
'`/agents` — list all available agents (name, mode, assigned model, description).\n' +
|
|
207
|
+
'`/agents switch <name>` — switch the active agent. Subsequent messages will use this agent.\n\n' +
|
|
208
|
+
'Agent switching requires an active session (`/sessions switch` or `/sessions new`).');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (topic === 'session') {
|
|
212
|
+
await sendLongMessage(token, chatId,
|
|
213
|
+
'`/session` is an alias for `/sessions`.\n\n' +
|
|
214
|
+
'`/sessions` — list active sessions\n' +
|
|
215
|
+
'`/sessions switch <number-or-name>` — switch to a session\n' +
|
|
216
|
+
'`/sessions new [name]` — create a new session\n' +
|
|
217
|
+
'`/sessions rename <name>` — rename the active session');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (topic === 'models') {
|
|
221
|
+
await sendLongMessage(token, chatId,
|
|
222
|
+
'`/models` — list all providers and their models.\n' +
|
|
223
|
+
'Shows provider name, connection status, default model, and up to 5 models per provider\n' +
|
|
224
|
+
'with status, context limit, and input/output costs.');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
await sendLongMessage(token, chatId,
|
|
228
|
+
'Unknown topic. Try `/help servers`, `/help sessions`, `/help session`, `/help agents`, `/help models`, or just `/help`.');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
await sendLongMessage(token, chatId,
|
|
232
|
+
'`/help` — show this message\n' +
|
|
233
|
+
'`/help <topic>` — detailed help\n' +
|
|
234
|
+
'`/status` — show current server and session\n' +
|
|
235
|
+
'`/cancel` — cancel the running prompt\n' +
|
|
236
|
+
'`/servers` — list running opencode servers\n' +
|
|
237
|
+
'`/servers switch <name>` — switch to a different server\n' +
|
|
238
|
+
'`/sessions` — list/create/switch/rename sessions\n' +
|
|
239
|
+
'`/sessions switch <number-or-name>` — switch to a session\n' +
|
|
240
|
+
'`/sessions new [name]` — create a new session\n' +
|
|
241
|
+
'`/sessions rename <name>` — rename the active session\n' +
|
|
242
|
+
'`/session` — alias for `/sessions`\n' +
|
|
243
|
+
'`/agents` — list available agents\n' +
|
|
244
|
+
'`/agents switch <name>` — switch active agent\n' +
|
|
245
|
+
'`/models` — list providers and models\n' +
|
|
246
|
+
'`/shutdown` — stop tgagentp (only with `--dev` flag)\n\n' +
|
|
247
|
+
'Everything else is forwarded to the active server as `[Telegram] <text>`.');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function cmdServers(token, chatId, args, setServer) {
|
|
251
|
+
if (args.length > 0 && args[0].toLowerCase() === 'switch') {
|
|
252
|
+
if (args.length < 2) {
|
|
253
|
+
await sendLongMessage(token, chatId,
|
|
254
|
+
'Usage: `/servers switch <name>`\n' +
|
|
255
|
+
'`<name>` can be a full directory path or a basename.');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const name = args.slice(1).join(' ');
|
|
259
|
+
const servers = listServers();
|
|
260
|
+
if (servers.length === 0) {
|
|
261
|
+
await sendLongMessage(token, chatId, 'No servers running.');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
let matches = servers.filter(s => s.dir === name);
|
|
265
|
+
if (matches.length === 0) {
|
|
266
|
+
matches = servers.filter(s => path.basename(s.dir) === name);
|
|
267
|
+
}
|
|
268
|
+
if (matches.length === 0) {
|
|
269
|
+
matches = servers.filter(s => s.dir.includes(name) || path.basename(s.dir).includes(name));
|
|
270
|
+
}
|
|
271
|
+
if (matches.length === 0) {
|
|
272
|
+
await sendLongMessage(token, chatId, `No server found matching "${name}".`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (matches.length > 1) {
|
|
276
|
+
const list = matches.map(s => ` ${s.url} → ${s.dir}`).join('\n');
|
|
277
|
+
await sendLongMessage(token, chatId,
|
|
278
|
+
`Multiple matches for "${name}":\n${list}\n\nPlease be more specific.`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const target = matches[0];
|
|
282
|
+
const pending = setServer(target.url);
|
|
283
|
+
activateServer(target.dir, target.index, target.url);
|
|
284
|
+
let msg = `Switched to server: ${target.url} → ${target.dir}`;
|
|
285
|
+
if (pending && pending.trim()) {
|
|
286
|
+
await sendLongMessage(token, chatId, msg);
|
|
287
|
+
await sendLongMessage(token, chatId, pending.trim());
|
|
288
|
+
} else {
|
|
289
|
+
await sendLongMessage(token, chatId, msg);
|
|
290
|
+
}
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Default: list servers
|
|
295
|
+
const servers = listServers();
|
|
296
|
+
if (servers.length === 0) {
|
|
297
|
+
await sendLongMessage(token, chatId, 'No servers running.');
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const lines = servers.map(s => ` ${s.url} → ${s.dir} (${s.status})`);
|
|
301
|
+
await sendLongMessage(token, chatId, 'Active servers:\n' + lines.join('\n'));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function sessionName(s) {
|
|
305
|
+
return s.title || s.slug || s.id;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function normalizeUrl(url) {
|
|
309
|
+
return url.replace(/\/+$/, '').replace(/^http:\/\/127\.0\.0\.1:/, 'http://localhost:');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function serverDir(serverUrl) {
|
|
313
|
+
const servers = listServers();
|
|
314
|
+
const normalized = normalizeUrl(serverUrl);
|
|
315
|
+
const found = servers.find(s => normalizeUrl(s.url) === normalized);
|
|
316
|
+
return found ? found.dir : null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Sort by time.updated descending, limit to N.
|
|
320
|
+
function sortedSessions(sessions, limit) {
|
|
321
|
+
return sessions
|
|
322
|
+
.filter(s => s.time && s.time.updated)
|
|
323
|
+
.sort((a, b) => b.time.updated - a.time.updated)
|
|
324
|
+
.slice(0, limit || 50);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function cmdSessions(token, chatId, args, setServer, setActiveSession, server) {
|
|
328
|
+
if (args.length > 0) {
|
|
329
|
+
const sub = args[0].toLowerCase();
|
|
330
|
+
|
|
331
|
+
if (sub === 'new') {
|
|
332
|
+
const title = args.slice(1).join(' ') || undefined;
|
|
333
|
+
try {
|
|
334
|
+
const session = await createSession(server, title);
|
|
335
|
+
const name = session.title || session.slug || session.id;
|
|
336
|
+
setActiveSession(session.id);
|
|
337
|
+
selectSession(server, session.id);
|
|
338
|
+
await sendLongMessage(token, chatId,
|
|
339
|
+
`Created session: \`${name}\`\nIt is now the active session.`);
|
|
340
|
+
} catch (err) {
|
|
341
|
+
await sendLongMessage(token, chatId, `Error creating session: ${err.message}`);
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (sub === 'rename') {
|
|
347
|
+
if (args.length < 2) {
|
|
348
|
+
await sendLongMessage(token, chatId, 'Usage: `/sessions rename <name>`');
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const st = getState(server);
|
|
352
|
+
if (!st.activeSessionId) {
|
|
353
|
+
await sendLongMessage(token, chatId, 'No active session. Use `/sessions switch` or `/sessions new` first.');
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const newTitle = args.slice(1).join(' ');
|
|
357
|
+
try {
|
|
358
|
+
const session = await updateSession(server, st.activeSessionId, newTitle);
|
|
359
|
+
const name = session.title || session.slug || session.id;
|
|
360
|
+
await sendLongMessage(token, chatId,
|
|
361
|
+
`Renamed session to: \`${name}\``);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
await sendLongMessage(token, chatId, `Error renaming session: ${err.message}`);
|
|
364
|
+
}
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (sub === 'switch') {
|
|
369
|
+
if (args.length < 2) {
|
|
370
|
+
await sendLongMessage(token, chatId,
|
|
371
|
+
'Usage: `/sessions switch <number-or-name>`\n' +
|
|
372
|
+
'Use `/sessions` to see numbered list. You can switch by number or by name (case-insensitive partial match).');
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const query = args.slice(1).join(' ');
|
|
376
|
+
try {
|
|
377
|
+
const projectDir = serverDir(server);
|
|
378
|
+
const sessions = await listSessions(server, projectDir);
|
|
379
|
+
const activeId = getState(server).activeSessionId;
|
|
380
|
+
const filtered = sessions.filter(s =>
|
|
381
|
+
(s.agent || s.directory || s.id === activeId) && (!projectDir || s.directory === projectDir));
|
|
382
|
+
const sorted = sortedSessions(filtered, 50);
|
|
383
|
+
|
|
384
|
+
// Try by position number first
|
|
385
|
+
const num = parseInt(query, 10);
|
|
386
|
+
if (/^\d+$/.test(query) && num >= 1 && num <= sorted.length) {
|
|
387
|
+
const target = sorted[num - 1];
|
|
388
|
+
setActiveSession(target.id);
|
|
389
|
+
selectSession(server, target.id);
|
|
390
|
+
await sendLongMessage(token, chatId,
|
|
391
|
+
`Switched to session #${num}: \`${sessionName(target)}\`\nSubsequent messages will be sent to this session.`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Try by name (case-insensitive partial match)
|
|
396
|
+
const q = query.toLowerCase();
|
|
397
|
+
const matches = sorted.filter(s => sessionName(s).toLowerCase().includes(q));
|
|
398
|
+
if (matches.length === 0) {
|
|
399
|
+
await sendLongMessage(token, chatId,
|
|
400
|
+
`No session matching "${query}". Use /sessions to list active sessions.`);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (matches.length > 1) {
|
|
404
|
+
const list = matches.map(s => ` \`${sessionName(s)}\``).join('\n');
|
|
405
|
+
await sendLongMessage(token, chatId,
|
|
406
|
+
`Multiple matches for "${query}":\n${list}\n\nPlease be more specific or use a number.`);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const target = matches[0];
|
|
410
|
+
setActiveSession(target.id);
|
|
411
|
+
selectSession(server, target.id);
|
|
412
|
+
await sendLongMessage(token, chatId,
|
|
413
|
+
`Switched to session: \`${sessionName(target)}\`\nSubsequent messages will be sent to this session.`);
|
|
414
|
+
} catch (err) {
|
|
415
|
+
await sendLongMessage(token, chatId, `Error switching session: ${err.message}`);
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Unknown subcommand
|
|
421
|
+
await sendLongMessage(token, chatId,
|
|
422
|
+
`Unknown subcommand: /sessions ${sub}\n\n` +
|
|
423
|
+
'Usage:\n' +
|
|
424
|
+
' `/sessions` — list sessions\n' +
|
|
425
|
+
' `/sessions switch <number-or-name>` — switch to a session\n' +
|
|
426
|
+
' `/sessions new [name]` — create a new session\n' +
|
|
427
|
+
' `/sessions rename <name>` — rename the active session');
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Default: list sessions
|
|
432
|
+
try {
|
|
433
|
+
const projectDir = serverDir(server);
|
|
434
|
+
const sessions = await listSessions(server, projectDir);
|
|
435
|
+
if (!sessions || sessions.length === 0) {
|
|
436
|
+
await sendLongMessage(token, chatId, 'No active sessions.');
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Show user sessions (have agent or directory) and always include the active session
|
|
441
|
+
const activeId = getState(server).activeSessionId;
|
|
442
|
+
const filtered = sessions.filter(s =>
|
|
443
|
+
(s.agent || s.directory || s.id === activeId) && (!projectDir || s.directory === projectDir));
|
|
444
|
+
const sorted = sortedSessions(filtered, 50);
|
|
445
|
+
if (sorted.length === 0) {
|
|
446
|
+
await sendLongMessage(token, chatId, 'No active sessions.');
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Build lines with date headings and time labels
|
|
451
|
+
const now = new Date();
|
|
452
|
+
const todayStr = now.toLocaleDateString();
|
|
453
|
+
const oneWeekAgo = new Date(now);
|
|
454
|
+
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
|
455
|
+
|
|
456
|
+
const lines = [];
|
|
457
|
+
let currentHeading = null;
|
|
458
|
+
|
|
459
|
+
sorted.forEach((s, idx) => {
|
|
460
|
+
const created = new Date(s.time.updated);
|
|
461
|
+
const isToday = created.toLocaleDateString() === todayStr;
|
|
462
|
+
const isRecent = created >= oneWeekAgo;
|
|
463
|
+
|
|
464
|
+
let heading;
|
|
465
|
+
if (isToday) {
|
|
466
|
+
heading = '**Today**';
|
|
467
|
+
} else if (isRecent) {
|
|
468
|
+
heading = '**' + created.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }) + '**';
|
|
469
|
+
} else {
|
|
470
|
+
heading = '**' + created.getFullYear() + '-' + String(created.getMonth() + 1).padStart(2, '0') + '**';
|
|
471
|
+
}
|
|
472
|
+
if (heading !== currentHeading) {
|
|
473
|
+
lines.push('');
|
|
474
|
+
lines.push(heading);
|
|
475
|
+
currentHeading = heading;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const hh = String(created.getHours()).padStart(2, '0');
|
|
479
|
+
const mm = String(created.getMinutes()).padStart(2, '0');
|
|
480
|
+
const timeLabel = isRecent
|
|
481
|
+
? `[${hh}:${mm}]`
|
|
482
|
+
: `[${String(created.getDate()).padStart(2, '0')}/${String(created.getMonth() + 1).padStart(2, '0')} - ${hh}:${mm}]`;
|
|
483
|
+
|
|
484
|
+
const name = sessionName(s);
|
|
485
|
+
const pos = idx + 1;
|
|
486
|
+
const bullet = s.id === activeId ? '▶ ' : ' · ';
|
|
487
|
+
lines.push('`' + bullet + pos + ' ' + timeLabel + ' – ' + name.replace(/`/g, "'") + '`');
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
await sendLongMessage(token, chatId, 'Sessions:\n' + lines.join('\n'));
|
|
491
|
+
} catch (err) {
|
|
492
|
+
await sendLongMessage(token, chatId, `Error listing sessions: ${err.message}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function cmdAgents(token, chatId, args, setServer, setActiveSession, server) {
|
|
497
|
+
if (args.length > 0 && args[0].toLowerCase() === 'switch') {
|
|
498
|
+
if (args.length < 2) {
|
|
499
|
+
await sendLongMessage(token, chatId,
|
|
500
|
+
'Usage: `/agents switch <name>`\n' +
|
|
501
|
+
'Use `/agents` to list available agents.');
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const name = args.slice(1).join(' ');
|
|
505
|
+
try {
|
|
506
|
+
const agents = await listAgents(server);
|
|
507
|
+
if (!agents || agents.length === 0) {
|
|
508
|
+
await sendLongMessage(token, chatId, 'No agents available.');
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
let matches = agents.filter(a => a.name === name);
|
|
512
|
+
if (matches.length === 0) {
|
|
513
|
+
matches = agents.filter(a => a.name.toLowerCase().includes(name.toLowerCase()));
|
|
514
|
+
}
|
|
515
|
+
if (matches.length === 0) {
|
|
516
|
+
await sendLongMessage(token, chatId, `No agent matching "${name}".`);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (matches.length > 1) {
|
|
520
|
+
const list = matches.map(a => ` \`${a.name}\``).join('\n');
|
|
521
|
+
await sendLongMessage(token, chatId,
|
|
522
|
+
`Multiple matches for "${name}":\n${list}\n\nPlease be more specific.`);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const target = matches[0];
|
|
526
|
+
getState(server).activeAgentName = target.name;
|
|
527
|
+
const mode = target.mode === 'primary' ? '' : ` (${target.mode})`;
|
|
528
|
+
const model = target.model ? ` — \`${target.model.providerID}/${target.model.modelID}\`` : '';
|
|
529
|
+
await sendLongMessage(token, chatId,
|
|
530
|
+
`Switched to agent: **${target.name}**${mode}${model}\nSubsequent messages will use this agent.`);
|
|
531
|
+
} catch (err) {
|
|
532
|
+
await sendLongMessage(token, chatId, `Error switching agent: ${err.message}`);
|
|
533
|
+
}
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
const agents = await listAgents(server);
|
|
539
|
+
if (!agents || agents.length === 0) {
|
|
540
|
+
await sendLongMessage(token, chatId, 'No agents available.');
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const lines = agents.map(a => {
|
|
544
|
+
const mode = a.mode === 'primary' ? '' : ` (${a.mode})`;
|
|
545
|
+
const model = a.model ? ` — \`${a.model.providerID}/${a.model.modelID}\`` : '';
|
|
546
|
+
const desc = a.description ? `. ${a.description}` : '';
|
|
547
|
+
return '**' + a.name + '**' + mode + model + desc;
|
|
548
|
+
});
|
|
549
|
+
await sendLongMessage(token, chatId, 'Agents:\n' + lines.join('\n'));
|
|
550
|
+
} catch (err) {
|
|
551
|
+
await sendLongMessage(token, chatId, `Error listing agents: ${err.message}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function cmdModels(token, chatId, args, setServer, setActiveSession, server) {
|
|
556
|
+
try {
|
|
557
|
+
const result = await listProviders(server);
|
|
558
|
+
const all = result.all || [];
|
|
559
|
+
const connectedIds = result.connected || [];
|
|
560
|
+
|
|
561
|
+
const filtered = all.filter(p => connectedIds.includes(p.id));
|
|
562
|
+
if (filtered.length === 0) {
|
|
563
|
+
await sendLongMessage(token, chatId, 'No connected providers.');
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const lines = [];
|
|
568
|
+
|
|
569
|
+
for (const provider of filtered) {
|
|
570
|
+
lines.push('');
|
|
571
|
+
lines.push('**' + provider.name + '** ✅');
|
|
572
|
+
|
|
573
|
+
const modelEntries = Object.entries(provider.models || {});
|
|
574
|
+
if (modelEntries.length === 0) continue;
|
|
575
|
+
|
|
576
|
+
const shown = modelEntries.slice(0, 5);
|
|
577
|
+
for (const [id, model] of shown) {
|
|
578
|
+
const ctx = model.limit ? Math.round(model.limit.context / 1000) + 'K' : '';
|
|
579
|
+
const cost = model.cost ? ` $${model.cost.input}/${model.cost.output}` : '';
|
|
580
|
+
lines.push(' `' + id + '`' + (ctx ? ' ' + ctx : '') + cost);
|
|
581
|
+
}
|
|
582
|
+
if (modelEntries.length > 5) {
|
|
583
|
+
lines.push(' *…and ' + (modelEntries.length - 5) + ' more*');
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
await sendLongMessage(token, chatId, 'Connected providers:\n' + lines.join('\n'));
|
|
588
|
+
} catch (err) {
|
|
589
|
+
await sendLongMessage(token, chatId, `Error listing models: ${err.message}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const COMMANDS = {
|
|
594
|
+
'help': cmdHelp,
|
|
595
|
+
'servers': cmdServers,
|
|
596
|
+
'sessions': cmdSessions,
|
|
597
|
+
'session': cmdSessions,
|
|
598
|
+
'agents': cmdAgents,
|
|
599
|
+
'models': cmdModels,
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// --------------------------------------------------------------------------
|
|
603
|
+
// Module-level state
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
|
|
606
|
+
const serverState = {};
|
|
607
|
+
function getState(url) {
|
|
608
|
+
return serverState[url] = serverState[url] || { busy: false, pendingResponse: null, activeSessionId: null, activeAgentName: null, taskId: 0 };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// --------------------------------------------------------------------------
|
|
612
|
+
// Main
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
|
|
615
|
+
async function main() {
|
|
616
|
+
let serverBase = 'http://localhost:4096';
|
|
617
|
+
|
|
618
|
+
let DEV_MODE = false;
|
|
619
|
+
|
|
620
|
+
const args = process.argv.slice(2);
|
|
621
|
+
for (let i = 0; i < args.length; i++) {
|
|
622
|
+
if (args[i] === '--version') {
|
|
623
|
+
console.log(version);
|
|
624
|
+
process.exit(0);
|
|
625
|
+
} else if (args[i] === '--dev') {
|
|
626
|
+
DEV_MODE = true;
|
|
627
|
+
} else if (args[i] === '--help') {
|
|
628
|
+
console.error('Usage: tgagentp [options] [url]');
|
|
629
|
+
console.error('');
|
|
630
|
+
console.error('Bridge a Telegram bot chat with an OpenCode TUI session.');
|
|
631
|
+
console.error('Incoming Telegram messages are forwarded to OpenCode; answers are sent back to the chat.');
|
|
632
|
+
console.error('');
|
|
633
|
+
console.error('Options:');
|
|
634
|
+
console.error(' --dev Enable development mode (adds /shutdown command)');
|
|
635
|
+
console.error(' --version Show version');
|
|
636
|
+
console.error(' --help Show this help message');
|
|
637
|
+
console.error('');
|
|
638
|
+
console.error('Arguments:');
|
|
639
|
+
console.error(' url OpenCode TUI server URL or port number (default: 4096)');
|
|
640
|
+
console.error(' Examples: 4096, http://localhost:4096, http://192.168.1.50:4096');
|
|
641
|
+
console.error('');
|
|
642
|
+
console.error('Environment variables:');
|
|
643
|
+
console.error(' TELEGRAM_BOT_TOKEN Telegram bot token (required)');
|
|
644
|
+
console.error(' OPENCODE_SERVER_PASSWORD OpenCode server password (optional)');
|
|
645
|
+
console.error(' OPENCODE_SERVER_USERNAME OpenCode server username (optional, default: opencode)');
|
|
646
|
+
console.error(' TGAGENTP_ALLOWED_CHAT_IDS Comma-separated Telegram chat IDs (optional)');
|
|
647
|
+
console.error(' Example: "123456,-789012"');
|
|
648
|
+
console.error('');
|
|
649
|
+
console.error('See also: agentp, ocmux');
|
|
650
|
+
console.error('');
|
|
651
|
+
console.error('Examples:');
|
|
652
|
+
console.error(' export TELEGRAM_BOT_TOKEN="your-bot-token"');
|
|
653
|
+
console.error(' tgagentp');
|
|
654
|
+
console.error(' tgagentp 4096');
|
|
655
|
+
console.error(' tgagentp http://192.168.1.50:4096');
|
|
656
|
+
process.exit(0);
|
|
657
|
+
} else if (args[i].includes('://')) {
|
|
658
|
+
serverBase = args[i].replace(/\/+$/, '');
|
|
659
|
+
} else if (/^\d+$/.test(args[i])) {
|
|
660
|
+
serverBase = 'http://localhost:' + args[i];
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const token = process.env.TELEGRAM_BOT_TOKEN;
|
|
665
|
+
if (!token) {
|
|
666
|
+
console.error('Error: TELEGRAM_BOT_TOKEN environment variable is not set.');
|
|
667
|
+
console.error('Create a bot with @BotFather on Telegram and export the token before running tgagentp.');
|
|
668
|
+
process.exit(1);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
process.stderr.write(`tgagentp v${version} started. OpenCode server: ${serverBase}\n`);
|
|
672
|
+
|
|
673
|
+
if (!process.env.OPENCODE_SERVER_PASSWORD) {
|
|
674
|
+
process.stderr.write('🔓 ⚠️ Server is not password-protected! Set OPENCODE_SERVER_PASSWORD for better security.\n');
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const allowedChatIds = parseAllowlist(process.env.TGAGENTP_ALLOWED_CHAT_IDS);
|
|
678
|
+
if (allowedChatIds) {
|
|
679
|
+
process.stderr.write(`Chat allowlist: ${allowedChatIds.length} chat(s) configured\n`);
|
|
680
|
+
} else {
|
|
681
|
+
process.stderr.write('Chat allowlist: not configured (all chats accepted)\n');
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
let lockedChatId = null;
|
|
685
|
+
|
|
686
|
+
process.stderr.write('Waiting for Telegram messages... (Ctrl+C to stop)\n');
|
|
687
|
+
|
|
688
|
+
getState(serverBase);
|
|
689
|
+
|
|
690
|
+
const setServer = (url) => {
|
|
691
|
+
serverBase = url;
|
|
692
|
+
getState(url);
|
|
693
|
+
process.stderr.write(`Switched to server: ${url}\n`);
|
|
694
|
+
const p = getState(url).pendingResponse;
|
|
695
|
+
if (p) {
|
|
696
|
+
getState(url).pendingResponse = null;
|
|
697
|
+
return p;
|
|
698
|
+
}
|
|
699
|
+
return null;
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
const setActiveSession = (id) => {
|
|
703
|
+
getState(serverBase).activeSessionId = id;
|
|
704
|
+
process.stderr.write(`Switched to session: ${id}\n`);
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
async function processMessageAsync(chatId, text, url, useSession, sessionId, myTaskId) {
|
|
708
|
+
const st = getState(url);
|
|
709
|
+
const isCancelled = () => getState(url).taskId !== myTaskId;
|
|
710
|
+
const cancelRef = { current: null };
|
|
711
|
+
st._cancelRef = cancelRef;
|
|
712
|
+
try {
|
|
713
|
+
let answer;
|
|
714
|
+
if (useSession) {
|
|
715
|
+
answer = await sendToSession(url, sessionId, `[Telegram] ${text}`, st.activeAgentName);
|
|
716
|
+
} else {
|
|
717
|
+
await sendText(url, `[Telegram] ${text}`);
|
|
718
|
+
answer = await listenForFinalAnswer(url, null, cancelRef);
|
|
719
|
+
}
|
|
720
|
+
if (isCancelled()) {
|
|
721
|
+
process.stderr.write(` → cancelled, discarding response from ${url}\n`);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (url === serverBase) {
|
|
725
|
+
if (answer && answer.trim()) {
|
|
726
|
+
await sendLongMessage(token, chatId, answer.trim());
|
|
727
|
+
}
|
|
728
|
+
} else {
|
|
729
|
+
getState(url).pendingResponse = answer;
|
|
730
|
+
await sendLongMessage(token, chatId, `✅ Server \`${url}\` finished processing. Switch back to see the response.`);
|
|
731
|
+
}
|
|
732
|
+
} catch (err) {
|
|
733
|
+
if (isCancelled()) {
|
|
734
|
+
process.stderr.write(` → cancelled, ignoring error from ${url}: ${err.message}\n`);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
process.stderr.write(`Error processing message (${url}): ${err.message}\n`);
|
|
738
|
+
if (url === serverBase) {
|
|
739
|
+
try { await sendLongMessage(token, chatId, `Error: ${err.message}`); } catch (_) {}
|
|
740
|
+
}
|
|
741
|
+
} finally {
|
|
742
|
+
if (st._cancelRef === cancelRef) st._cancelRef = null;
|
|
743
|
+
if (!isCancelled()) {
|
|
744
|
+
getState(url).busy = false;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const cmdCancel = async (token, chatId) => {
|
|
750
|
+
const st = getState(serverBase);
|
|
751
|
+
if (!st.busy) {
|
|
752
|
+
await sendLongMessage(token, chatId, 'Nothing to cancel.');
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
st.taskId++;
|
|
756
|
+
st.busy = false;
|
|
757
|
+
// Abort the SSE stream so the server stops processing
|
|
758
|
+
if (st._cancelRef && st._cancelRef.current) {
|
|
759
|
+
st._cancelRef.current.destroy();
|
|
760
|
+
st._cancelRef.current = null;
|
|
761
|
+
}
|
|
762
|
+
await sendLongMessage(token, chatId, '✅ Cancelled.');
|
|
763
|
+
process.stderr.write(` → /cancel on ${serverBase}\n`);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const cmdStatus = async (token, chatId) => {
|
|
767
|
+
const st = getState(serverBase);
|
|
768
|
+
const busyInfo = st.busy ? '⏳ busy' : '✅ idle';
|
|
769
|
+
const dir = serverDir(serverBase);
|
|
770
|
+
const lines = ['**Server:**'];
|
|
771
|
+
if (dir) lines.push(' • **Path:** `' + dir + '`');
|
|
772
|
+
lines.push(' • **URL:** `' + serverBase + '`');
|
|
773
|
+
lines.push(' • **Status:** ' + busyInfo);
|
|
774
|
+
lines.push('**Session:** ' + (st.activeSessionId ? '`' + st.activeSessionId + '`' : '*none (TUI+SSE)*'));
|
|
775
|
+
lines.push('**Agent:** ' + (st.activeAgentName ? '`' + st.activeAgentName + '`' : '*default*'));
|
|
776
|
+
await sendLongMessage(token, chatId, lines.join('\n'));
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
const cmdShutdown = async (token, chatId) => {
|
|
780
|
+
await sendLongMessage(token, chatId, '👋 Shutting down...');
|
|
781
|
+
try {
|
|
782
|
+
fs.writeFileSync(STARTUP_CHAT_FILE, String(chatId));
|
|
783
|
+
} catch (_) {}
|
|
784
|
+
try { await getUpdates(token, offset, 1); } catch (_) {}
|
|
785
|
+
process.stderr.write('/shutdown received, exiting...\n');
|
|
786
|
+
process.exit(0);
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
const cmds = { ...COMMANDS, status: cmdStatus, cancel: cmdCancel };
|
|
790
|
+
if (DEV_MODE) cmds.shutdown = cmdShutdown;
|
|
791
|
+
|
|
792
|
+
let offset = 0;
|
|
793
|
+
|
|
794
|
+
// Startup greeting to last-known chat
|
|
795
|
+
const savedChatId = (() => { try { return fs.readFileSync(STARTUP_CHAT_FILE, 'utf8').trim(); } catch { return null; } })();
|
|
796
|
+
if (savedChatId) {
|
|
797
|
+
const dir = serverDir(serverBase);
|
|
798
|
+
const lines = ['✅ Bot started.', '**Server:**'];
|
|
799
|
+
if (dir) lines.push(' • **Path:** `' + dir + '`');
|
|
800
|
+
lines.push(' • **URL:** `' + serverBase + '`');
|
|
801
|
+
lines.push(' • **Status:** ✅ idle');
|
|
802
|
+
lines.push('**Session:** *none (TUI+SSE)*');
|
|
803
|
+
try { await sendLongMessage(token, savedChatId, lines.join('\n')); } catch (_) {}
|
|
804
|
+
try { fs.unlinkSync(STARTUP_CHAT_FILE); } catch (_) {}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Poll for updates and forward each text message to OpenCode, sending the answer back.
|
|
808
|
+
while (true) {
|
|
809
|
+
let result;
|
|
810
|
+
try {
|
|
811
|
+
result = await getUpdates(token, offset, 30);
|
|
812
|
+
} catch (err) {
|
|
813
|
+
process.stderr.write(`Polling error: ${err.message}\n`);
|
|
814
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (!result.ok) {
|
|
819
|
+
process.stderr.write(`Telegram API error: ${result.description || JSON.stringify(result)}\n`);
|
|
820
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
for (const update of result.result) {
|
|
825
|
+
offset = update.update_id + 1;
|
|
826
|
+
|
|
827
|
+
const msg = update.message;
|
|
828
|
+
if (!msg || typeof msg.text !== 'string') {
|
|
829
|
+
// Ignore non-text messages (photos, stickers, etc.)
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const chatId = String(msg.chat.id);
|
|
834
|
+
const text = msg.text;
|
|
835
|
+
const from = msg.from?.username
|
|
836
|
+
? `@${msg.from.username}`
|
|
837
|
+
: [msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(' ') || 'unknown';
|
|
838
|
+
|
|
839
|
+
// Enforce chat allowlist if configured
|
|
840
|
+
if (allowedChatIds && !allowedChatIds.includes(chatId)) {
|
|
841
|
+
process.stderr.write(`[chat ${chatId}] ${from}: blocked (not in allowlist)\n`);
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Lock to the first chat that connects (single-user guard)
|
|
846
|
+
if (lockedChatId === null) {
|
|
847
|
+
lockedChatId = chatId;
|
|
848
|
+
process.stderr.write(`[chat ${chatId}] ${from}: locked to this chat\n`);
|
|
849
|
+
} else if (lockedChatId !== chatId) {
|
|
850
|
+
process.stderr.write(`[chat ${chatId}] ${from}: blocked (locked to ${lockedChatId})\n`);
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
process.stderr.write(`[chat ${chatId}] ${from}: ${text}\n`);
|
|
855
|
+
try { fs.writeFileSync(STARTUP_CHAT_FILE, String(chatId)); } catch (_) {}
|
|
856
|
+
|
|
857
|
+
// Intercept /commands
|
|
858
|
+
if (text.startsWith('/')) {
|
|
859
|
+
const parts = text.slice(1).split(' ');
|
|
860
|
+
const cmd = parts[0].toLowerCase();
|
|
861
|
+
const cmdArgs = parts.slice(1);
|
|
862
|
+
const handler = cmds[cmd];
|
|
863
|
+
if (handler) {
|
|
864
|
+
try {
|
|
865
|
+
await handler(token, chatId, cmdArgs, setServer, setActiveSession, serverBase);
|
|
866
|
+
} catch (err) {
|
|
867
|
+
process.stderr.write(`Error handling /${cmd}: ${err.message}\n`);
|
|
868
|
+
try { await sendLongMessage(token, chatId, `Error: ${err.message}`); } catch (_) {}
|
|
869
|
+
}
|
|
870
|
+
} else {
|
|
871
|
+
await sendLongMessage(token, chatId,
|
|
872
|
+
`Unknown command: /${cmd}\n\nAvailable commands: /help, /status, /servers, /sessions, /agents, /models`);
|
|
873
|
+
}
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Non-command message — process in background
|
|
878
|
+
const st = getState(serverBase);
|
|
879
|
+
if (st.busy) {
|
|
880
|
+
await sendLongMessage(token, chatId, '⏳ Server is busy processing a previous message. Please wait for it to finish, or use /cancel to cancel it.');
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
st.busy = true;
|
|
884
|
+
const myTaskId = ++st.taskId;
|
|
885
|
+
process.stderr.write(` → started background processing (task ${myTaskId})\n`);
|
|
886
|
+
processMessageAsync(chatId, text, serverBase, !!st.activeSessionId, st.activeSessionId, myTaskId);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
process.on('SIGINT', () => {
|
|
892
|
+
process.stderr.write('\ntgagentp stopped.\n');
|
|
893
|
+
process.exit(0);
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
main().catch((err) => {
|
|
897
|
+
console.error('Error:', err.message);
|
|
898
|
+
process.exit(1);
|
|
899
|
+
});
|