agentp 0.11.4 → 0.11.5

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/bin/agentp CHANGED
@@ -10,6 +10,7 @@ const {
10
10
  listSessions,
11
11
  createSession,
12
12
  getSession,
13
+ selectSession,
13
14
  } = require('../lib/opencode');
14
15
 
15
16
  const TGAGENTP_PORT_FILE = '/tmp/tgagentp-port';
@@ -356,168 +357,6 @@ async function main() {
356
357
  }
357
358
  }
358
359
 
359
- let answer;
360
- try {
361
- answer = await sendToSession(serverBase, sessionId, promptText);
362
- } catch (err) {
363
- console.error('Error sending prompt:', err.message);
364
- process.exit(1);
365
- }
366
- const n = parseInt(nStr, 10);
367
- try {
368
- const sessions = await listSessions(serverBase);
369
- if (!sessions || sessions.length === 0) {
370
- console.error('No sessions found.');
371
- process.exit(1);
372
- }
373
- const sorted = sessions
374
- .filter(s => s.time && s.time.updated)
375
- .sort((a, b) => b.time.updated - a.time.updated);
376
- const sessionId = sorted[0].id;
377
-
378
- function buildPairsFromMessages(messages) {
379
- const pairs = [];
380
- let cur = null;
381
- for (const raw of messages) {
382
- // Normalize: export gives {role, parts: [{type, text}]}, SQLite gives {role, text}
383
- const m = raw.parts ? raw : { role: raw.role, parts: [{ type: 'text', text: raw.text }] };
384
- const role = m.role;
385
- const text = (m.parts || []).filter(p => p.type === 'text').map(p => p.text).join('').trim();
386
- if (!text) continue;
387
- if (role === 'user') {
388
- if (cur) pairs.push(cur);
389
- cur = { prompt: text, answer: '' };
390
- } else if (role === 'assistant' && cur) {
391
- if (cur.answer) cur.answer += '\n';
392
- cur.answer += text;
393
- }
394
- }
395
- // Don't push incomplete pair (no answer yet)
396
- return pairs;
397
- }
398
-
399
- function formatPairs(pairs) {
400
- return pairs.map(p =>
401
- HUMAN_ROW + p.prompt + '\n' + AGENT_ROW + p.answer + '\n' + FINAL_ROW
402
- ).join('\n');
403
- }
404
-
405
- const formatAsQa = qaMode || args.includes('--qa');
406
- function extractAnswers(pairs) {
407
- return pairs.map(p => p.answer);
408
- }
409
- let output = '';
410
- try {
411
- const raw = child_process.execFileSync('opencode', ['export', sessionId], { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
412
- const jsonStart = raw.indexOf('{');
413
- const body = jsonStart >= 0 ? raw.slice(jsonStart) : raw;
414
- const exportData = JSON.parse(body);
415
- const messages = exportData.messages || [];
416
- const pairs = buildPairsFromMessages(messages);
417
- const lastN = pairs.slice(-n);
418
- output = formatAsQa ? formatPairs(lastN) : extractAnswers(lastN).join('\n');
419
- } catch {
420
- // export JSON is malformed (opencode bug);
421
- // fall back to SQLite
422
- const home = process.env.HOME || process.env.USERPROFILE || '';
423
- const dbCandidates = [
424
- `${home}/.local/share/opencode/opencode.db`,
425
- `${home}/Library/Application Support/opencode/opencode.db`,
426
- `${home}/.opencode/opencode.db`,
427
- ];
428
- let dbPath = null;
429
- for (const p of dbCandidates) {
430
- try { fs.accessSync(p); dbPath = p; break; } catch {}
431
- }
432
- if (dbPath) {
433
- const sql = `SELECT m.data->>'role' as role, group_concat(p.data->>'text', '') as text FROM message m JOIN part p ON p.message_id = m.id WHERE m.session_id = '${sessionId.replace(/'/g, "''")}' AND p.data->>'type' = 'text' AND m.data->>'role' IN ('user','assistant') GROUP BY m.id ORDER BY m.time_created ASC`;
434
- const out = child_process.execFileSync('sqlite3', ['-json', dbPath, sql], { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
435
- const messages = JSON.parse(out);
436
- const pairs = buildPairsFromMessages(messages);
437
- const lastN = pairs.slice(-n);
438
- output = formatAsQa ? formatPairs(lastN) : extractAnswers(lastN).join('\n');
439
- }
440
- }
441
- if (!output) {
442
- console.error(formatAsQa ? 'No complete prompt-answer pairs found in session history.' : 'No assistant answers found in session history.');
443
- process.exit(1);
444
- }
445
- process.stdout.write(output);
446
- if (!output.endsWith('\n')) process.stdout.write('\n');
447
- } catch (err) {
448
- console.error('Error retrieving session history:', err.message);
449
- process.exit(1);
450
- }
451
- return;
452
- } else if (args[i] === '--tg') {
453
- tgMode = true;
454
- } else if (args[i] === '--no-tg') {
455
- tgMode = false;
456
- } else if (args[i].includes('://')) {
457
- serverBase = args[i].replace(/\/+$/, '');
458
- } else if (/^\d+$/.test(args[i])) {
459
- serverBase = 'http://localhost:' + args[i];
460
- } else if (args[i].startsWith('--')) {
461
- console.error(`Error: unknown option '${args[i]}'`);
462
- console.error(`Usage: agentp [options] [url]`);
463
- console.error(`Try 'agentp --help' for more information.`);
464
- process.exit(1);
465
- } else {
466
- console.error(`Error: unexpected argument '${args[i]}'`);
467
- process.exit(1);
468
- }
469
- }
470
-
471
- // Resolve default: --qa → auto-detect, standalone → disabled
472
- if (tgMode === null) {
473
- tgMode = qaMode ? 'auto' : false;
474
- }
475
-
476
- // Pre-send: probe tgagentp gateway if enabled
477
- let tgPort = null;
478
- if (tgMode) {
479
- try {
480
- tgPort = fs.readFileSync(TGAGENTP_PORT_FILE, 'utf8').trim();
481
- if (!tgPort || !/^\d+$/.test(tgPort)) tgPort = null;
482
- } catch {
483
- tgPort = null;
484
- }
485
- if (!tgPort && tgMode === true) {
486
- console.error('Error: tgagentp gateway not found. Is tgagentp running?');
487
- process.exit(1);
488
- }
489
- // auto mode: tgPort stays null → silently skip tgagentp
490
- }
491
-
492
- const promptText = await readAllStdin();
493
-
494
- // Find most recent session or create one
495
- let sessions;
496
- try {
497
- sessions = await listSessions(serverBase);
498
- } catch (err) {
499
- console.error('Error connecting to server:', err.message);
500
- process.exit(1);
501
- }
502
-
503
- let sessionId = null;
504
- if (sessions && sessions.length > 0) {
505
- const sorted = sessions
506
- .filter(s => s.time && s.time.updated)
507
- .sort((a, b) => b.time.updated - a.time.updated);
508
- if (sorted.length > 0) sessionId = sorted[0].id;
509
- }
510
-
511
- if (!sessionId) {
512
- try {
513
- const session = await createSession(serverBase, 'agentp');
514
- sessionId = session.id;
515
- } catch (err) {
516
- console.error('Error creating session:', err.message);
517
- process.exit(1);
518
- }
519
- }
520
-
521
360
  let answer;
522
361
  try {
523
362
  answer = await sendToSession(serverBase, sessionId, promptText);
@@ -570,7 +409,12 @@ async function main() {
570
409
  }
571
410
  }
572
411
 
573
- main().catch((err) => {
574
- console.error('Error:', err.message);
575
- process.exit(1);
576
- });
412
+ if (require.main === module) {
413
+ main().catch((err) => {
414
+ console.error('Error:', err.message);
415
+ process.exit(1);
416
+ });
417
+ }
418
+
419
+ module.exports = { main, notifyAgentpGateway, readAllStdin };
420
+
package/bin/tgagentp CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const http = require('http');
4
- const https = require('https');
5
4
  const fs = require('fs');
6
5
  const path = require('path');
7
6
  const { version } = require('../package.json');
@@ -21,6 +20,8 @@ const TGAGENTP_DEBOUNCE_MS = parseInt(process.env.TGAGENTP_DEBOUNCE_MS || '5000'
21
20
  const CONNECTIONS_FILE = '/tmp/tgagentp-connections.json';
22
21
  const { listServers, activateServer, resurrectServer, statefileFor, readState, tuiPaneId } = require('../lib/ocmux.js');
23
22
  const { executeTuiCommand } = require('../lib/tui-cmd.js');
23
+ const { getUpdates, downloadTelegramFile, sendTelegramDocument } = require('../lib/telegram-api.js');
24
+ const { sendLongMessage } = require('../lib/telegram-format.js');
24
25
 
25
26
  // Persist chat↔server directory mappings across restarts
26
27
  function saveConnection(chatId, threadId, dir) {
@@ -144,123 +145,7 @@ function flushRecorded(chatId, threadId) {
144
145
  // Telegram Bot API helpers
145
146
  // ---------------------------------------------------------------------------
146
147
 
147
- function telegramRequest(token, method, params) {
148
- return new Promise((resolve, reject) => {
149
- const body = JSON.stringify(params || {});
150
- const options = {
151
- hostname: 'api.telegram.org',
152
- port: 443,
153
- path: `/bot${token}/${method}`,
154
- method: 'POST',
155
- headers: {
156
- 'Content-Type': 'application/json',
157
- 'Content-Length': Buffer.byteLength(body),
158
- },
159
- };
160
- const req = https.request(options, (res) => {
161
- let data = '';
162
- res.on('data', (chunk) => { data += chunk; });
163
- res.on('end', () => {
164
- try {
165
- resolve(JSON.parse(data));
166
- } catch (e) {
167
- reject(new Error(`Failed to parse Telegram response: ${data}`));
168
- }
169
- });
170
- });
171
- req.on('error', reject);
172
- req.write(body);
173
- req.end();
174
- });
175
- }
176
-
177
- function getUpdates(token, offset, timeout) {
178
- return telegramRequest(token, 'getUpdates', {
179
- offset,
180
- timeout: timeout != null ? timeout : 30,
181
- allowed_updates: ['message'],
182
- });
183
- }
184
-
185
- function sendTelegramMessage(token, chatId, text, parseMode, replyTo, threadId) {
186
- const params = { chat_id: chatId, text };
187
- if (parseMode) params.parse_mode = parseMode;
188
- if (replyTo) params.reply_to_message_id = replyTo;
189
- if (threadId) params.message_thread_id = threadId;
190
- return telegramRequest(token, 'sendMessage', params);
191
- }
192
-
193
148
  // Lightweight Markdown to Telegram HTML conversion.
194
- function convertMarkdownToHtml(text) {
195
- const parts = text.split('```');
196
-
197
- return parts.map((part, i) => {
198
- if (i % 2 === 1) {
199
- // Code block ─ strip language tag, escape entities, wrap in <pre>
200
- const nl = part.indexOf('\n');
201
- const inner = nl >= 0 ? part.slice(nl + 1) : part;
202
- return '<pre>' + inner
203
- .replace(/&/g, '&amp;')
204
- .replace(/</g, '&lt;')
205
- .replace(/>/g, '&gt;') + '</pre>';
206
- }
207
-
208
- // Text segment
209
-
210
- // Phase 1: extract table blocks into placeholders
211
- const placeholders = [];
212
- let processed = '';
213
- const lines = part.split('\n');
214
- let li = 0;
215
- while (li < lines.length) {
216
- if (/^\|/.test(lines[li])) {
217
- const table = [];
218
- while (li < lines.length && /^\|/.test(lines[li])) {
219
- table.push(lines[li]);
220
- li++;
221
- }
222
- const hasSep = table.some(l => /^\|[-:| ]+\|$/.test(l));
223
- if (table.length >= 2 && hasSep) {
224
- const cleaned = table
225
- .map(l => l.replace(/^\|/, '').replace(/\|$/, ''))
226
- .filter(l => !/^\s*[|:\- ]+\s*$/.test(l));
227
- if (cleaned.length > 0) {
228
- const escaped = cleaned.map(l =>
229
- l.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
230
- );
231
- const idx = placeholders.length;
232
- placeholders.push('<pre>' + escaped.join('\n') + '</pre>');
233
- processed += '\0T' + idx + '\0\n';
234
- continue;
235
- }
236
- }
237
- for (const l of table) { processed += l + '\n'; }
238
- } else {
239
- processed += lines[li] + '\n';
240
- li++;
241
- }
242
- }
243
-
244
- // Phase 2: escape entities and apply markdown on non-table text
245
- let html = processed
246
- .replace(/&/g, '&amp;')
247
- .replace(/</g, '&lt;')
248
- .replace(/>/g, '&gt;');
249
- html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
250
- html = html.replace(/^#{1,6}\s+(.+)$/gm, '<b>$1</b>');
251
- html = html.replace(/^\s*-{3,}\s*$/gm, '────────────────────');
252
- html = html.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
253
- html = html.replace(/\*(.+?)\*/g, '<i>$1</i>');
254
-
255
- // Phase 3: restore table placeholders
256
- for (let j = 0; j < placeholders.length; j++) {
257
- html = html.split('\0T' + j + '\0').join(placeholders[j]);
258
- }
259
-
260
- return html;
261
- }).join('');
262
- }
263
-
264
149
  // Parse TGAGENTP_ALLOWED_CHAT_IDS env var (comma-separated chat IDs).
265
150
  // Returns null when unset (all chats allowed), or an array of IDs to filter.
266
151
  function parseAllowlist(raw) {
@@ -269,43 +154,6 @@ function parseAllowlist(raw) {
269
154
  return ids.length > 0 ? ids : null;
270
155
  }
271
156
 
272
- // Split text into chunks of at most maxLength characters,
273
- // breaking at newlines when possible.
274
- function splitMessage(text, maxLength = 4096) {
275
- if (text.length <= maxLength) return [text];
276
- const chunks = [];
277
- let start = 0;
278
- while (start < text.length) {
279
- let end = Math.min(start + maxLength, text.length);
280
- if (end < text.length) {
281
- const newlinePos = text.lastIndexOf('\n', end);
282
- if (newlinePos > start) {
283
- end = newlinePos + 1;
284
- }
285
- }
286
- chunks.push(text.slice(start, end));
287
- start = end;
288
- }
289
- return chunks;
290
- }
291
-
292
- // Send a text message to a Telegram chat, splitting into multiple
293
- // messages if it exceeds the 4096-character limit.
294
- // Converts Markdown to Telegram HTML formatting.
295
- // Splits the raw text first, then converts each chunk independently
296
- // to avoid broken HTML tags across messages.
297
- async function sendLongMessage(token, chatIdOrState, text, replyTo, threadId) {
298
- // Accept either (token, chatState, ...) or (token, rawChatId, ...)
299
- const chatId = typeof chatIdOrState === 'object' && chatIdOrState.chatId != null
300
- ? chatIdOrState.chatId : chatIdOrState;
301
- const effectiveThreadId = threadId || (typeof chatIdOrState === 'object' ? chatIdOrState.threadId : undefined);
302
- const chunks = splitMessage(text);
303
- for (let i = 0; i < chunks.length; i++) {
304
- const html = convertMarkdownToHtml(chunks[i]);
305
- await sendTelegramMessage(token, chatId, html, 'HTML', i === 0 ? replyTo : undefined, effectiveThreadId);
306
- }
307
- }
308
-
309
157
  // Build a quoted reply context from a Telegram reply_to_message.
310
158
  // Returns empty string if the message isn't a reply or has no text.
311
159
  // Uses a dynamic code-fence length (max consecutive backticks + 1)
@@ -332,24 +180,6 @@ function quoteReply(msg) {
332
180
  // File upload helpers
333
181
  // ---------------------------------------------------------------------------
334
182
 
335
- // Download a file from Telegram's file storage.
336
- // Returns a Buffer of the file contents.
337
- async function downloadTelegramFile(token, fileId) {
338
- const fileInfo = await telegramRequest(token, 'getFile', { file_id: fileId });
339
- if (!fileInfo.ok || !fileInfo.result?.file_path) {
340
- throw new Error('Failed to get file info from Telegram');
341
- }
342
- const filePath = fileInfo.result.file_path;
343
- return new Promise((resolve, reject) => {
344
- const req = https.get(`https://api.telegram.org/file/bot${token}/${filePath}`, (res) => {
345
- const chunks = [];
346
- res.on('data', (chunk) => chunks.push(chunk));
347
- res.on('end', () => resolve(Buffer.concat(chunks)));
348
- });
349
- req.on('error', reject);
350
- });
351
- }
352
-
353
183
  // Handle an uploaded file from Telegram.
354
184
  // Downloads the file, saves it, and returns a notification message.
355
185
  async function handleFileUpload(token, msg, projectDir) {
@@ -1160,69 +990,6 @@ function handleSendFileRequest(req, res, token, serverBaseResolver) {
1160
990
  });
1161
991
  }
1162
992
 
1163
- // Send a document to Telegram using multipart/form-data.
1164
- async function sendTelegramDocument(token, chatIdOrState, fileData, fileName, caption) {
1165
- const chatId = typeof chatIdOrState === 'object' && chatIdOrState.chatId != null
1166
- ? chatIdOrState.chatId : chatIdOrState;
1167
- const threadId = typeof chatIdOrState === 'object' ? chatIdOrState.threadId : undefined;
1168
-
1169
- const boundary = '----tgagentpBoundary' + Math.random().toString(36).slice(2);
1170
- const parts = [];
1171
-
1172
- // chat_id field
1173
- parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="chat_id"\r\n\r\n${chatId}\r\n`));
1174
-
1175
- // message_thread_id field (if present)
1176
- if (threadId) {
1177
- parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="message_thread_id"\r\n\r\n${threadId}\r\n`));
1178
- }
1179
-
1180
- // document field (the file)
1181
- const mimeType = 'application/octet-stream';
1182
- parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="document"; filename="${fileName}"\r\nContent-Type: ${mimeType}\r\n\r\n`));
1183
- parts.push(fileData);
1184
- parts.push(Buffer.from('\r\n'));
1185
-
1186
- // caption field (optional)
1187
- if (caption) {
1188
- parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="caption"\r\n\r\n${caption}\r\n`));
1189
- }
1190
-
1191
- // closing boundary
1192
- parts.push(Buffer.from(`--${boundary}--\r\n`));
1193
-
1194
- const body = Buffer.concat(parts);
1195
-
1196
- return new Promise((resolve, reject) => {
1197
- const options = {
1198
- hostname: 'api.telegram.org',
1199
- port: 443,
1200
- path: `/bot${token}/sendDocument`,
1201
- method: 'POST',
1202
- headers: {
1203
- 'Content-Type': `multipart/form-data; boundary=${boundary}`,
1204
- 'Content-Length': body.length,
1205
- },
1206
- };
1207
- const req = https.request(options, (res) => {
1208
- let data = '';
1209
- res.on('data', (chunk) => { data += chunk; });
1210
- res.on('end', () => {
1211
- try {
1212
- const result = JSON.parse(data);
1213
- if (result.ok) resolve(result);
1214
- else reject(new Error(result.description || 'Telegram API error'));
1215
- } catch (e) {
1216
- reject(new Error(`Failed to parse Telegram response: ${data}`));
1217
- }
1218
- });
1219
- });
1220
- req.on('error', reject);
1221
- req.write(body);
1222
- req.end();
1223
- });
1224
- }
1225
-
1226
993
  async function cmdAllow(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
1227
994
  const st = getState(chatState.serverBase);
1228
995
  if (!st.pendingPermission) {
@@ -1866,7 +1633,8 @@ async function main() {
1866
1633
  sendLongMessage(token, chatState, html, replyTo, threadId).catch(() => {});
1867
1634
  thinkingBuf = '';
1868
1635
  }
1869
- if (cancelRef.current) { cancelRef.current.destroy(); cancelRef.current = null; }
1636
+ // Keep the SSE listener alive so subsequent permission/question events
1637
+ // that arrive after sendToSession returns are still forwarded to Telegram.
1870
1638
  } else {
1871
1639
  await sendText(url, `[Telegram] ${text}`);
1872
1640
  answer = await listenForFinalAnswer(url, null, cancelRef);
@@ -1924,7 +1692,10 @@ async function main() {
1924
1692
  }
1925
1693
  } finally {
1926
1694
  st.pendingPermission = null;
1927
- if (st._cancelRef === cancelRef) st._cancelRef = null;
1695
+ // Only clear the reference if the SSE listener was destroyed or finished.
1696
+ // If it is still running, keep the reference so the next message call
1697
+ // can destroy it (lines 1557–1559).
1698
+ if (st._cancelRef === cancelRef && !cancelRef.current) st._cancelRef = null;
1928
1699
  if (st._sessionReq === sessionCancelRef) st._sessionReq = null;
1929
1700
  if (!isCancelled()) {
1930
1701
  getState(url).busy = false;
@@ -2039,8 +1810,12 @@ async function main() {
2039
1810
  }
2040
1811
  st.serverDead = false;
2041
1812
  }
1813
+ const intro = 'The following information is for your awareness. Reply with only "Ack" and do not take any action:';
1814
+ const noteText = intro + '\n\n' + text;
2042
1815
  if (st.busy) {
2043
- await sendLongMessage(token, chatState, '⏳ Server is busy. Use `/cancel` to cancel or note again later.');
1816
+ if (!st.messageQueue) st.messageQueue = [];
1817
+ st.messageQueue.push({ text: noteText, replyTo: null });
1818
+ await sendLongMessage(token, chatState, `⏳ Server is busy. Note queued (#${st.messageQueue.length}).`);
2044
1819
  return;
2045
1820
  }
2046
1821
 
@@ -2049,11 +1824,10 @@ async function main() {
2049
1824
  const srvMatch = srvList.find(s => normalizeUrl(s.url) === normalizeUrl(url));
2050
1825
  if (srvMatch) activateServer(srvMatch.dir, srvMatch.index, srvMatch.url);
2051
1826
 
2052
- const intro = 'The following information is for your awareness. Reply with only "Ack" and do not take any action:';
2053
1827
  st.busy = true;
2054
1828
  const myTaskId = ++st.taskId;
2055
1829
  log.debug(` → /note: starting background processing (task ${myTaskId})`);
2056
- processMessageAsync(chatId, intro + '\n\n' + text, url, !!st.activeSessionId, st.activeSessionId, myTaskId, null, chatState.threadId);
1830
+ processMessageAsync(chatId, noteText, url, !!st.activeSessionId, st.activeSessionId, myTaskId, null, chatState.threadId);
2057
1831
  };
2058
1832
 
2059
1833
  const cmds = { ...COMMANDS, status: cmdStatus, cancel: cmdCancel, note: cmdNote };
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ const https = require('https');
4
+
5
+ // Make a JSON request to the Telegram Bot API.
6
+ function telegramRequest(token, method, params) {
7
+ return new Promise((resolve, reject) => {
8
+ const body = JSON.stringify(params || {});
9
+ const options = {
10
+ hostname: 'api.telegram.org',
11
+ port: 443,
12
+ path: `/bot${token}/${method}`,
13
+ method: 'POST',
14
+ headers: {
15
+ 'Content-Type': 'application/json',
16
+ 'Content-Length': Buffer.byteLength(body),
17
+ },
18
+ };
19
+ const req = https.request(options, (res) => {
20
+ let data = '';
21
+ res.on('data', (chunk) => { data += chunk; });
22
+ res.on('end', () => {
23
+ try {
24
+ resolve(JSON.parse(data));
25
+ } catch (e) {
26
+ reject(new Error(`Failed to parse Telegram response: ${data}`));
27
+ }
28
+ });
29
+ });
30
+ req.on('error', reject);
31
+ req.write(body);
32
+ req.end();
33
+ });
34
+ }
35
+
36
+ function getUpdates(token, offset, timeout) {
37
+ return telegramRequest(token, 'getUpdates', {
38
+ offset,
39
+ timeout: timeout != null ? timeout : 30,
40
+ allowed_updates: ['message'],
41
+ });
42
+ }
43
+
44
+ function sendTelegramMessage(token, chatId, text, parseMode, replyTo, threadId) {
45
+ const params = { chat_id: chatId, text };
46
+ if (parseMode) params.parse_mode = parseMode;
47
+ if (replyTo) params.reply_to_message_id = replyTo;
48
+ if (threadId) params.message_thread_id = threadId;
49
+ return telegramRequest(token, 'sendMessage', params);
50
+ }
51
+
52
+ // Download a file from Telegram's file storage.
53
+ // Returns a Buffer of the file contents.
54
+ async function downloadTelegramFile(token, fileId) {
55
+ const fileInfo = await telegramRequest(token, 'getFile', { file_id: fileId });
56
+ if (!fileInfo.ok || !fileInfo.result?.file_path) {
57
+ throw new Error('Failed to get file info from Telegram');
58
+ }
59
+ const filePath = fileInfo.result.file_path;
60
+ return new Promise((resolve, reject) => {
61
+ const req = https.get(`https://api.telegram.org/file/bot${token}/${filePath}`, (res) => {
62
+ const chunks = [];
63
+ res.on('data', (chunk) => chunks.push(chunk));
64
+ res.on('end', () => resolve(Buffer.concat(chunks)));
65
+ });
66
+ req.on('error', reject);
67
+ });
68
+ }
69
+
70
+ // Send a document to Telegram using multipart/form-data.
71
+ async function sendTelegramDocument(token, chatIdOrState, fileData, fileName, caption) {
72
+ const chatId = typeof chatIdOrState === 'object' && chatIdOrState.chatId != null
73
+ ? chatIdOrState.chatId : chatIdOrState;
74
+ const threadId = typeof chatIdOrState === 'object' ? chatIdOrState.threadId : undefined;
75
+
76
+ const boundary = '----tgagentpBoundary' + Math.random().toString(36).slice(2);
77
+ const parts = [];
78
+
79
+ // chat_id field
80
+ parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="chat_id"\r\n\r\n${chatId}\r\n`));
81
+
82
+ // message_thread_id field (if present)
83
+ if (threadId) {
84
+ parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="message_thread_id"\r\n\r\n${threadId}\r\n`));
85
+ }
86
+
87
+ // document field (the file)
88
+ const mimeType = 'application/octet-stream';
89
+ parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="document"; filename="${fileName}"\r\nContent-Type: ${mimeType}\r\n\r\n`));
90
+ parts.push(fileData);
91
+ parts.push(Buffer.from('\r\n'));
92
+
93
+ // caption field (optional)
94
+ if (caption) {
95
+ parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="caption"\r\n\r\n${caption}\r\n`));
96
+ }
97
+
98
+ // closing boundary
99
+ parts.push(Buffer.from(`--${boundary}--\r\n`));
100
+
101
+ const body = Buffer.concat(parts);
102
+
103
+ return new Promise((resolve, reject) => {
104
+ const options = {
105
+ hostname: 'api.telegram.org',
106
+ port: 443,
107
+ path: `/bot${token}/sendDocument`,
108
+ method: 'POST',
109
+ headers: {
110
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
111
+ 'Content-Length': body.length,
112
+ },
113
+ };
114
+ const req = https.request(options, (res) => {
115
+ let data = '';
116
+ res.on('data', (chunk) => { data += chunk; });
117
+ res.on('end', () => {
118
+ try {
119
+ const result = JSON.parse(data);
120
+ if (result.ok) resolve(result);
121
+ else reject(new Error(result.description || 'Telegram API error'));
122
+ } catch (e) {
123
+ reject(new Error(`Failed to parse Telegram response: ${data}`));
124
+ }
125
+ });
126
+ });
127
+ req.on('error', reject);
128
+ req.write(body);
129
+ req.end();
130
+ });
131
+ }
132
+
133
+ module.exports = {
134
+ telegramRequest,
135
+ getUpdates,
136
+ sendTelegramMessage,
137
+ downloadTelegramFile,
138
+ sendTelegramDocument,
139
+ };
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ const { sendTelegramMessage } = require('./telegram-api.js');
4
+
5
+ // Lightweight Markdown to Telegram HTML conversion.
6
+ function convertMarkdownToHtml(text) {
7
+ const parts = text.split('```');
8
+
9
+ return parts.map((part, i) => {
10
+ if (i % 2 === 1) {
11
+ // Code block ─ strip language tag, escape entities, wrap in <pre>
12
+ const nl = part.indexOf('\n');
13
+ const inner = nl >= 0 ? part.slice(nl + 1) : part;
14
+ return '<pre>' + inner
15
+ .replace(/&/g, '&amp;')
16
+ .replace(/</g, '&lt;')
17
+ .replace(/>/g, '&gt;') + '</pre>';
18
+ }
19
+
20
+ // Text segment
21
+
22
+ // Phase 1: extract table blocks into placeholders
23
+ const placeholders = [];
24
+ let processed = '';
25
+ const lines = part.split('\n');
26
+ let li = 0;
27
+ while (li < lines.length) {
28
+ if (/^\|/.test(lines[li])) {
29
+ const table = [];
30
+ while (li < lines.length && /^\|/.test(lines[li])) {
31
+ table.push(lines[li]);
32
+ li++;
33
+ }
34
+ const hasSep = table.some(l => /^\|[-:| ]+\|$/.test(l));
35
+ if (table.length >= 2 && hasSep) {
36
+ const cleaned = table
37
+ .map(l => l.replace(/^\|/, '').replace(/\|$/, ''))
38
+ .filter(l => !/^\s*[|:\- ]+\s*$/.test(l));
39
+ if (cleaned.length > 0) {
40
+ const escaped = cleaned.map(l =>
41
+ l.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
42
+ );
43
+ const idx = placeholders.length;
44
+ placeholders.push('<pre>' + escaped.join('\n') + '</pre>');
45
+ processed += '\0T' + idx + '\0\n';
46
+ continue;
47
+ }
48
+ }
49
+ for (const l of table) { processed += l + '\n'; }
50
+ } else {
51
+ processed += lines[li] + '\n';
52
+ li++;
53
+ }
54
+ }
55
+
56
+ // Phase 2: escape entities and apply markdown on non-table text
57
+ let html = processed
58
+ .replace(/&/g, '&amp;')
59
+ .replace(/</g, '&lt;')
60
+ .replace(/>/g, '&gt;');
61
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
62
+ html = html.replace(/^#{1,6}\s+(.+)$/gm, '<b>$1</b>');
63
+ html = html.replace(/^\s*-{3,}\s*$/gm, '────────────────────');
64
+ html = html.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
65
+ html = html.replace(/\*(.+?)\*/g, '<i>$1</i>');
66
+
67
+ // Phase 3: restore table placeholders
68
+ for (let j = 0; j < placeholders.length; j++) {
69
+ html = html.split('\0T' + j + '\0').join(placeholders[j]);
70
+ }
71
+
72
+ return html;
73
+ }).join('');
74
+ }
75
+
76
+ // Split text into chunks of at most maxLength characters,
77
+ // breaking at newlines when possible.
78
+ function splitMessage(text, maxLength = 4096) {
79
+ if (text.length <= maxLength) return [text];
80
+ const chunks = [];
81
+ let start = 0;
82
+ while (start < text.length) {
83
+ let end = Math.min(start + maxLength, text.length);
84
+ if (end < text.length) {
85
+ const newlinePos = text.lastIndexOf('\n', end);
86
+ if (newlinePos > start) {
87
+ end = newlinePos + 1;
88
+ }
89
+ }
90
+ chunks.push(text.slice(start, end));
91
+ start = end;
92
+ }
93
+ return chunks;
94
+ }
95
+
96
+ // Send a text message to a Telegram chat, splitting into multiple
97
+ // messages if it exceeds the 4096-character limit.
98
+ // Converts Markdown to Telegram HTML formatting.
99
+ // Splits the raw text first, then converts each chunk independently
100
+ // to avoid broken HTML tags across messages.
101
+ async function sendLongMessage(token, chatIdOrState, text, replyTo, threadId) {
102
+ // Accept either (token, chatState, ...) or (token, rawChatId, ...)
103
+ const chatId = typeof chatIdOrState === 'object' && chatIdOrState.chatId != null
104
+ ? chatIdOrState.chatId : chatIdOrState;
105
+ const effectiveThreadId = threadId || (typeof chatIdOrState === 'object' ? chatIdOrState.threadId : undefined);
106
+ const chunks = splitMessage(text);
107
+ for (let i = 0; i < chunks.length; i++) {
108
+ const html = convertMarkdownToHtml(chunks[i]);
109
+ await sendTelegramMessage(token, chatId, html, 'HTML', i === 0 ? replyTo : undefined, effectiveThreadId);
110
+ }
111
+ }
112
+
113
+ module.exports = {
114
+ convertMarkdownToHtml,
115
+ splitMessage,
116
+ sendLongMessage,
117
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentp",
3
- "version": "0.11.4",
3
+ "version": "0.11.5",
4
4
  "description": "Pipe prompts into a running OpenCode TUI session and stream the final answer",
5
5
  "bin": {
6
6
  "agentp": "bin/agentp",