agentp 0.11.6 → 0.11.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,9 +7,9 @@
7
7
 
8
8
  This package provides three CLI tools:
9
9
 
10
- - **`agentp`** — pipes prompt text into a running OpenCode TUI session and streams the assistant final answer back to stdout
10
+ - **`agentp`** — pipes prompt text into a running OpenCode server and streams the assistant final answer back to stdout
11
11
  - **`ocmux`** — manages OpenCode server + TUI sessions in tmux (create, switch, kill, list)
12
- - **`tgagentp`** — bridges a Telegram bot chat with all running OpenCode TUI sessions (receives messages from Telegram, routes them to the active server, sends answers back). Supports slash commands for multi-server management, session switching, agent/model listing, and remote restart.
12
+ - **`tgagentp`** — bridges a Telegram bot chat with all running OpenCode servers (receives messages from Telegram, routes them to the active server, sends answers back). Supports slash commands for multi-server management, session switching, agent/model listing, including file sharing from the chat.
13
13
 
14
14
  It is designed for prompt-driven workflows where you want to do things like:
15
15
 
@@ -18,6 +18,10 @@ It is designed for prompt-driven workflows where you want to do things like:
18
18
  - capture output in files or pipe to other tools
19
19
  - **drive prompts directly from editors** like Vim/Neovim
20
20
 
21
+ ## Author's note
22
+
23
+ These tools are built for my own daily workflow. They are heavily AI-assisted — including the tests — and I review things before shipping, but the real test is using them every day. Bugs happen; I value a working feature more than a flawless one. MIT license, no warranty. Issues, suggestions, and PRs are welcome.
24
+
21
25
  ## Install
22
26
 
23
27
  From npm:
@@ -57,6 +61,8 @@ Options:
57
61
  - `--no-tg`: do not forward to Telegram
58
62
  - `--flush`: flush tgagentp's recorded buffer without prepending it to output
59
63
  - `--getLast <n>`: retrieve last n assistant answers from session history
64
+ - `--session <name>`: target a specific session by name (exact or partial match)
65
+ - `--new`: create a new session with the given title (requires `--session`)
60
66
  - `--version`: show version
61
67
  - `--help`: show help message
62
68
 
@@ -146,8 +152,6 @@ Useful to grab recent answers without sending a new prompt.
146
152
 
147
153
  ## ocmux
148
154
 
149
- ## ocmux
150
-
151
155
  Manage OpenCode server + TUI in tmux for project directories.
152
156
 
153
157
  ```bash
@@ -231,6 +235,8 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
231
235
  | `/help [topic]` | Show general help or help for a topic (`servers`, `sessions`, `agents`, `models`, `allow`, `think`, `record`, `queue`) |
232
236
  | `/servers` | List all running ocmux-served projects with URL + status (✅ idle / ⏳ busy) |
233
237
  | `/servers switch <name> [--force]` | Switch active server; matches by full path, basename, or substring; `--force` takes over from another chat |
238
+ | `/serve <path>` | Start a server in an existing directory under `TGAGENTP_ROOT` |
239
+ | `/new <path>` | Create a directory, init git, and start a server under `TGAGENTP_ROOT` |
234
240
  | `/sessions` | List recent sessions for the current server (max 50, with date headings) |
235
241
  | `/sessions switch <number\|name>` | Switch active session by position or partial name match |
236
242
  | `/agents` | List primary agents (▶ marker for the active one) |
@@ -247,6 +253,7 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
247
253
  | `/reject` | Deny a permission request |
248
254
  | `/always` | Approve and remember for the session |
249
255
  | `/answer <number>` | Respond to a question asked by the AI (structured multiple-choice) |
256
+ | `/markdown` | Send the original markdown of the last response as a `.md` file; reply to a message to get that specific response |
250
257
  | `/shutdown [force\|clear]` | (requires `--dev`) Stop tgagentp; `clear` also wipes saved connections |
251
258
 
252
259
  #### TUI Command Passthrough
@@ -324,6 +331,7 @@ Chat-to-server directory mappings are saved to `/tmp/tgagentp-connections.json`
324
331
  | `TGAGENTP_ALLOWED_CHAT_IDS` | Optional. Comma-separated Telegram chat IDs that are allowed to use the bot. |
325
332
  | `TGAGENTP_PORT` | Optional. Agentp gateway listen port (default: `0` = random). |
326
333
  | `TGAGENTP_DEBOUNCE_MS` | Optional. Debounce interval for queued-message notifications (default: `5000`). |
334
+ | `TGAGENTP_ROOT` | Optional. Root directory for `/serve` and `/new` commands (must be writable). |
327
335
 
328
336
  ## License
329
337
 
package/bin/tgagentp CHANGED
@@ -22,6 +22,27 @@ const { listServers, activateServer, resurrectServer, statefileFor, readState, t
22
22
  const { executeTuiCommand } = require('../lib/tui-cmd.js');
23
23
  const { getUpdates, downloadTelegramFile, sendTelegramDocument } = require('../lib/telegram-api.js');
24
24
  const { sendLongMessage } = require('../lib/telegram-format.js');
25
+ const { spawnSync } = require('child_process');
26
+
27
+ const TGAGENTP_ROOT = process.env.TGAGENTP_ROOT;
28
+ let tgagentpRootValid = false;
29
+ if (TGAGENTP_ROOT) {
30
+ try {
31
+ const stats = fs.statSync(TGAGENTP_ROOT);
32
+ if (!stats.isDirectory()) {
33
+ log.error(`TGAGENTP_ROOT is not a directory: ${TGAGENTP_ROOT} — /serve and /new disabled`);
34
+ } else {
35
+ try {
36
+ fs.accessSync(TGAGENTP_ROOT, fs.constants.W_OK);
37
+ tgagentpRootValid = true;
38
+ } catch {
39
+ log.error(`TGAGENTP_ROOT is not writable: ${TGAGENTP_ROOT} — /serve and /new disabled`);
40
+ }
41
+ }
42
+ } catch {
43
+ log.error(`TGAGENTP_ROOT does not exist: ${TGAGENTP_ROOT} — /serve and /new disabled`);
44
+ }
45
+ }
25
46
 
26
47
  // Persist chat↔server directory mappings across restarts
27
48
  function saveConnection(chatId, threadId, dir) {
@@ -141,6 +162,33 @@ function flushRecorded(chatId, threadId) {
141
162
  return msgs;
142
163
  }
143
164
 
165
+ // Ring buffer of recent responses keyed by Telegram message ID.
166
+ // Stored per chatId+threadId for /markdown command lookups.
167
+ const responseHistory = {}; // convKey → [{ messageId, text }]
168
+
169
+ function recordResponseMessage(chatId, threadId, messageIds, text) {
170
+ const key = convKey(chatId, threadId);
171
+ if (!responseHistory[key]) responseHistory[key] = [];
172
+ responseHistory[key].push({ messageIds, text });
173
+ if (responseHistory[key].length > 20) responseHistory[key].shift();
174
+ }
175
+
176
+ function getResponseByMessageId(chatId, threadId, messageId) {
177
+ const key = convKey(chatId, threadId);
178
+ const list = responseHistory[key] || [];
179
+ return list.find(e => e.messageIds.includes(messageId));
180
+ }
181
+
182
+ function getRecentResponse(chatId, threadId, index) {
183
+ const key = convKey(chatId, threadId);
184
+ const list = responseHistory[key] || [];
185
+ if (list.length === 0) return null;
186
+ // index 1 = most recent, 2 = second most recent, etc.
187
+ const idx = list.length - (index || 1);
188
+ if (idx < 0 || idx >= list.length) return null;
189
+ return list[idx];
190
+ }
191
+
144
192
  // ---------------------------------------------------------------------------
145
193
  // Telegram Bot API helpers
146
194
  // ---------------------------------------------------------------------------
@@ -158,7 +206,7 @@ function parseAllowlist(raw) {
158
206
  // Returns empty string if the message isn't a reply or has no text.
159
207
  // Uses a dynamic code-fence length (max consecutive backticks + 1)
160
208
  // so nested backticks never break the structure.
161
- const { ensureSharedDir, saveUploadedFile, formatFileSize } = require('../lib/file-share.js');
209
+ const { formatFileSize } = require('../lib/file-share.js');
162
210
 
163
211
  function quoteReply(msg) {
164
212
  const rm = msg.reply_to_message;
@@ -181,7 +229,7 @@ function quoteReply(msg) {
181
229
  // ---------------------------------------------------------------------------
182
230
 
183
231
  // Handle an uploaded file from Telegram.
184
- // Downloads the file, saves it, and returns a notification message.
232
+ // Returns a notification message with fileId so the agent can download it on demand.
185
233
  async function handleFileUpload(token, msg, projectDir) {
186
234
  const doc = msg.document;
187
235
  const photo = msg.photo;
@@ -196,18 +244,15 @@ async function handleFileUpload(token, msg, projectDir) {
196
244
  fileName = doc.file_name || 'upload.bin';
197
245
  fileSize = doc.file_size || 0;
198
246
  } else if (photo) {
199
- // Use the largest photo
200
247
  const largest = photo[photo.length - 1];
201
248
  fileId = largest.file_id;
202
249
  fileName = 'photo.jpg';
203
250
  fileSize = largest.file_size || 0;
204
251
  }
205
252
 
206
- const fileData = await downloadTelegramFile(token, fileId);
207
- ensureSharedDir(projectDir);
208
- const relativePath = saveUploadedFile(projectDir, fileName, fileData);
209
253
  const sizeStr = fileSize > 0 ? ` (${formatFileSize(fileSize)})` : '';
210
- return `📎 File uploaded: \`${relativePath}\`${sizeStr}`;
254
+ return `📎 File uploaded: \`${fileName}\`${sizeStr}\n` +
255
+ `Use \`[Telegram]{"command":"download","fileId":"${fileId}","path":"<destination>"}\` to save it.`;
211
256
  }
212
257
 
213
258
  // ---------------------------------------------------------------------------
@@ -362,8 +407,37 @@ async function cmdHelp(token, chatId, args, chatState) {
362
407
  'Max 100 messages / 100KB. Oldest messages are dropped first.');
363
408
  return;
364
409
  }
410
+ if (topic === 'serve') {
411
+ const hint = TGAGENTP_ROOT ? '' :
412
+ '\nℹ️ Currently disabled. Set the `TGAGENTP_ROOT` environment variable to a writable directory to enable `/serve` and `/new`.';
413
+ await sendLongMessage(token, chatState,
414
+ '`/serve <path>` ─ start a server in an existing directory\n\n' +
415
+ 'The path is relative to `TGAGENTP_ROOT`.\n' +
416
+ 'Example: `/serve projects/my-app`\n\n' +
417
+ 'Requires the `TGAGENTP_ROOT` environment variable to be set to a writable directory.' +
418
+ hint);
419
+ return;
420
+ }
421
+ if (topic === 'new') {
422
+ const hint = TGAGENTP_ROOT ? '' :
423
+ '\nℹ️ Currently disabled. Set the `TGAGENTP_ROOT` environment variable to a writable directory to enable `/serve` and `/new`.';
424
+ await sendLongMessage(token, chatState,
425
+ '`/new <path>` ─ create a directory, initialize git, and start a server\n\n' +
426
+ 'The path is relative to `TGAGENTP_ROOT`.\n' +
427
+ 'Example: `/new projects/my-app`\n\n' +
428
+ 'Requires the `TGAGENTP_ROOT` environment variable to be set to a writable directory.' +
429
+ hint);
430
+ return;
431
+ }
432
+ if (topic === 'markdown') {
433
+ await sendLongMessage(token, chatState,
434
+ '`/markdown` ─ send the original markdown of the most recent response as a `.md` file\n\n' +
435
+ 'Reply to any message and use `/markdown` to get the original markdown of the response that generated that message ' +
436
+ '(works even if the response was split into multiple Telegram messages).');
437
+ return;
438
+ }
365
439
  await sendLongMessage(token, chatState,
366
- 'Unknown topic. Try `/help servers`, `/help sessions`, `/help agents`, `/help models`, `/help allow`, `/help think`, `/help record`, `/help queue`, `/help flush`, `/help note`, `/help comment`, `/help disconnect`, or just `/help`.');
440
+ 'Unknown topic. Try `/help servers`, `/help sessions`, `/help agents`, `/help models`, `/help allow`, `/help think`, `/help record`, `/help queue`, `/help flush`, `/help note`, `/help comment`, `/help disconnect`, `/help serve`, `/help new`, `/help markdown`, or just `/help`.');
367
441
  return;
368
442
  }
369
443
  await sendLongMessage(token, chatState,
@@ -390,7 +464,12 @@ async function cmdHelp(token, chatId, args, chatState) {
390
464
  '`/comment <text>` ─ save a comment in the chat (not forwarded to the agent)\n' +
391
465
  '`/disconnect` ─ disconnect this chat from its server\n' +
392
466
  '`/allow` / `/reject` / `/always` ─ respond to permission requests\n' +
393
- '`/shutdown` ─ stop tgagentp (only with `--dev` flag)\n\n' +
467
+ '`/markdown` ─ send the original markdown of the last response as a `.md` file\n' +
468
+ '`/shutdown` ─ stop tgagentp (only with `--dev` flag)\n' +
469
+ (tgagentpRootValid
470
+ ? '`/serve <path>` ─ start a server in an existing directory\n' +
471
+ '`/new <path>` ─ create a directory, init git, and start a server\n\n'
472
+ : 'ℹ️ Set `TGAGENTP_ROOT` to a writable directory to enable `/serve` and `/new`.\n\n') +
394
473
  'Everything else is forwarded to the active server as `[Telegram] <text>`.');
395
474
  }
396
475
 
@@ -510,6 +589,7 @@ function sessionName(s) {
510
589
  }
511
590
 
512
591
  function normalizeUrl(url) {
592
+ if (!url) return url;
513
593
  return url.replace(/\/+$/, '').replace(/^http:\/\/127\.0\.0\.1:/, 'http://localhost:');
514
594
  }
515
595
 
@@ -553,7 +633,7 @@ async function cmdSessions(token, chatId, args, chatState, setServerForChat, set
553
633
  try {
554
634
  const session = await createSession(server, title);
555
635
  const name = session.title || session.slug || session.id;
556
- setActiveSessionForChat(chatState, session.id);
636
+ setActiveSessionForChat(chatState, session.id, name);
557
637
  selectSession(server, session.id);
558
638
  await sendLongMessage(token, chatState,
559
639
  `Created session: \`${name}\`\nIt is now the active session.`);
@@ -605,7 +685,7 @@ async function cmdSessions(token, chatId, args, chatState, setServerForChat, set
605
685
  const num = parseInt(query, 10);
606
686
  if (/^\d+$/.test(query) && num >= 1 && num <= sorted.length) {
607
687
  const target = sorted[num - 1];
608
- setActiveSessionForChat(chatId, target.id);
688
+ setActiveSessionForChat(chatState, target.id, sessionName(target));
609
689
  selectSession(server, target.id);
610
690
  await sendLongMessage(token, chatState,
611
691
  `Switched to session #${num}: \`${sessionName(target)}\`\nSubsequent messages will be sent to this session.`);
@@ -627,7 +707,7 @@ async function cmdSessions(token, chatId, args, chatState, setServerForChat, set
627
707
  return;
628
708
  }
629
709
  const target = matches[0];
630
- setActiveSessionForChat(chatId, target.id);
710
+ setActiveSessionForChat(chatState, target.id);
631
711
  selectSession(server, target.id);
632
712
  await sendLongMessage(token, chatState,
633
713
  `Switched to session: \`${sessionName(target)}\`\nSubsequent messages will be sent to this session.`);
@@ -657,10 +737,13 @@ async function cmdSessions(token, chatId, args, chatState, setServerForChat, set
657
737
  return;
658
738
  }
659
739
 
660
- // Show user sessions (have agent or directory) and always include the active session
740
+ // Show primary-agent sessions and always include the active session
741
+ const agents = await listAgents(server).catch(() => []);
742
+ const primaryAgents = new Set(agents.filter(a => a.mode === 'primary').map(a => a.name));
743
+ const hasPrimaryAgent = (s) => s.agent && primaryAgents.has(s.agent);
661
744
  const activeId = getState(server).activeSessionId;
662
745
  const filtered = sessions.filter(s =>
663
- (s.agent || s.directory || s.id === activeId) && (!projectDir || s.directory === projectDir));
746
+ (hasPrimaryAgent(s) || s.id === activeId) && (!projectDir || s.directory === projectDir));
664
747
  const sorted = sortedSessions(filtered, 50);
665
748
  if (sorted.length === 0) {
666
749
  await sendLongMessage(token, chatState, 'No active sessions.');
@@ -844,8 +927,6 @@ function startAgentpGateway(token, serverBaseResolver) {
844
927
  }
845
928
  if (req.url === '/send') {
846
929
  handleSendRequest(req, res, token, serverBaseResolver);
847
- } else if (req.url === '/send-file') {
848
- handleSendFileRequest(req, res, token, serverBaseResolver);
849
930
  } else {
850
931
  res.writeHead(404);
851
932
  res.end('Not Found');
@@ -897,7 +978,9 @@ function handleSendRequest(req, res, token, serverBaseResolver) {
897
978
  const activeUrl = normalizeUrl(serverBaseResolver());
898
979
 
899
980
  if (targetUrl === activeUrl) {
900
- sendLongMessage(token, owningChatId, text.trim()).catch(() => {});
981
+ processTelegramCommands(text, server, token, owningChatId).then((cleaned) => {
982
+ if (cleaned) sendLongMessage(token, owningChatId, cleaned).catch(() => {});
983
+ });
901
984
  } else {
902
985
  if (!agentpQueues[server]) {
903
986
  agentpQueues[server] = { messages: [], notifyTimer: null, notified: false };
@@ -922,72 +1005,122 @@ function handleSendRequest(req, res, token, serverBaseResolver) {
922
1005
  });
923
1006
  }
924
1007
 
925
- function handleSendFileRequest(req, res, token, serverBaseResolver) {
926
- let body = '';
927
- req.on('data', (chunk) => { body += chunk; });
928
- req.on('end', () => {
929
- try {
930
- let data;
931
- try { data = JSON.parse(body); } catch {
932
- res.writeHead(400);
933
- res.end('Bad Request');
934
- return;
935
- }
936
- const { filePath, server, message } = data;
937
- if (typeof filePath !== 'string' || typeof server !== 'string') {
938
- res.writeHead(400);
939
- res.end('Bad Request: filePath and server are required');
940
- return;
941
- }
1008
+ // Scan response text for [Telegram]{...} structured messages, process them,
1009
+ // and return the text with those lines removed (or replaced, for help commands).
1010
+ // Supported commands:
1011
+ // "upload" — send a file to the Telegram chat.
1012
+ // Format: [Telegram]{"command":"upload","path":"<rel-path>","msg":"<caption>"}
1013
+ // "download" — save a Telegram file to the project directory.
1014
+ // Format: [Telegram]{"command":"download","fileId":"<id>","path":"<rel-path>"}
1015
+ // "help" — get help about available commands.
1016
+ // Format: [Telegram]{"command":"help","topic":"<upload|download|help>"}
1017
+ // The [Telegram]{...} must be the only content on its line.
1018
+
1019
+ // Parse a single [Telegram]{...} line. Returns { payload } or null.
1020
+ function parseTelegramLine(line) {
1021
+ const m = line.match(/^\[Telegram\](\{.*\})\s*$/);
1022
+ if (!m) return null;
1023
+ let payload;
1024
+ try { payload = JSON.parse(m[1]); } catch { return null; }
1025
+ if (!payload || typeof payload !== 'object') return null;
1026
+ return payload;
1027
+ }
942
1028
 
943
- // Find the chat that owns this server
944
- const targetUrl = normalizeUrl(server);
945
- const owningChatId = Object.entries(serverOwners).find(([url]) => normalizeUrl(url) === targetUrl)?.[1];
1029
+ const TELEGRAM_COMMANDS_HELP = {
1030
+ all: 'Available commands:\n\n' +
1031
+ '• upload — send a file to Telegram.\n' +
1032
+ ' [Telegram]{"command":"upload","path":"<relative-path>","msg":"<optional caption>"}\n\n' +
1033
+ '• download — save a file from Telegram to the project.\n' +
1034
+ ' [Telegram]{"command":"download","fileId":"<telegram-file-id>","path":"<relative-destination>"}\n\n' +
1035
+ '• help — show this help or details for a specific command.\n' +
1036
+ ' [Telegram]{"command":"help","topic":"<command>"}',
1037
+ upload: 'Upload a file to the Telegram chat.\n\n' +
1038
+ 'Format: [Telegram]{"command":"upload","path":"<relative-path>","msg":"<optional caption>"}\n\n' +
1039
+ '- path: file path relative to the project root (required)\n' +
1040
+ '- msg: optional caption sent with the file',
1041
+ download: 'Download a file the user uploaded to Telegram.\n\n' +
1042
+ 'Format: [Telegram]{"command":"download","fileId":"<telegram-file-id>","path":"<relative-destination>"}\n\n' +
1043
+ '- fileId: the Telegram file identifier (provided when the user uploads a file)\n' +
1044
+ '- path: destination path relative to the project root (required)',
1045
+ help: 'Get help about Telegram commands.\n\n' +
1046
+ 'Format: [Telegram]{"command":"help","topic":"<command>"}\n\n' +
1047
+ '- topic: one of "upload", "download", "help". Omit for general help.',
1048
+ };
946
1049
 
947
- if (!owningChatId) {
948
- res.writeHead(200, { 'Content-Type': 'application/json' });
949
- res.end(JSON.stringify({ sent: false, reason: 'no owner' }));
950
- return;
951
- }
1050
+ function getHelpText(topic) {
1051
+ if (topic && TELEGRAM_COMMANDS_HELP[topic]) return TELEGRAM_COMMANDS_HELP[topic];
1052
+ return TELEGRAM_COMMANDS_HELP.all;
1053
+ }
952
1054
 
953
- // Resolve the file path relative to the server directory
954
- const dir = serverDir(server);
955
- if (!dir) {
956
- res.writeHead(200, { 'Content-Type': 'application/json' });
957
- res.end(JSON.stringify({ sent: false, reason: 'no server dir' }));
958
- return;
1055
+ async function processTelegramCommands(text, server, token, chatState) {
1056
+ if (!text) return text;
1057
+ const lines = text.split('\n');
1058
+ const toStrip = new Set();
1059
+ const filesSent = [];
1060
+ const dir = serverDir(server);
1061
+
1062
+ for (let i = 0; i < lines.length; i++) {
1063
+ const payload = parseTelegramLine(lines[i]);
1064
+ if (!payload) continue;
1065
+
1066
+ if (payload.command === 'upload' && payload.path) {
1067
+ toStrip.add(i);
1068
+ const resolved = dir ? path.resolve(dir, payload.path) : path.resolve(payload.path);
1069
+ if (dir && !resolved.startsWith(dir + path.sep)) {
1070
+ log.error(`processTelegramCommands: path outside project rejected: ${payload.path}`);
1071
+ continue;
959
1072
  }
960
- const fullPath = path.resolve(dir, filePath);
961
- if (!fs.existsSync(fullPath)) {
962
- res.writeHead(200, { 'Content-Type': 'application/json' });
963
- res.end(JSON.stringify({ sent: false, reason: 'file not found' }));
964
- return;
1073
+ try {
1074
+ if (!fs.existsSync(resolved)) {
1075
+ log.error(`processTelegramCommands: file not found: ${resolved}`);
1076
+ continue;
1077
+ }
1078
+ const fileData = fs.readFileSync(resolved);
1079
+ const fileName = path.basename(resolved);
1080
+ const caption = typeof payload.msg === 'string' ? payload.msg : undefined;
1081
+ await sendTelegramDocument(token, chatState, fileData, fileName, caption);
1082
+ filesSent.push(fileName);
1083
+ } catch (err) {
1084
+ log.error(`processTelegramCommands: failed to send file ${resolved}: ${err.message}`);
1085
+ }
1086
+ } else if (payload.command === 'download' && payload.fileId && payload.path) {
1087
+ toStrip.add(i);
1088
+ const resolved = dir ? path.resolve(dir, payload.path) : path.resolve(payload.path);
1089
+ if (dir && !resolved.startsWith(dir + path.sep)) {
1090
+ log.error(`processTelegramCommands: download path outside project rejected: ${payload.path}`);
1091
+ continue;
965
1092
  }
1093
+ try {
1094
+ const fileData = await downloadTelegramFile(token, payload.fileId);
1095
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
1096
+ fs.writeFileSync(resolved, fileData);
1097
+ filesSent.push(path.basename(resolved));
1098
+ } catch (err) {
1099
+ log.error(`processTelegramCommands: failed to download file: ${err.message}`);
1100
+ }
1101
+ } else if (payload.command === 'help') {
1102
+ toStrip.add(i);
1103
+ const topic = typeof payload.topic === 'string' ? payload.topic : undefined;
1104
+ const st = getState(server);
1105
+ if (st.activeSessionId) {
1106
+ sendToSession(server, st.activeSessionId, getHelpText(topic)).catch(() => {});
1107
+ }
1108
+ } else {
1109
+ toStrip.add(i); // unrecognized command — strip silently
1110
+ }
1111
+ }
966
1112
 
967
- const fileData = fs.readFileSync(fullPath);
968
- const fileName = path.basename(fullPath);
1113
+ if (toStrip.size === 0) return text;
969
1114
 
970
- // Send file to Telegram
971
- sendTelegramDocument(token, owningChatId, fileData, fileName, message).then(() => {
972
- // Clean up: only delete files from telegram-shared/downloads/
973
- // to prevent accidental deletion of project files
974
- const downloadsDir = path.join(dir, 'telegram-shared', 'downloads');
975
- if (fullPath.startsWith(downloadsDir + path.sep)) {
976
- try { fs.unlinkSync(fullPath); } catch (_) {}
977
- }
978
- res.writeHead(200, { 'Content-Type': 'application/json' });
979
- res.end(JSON.stringify({ sent: true }));
980
- }).catch((err) => {
981
- log.error(`Failed to send file: ${err.message}`);
982
- res.writeHead(500, { 'Content-Type': 'application/json' });
983
- res.end(JSON.stringify({ sent: false, reason: err.message }));
984
- });
985
- } catch (err) {
986
- log.error(`Agentp gateway handler error: ${err.message}`);
987
- res.writeHead(500);
988
- res.end('Internal Server Error');
989
- }
990
- });
1115
+ if (filesSent.length > 0) {
1116
+ log.info(`processTelegramCommands: processed ${filesSent.length} file(s): ${filesSent.join(', ')}`);
1117
+ }
1118
+
1119
+ const out = [];
1120
+ for (let i = 0; i < lines.length; i++) {
1121
+ if (!toStrip.has(i)) out.push(lines[i]);
1122
+ }
1123
+ return out.join('\n').replace(/\n{3,}/g, '\n\n').trim();
991
1124
  }
992
1125
 
993
1126
  async function cmdAllow(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
@@ -1059,6 +1192,7 @@ async function cmdAnswer(token, chatId, args, chatState, setServerForChat, setAc
1059
1192
  async function cmdThink(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
1060
1193
  const st = getState(chatState.serverBase);
1061
1194
  const sub = args && args.length > 0 ? args[0].toLowerCase() : 'switch';
1195
+ const wasOn = st.thinkingEnabled;
1062
1196
  if (sub === 'on') {
1063
1197
  st.thinkingEnabled = true;
1064
1198
  await sendLongMessage(token, chatState, '💭 Thinking messages **on** (forwarded to chat).');
@@ -1072,6 +1206,13 @@ async function cmdThink(token, chatId, args, chatState, setServerForChat, setAct
1072
1206
  } else {
1073
1207
  await sendLongMessage(token, chatState, 'Usage: `/think [on|off|switch]` (default: switch).');
1074
1208
  }
1209
+ // When toggled on, flush any thinking text already buffered by the current request
1210
+ if (!wasOn && st.thinkingEnabled && st._thinkingBuf) {
1211
+ const buf = st._thinkingBuf;
1212
+ st._thinkingBuf = '';
1213
+ const html = '💭 ' + buf.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1214
+ sendLongMessage(token, chatState, html, null, chatState.threadId).catch(() => {});
1215
+ }
1075
1216
  }
1076
1217
 
1077
1218
  async function cmdQueue(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat, replyTo) {
@@ -1308,6 +1449,153 @@ async function cmdDisconnect(token, chatId, args, chatState) {
1308
1449
  log.info(`[chat ${chatId}] Disconnected from server: ${url}`);
1309
1450
  }
1310
1451
 
1452
+ async function cmdServe(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
1453
+ if (!tgagentpRootValid) {
1454
+ const msg = TGAGENTP_ROOT
1455
+ ? '❌ `TGAGENTP_ROOT` is not a valid writable directory.\nCheck the logs for details.'
1456
+ : '❌ `TGAGENTP_ROOT` environment variable is not set.\nSet it to a writable directory to enable `/serve` and `/new` commands.';
1457
+ await sendLongMessage(token, chatState, msg);
1458
+ return;
1459
+ }
1460
+ const relPath = args.join(' ').trim();
1461
+ if (!relPath) {
1462
+ await sendLongMessage(token, chatState,
1463
+ 'Usage: `/serve <path>`\n\n' +
1464
+ 'Run `ocmux serve` on a directory under `TGAGENTP_ROOT`.\n' +
1465
+ 'The path is relative to the root directory.');
1466
+ return;
1467
+ }
1468
+ const targetDir = path.resolve(TGAGENTP_ROOT, relPath);
1469
+ if (!targetDir.startsWith(TGAGENTP_ROOT)) {
1470
+ await sendLongMessage(token, chatState,
1471
+ '❌ Path must be within `TGAGENTP_ROOT`.\n' +
1472
+ `Requested: \`${relPath}\``);
1473
+ return;
1474
+ }
1475
+ try {
1476
+ const stats = fs.statSync(targetDir);
1477
+ if (!stats.isDirectory()) {
1478
+ await sendLongMessage(token, chatState,
1479
+ `❌ Not a directory: \`${relPath}\``);
1480
+ return;
1481
+ }
1482
+ } catch {
1483
+ await sendLongMessage(token, chatState,
1484
+ `❌ Directory does not exist: \`${relPath}\``);
1485
+ return;
1486
+ }
1487
+ const sf = statefileFor(targetDir);
1488
+ if (fs.existsSync(sf)) {
1489
+ await sendLongMessage(token, chatState,
1490
+ `⚠️ Server already exists for \`${relPath}\`.\n` +
1491
+ 'Use `/servers switch <name>` to connect to it.');
1492
+ return;
1493
+ }
1494
+ await sendLongMessage(token, chatState,
1495
+ `🚀 Starting server for \`${relPath}\`...`);
1496
+ const result = spawnSync('ocmux', ['serve', targetDir], { encoding: 'utf8', timeout: 15000 });
1497
+ if (result.status !== 0) {
1498
+ const err = result.stderr || result.stdout || 'unknown error';
1499
+ await sendLongMessage(token, chatState,
1500
+ `❌ Failed to start server:\n\`\`\`\n${err}\n\`\`\``);
1501
+ return;
1502
+ }
1503
+ const url = (result.stdout || '').trim();
1504
+ if (!url) {
1505
+ await sendLongMessage(token, chatState,
1506
+ '❌ Server started but did not return a URL.');
1507
+ return;
1508
+ }
1509
+ await setServerForChat(chatState, url);
1510
+ await sendLongMessage(token, chatState,
1511
+ `✅ Server started.\nURL: \`${url}\`\nDir: \`${targetDir}\``);
1512
+ log.info(`[chat ${chatId}] Served ${targetDir} → ${url}`);
1513
+ }
1514
+
1515
+ async function cmdNew(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
1516
+ if (!tgagentpRootValid) {
1517
+ const msg = TGAGENTP_ROOT
1518
+ ? '❌ `TGAGENTP_ROOT` is not a valid writable directory.\nCheck the logs for details.'
1519
+ : '❌ `TGAGENTP_ROOT` environment variable is not set.\nSet it to a writable directory to enable `/serve` and `/new` commands.';
1520
+ await sendLongMessage(token, chatState, msg);
1521
+ return;
1522
+ }
1523
+ const relPath = args.join(' ').trim();
1524
+ if (!relPath) {
1525
+ await sendLongMessage(token, chatState,
1526
+ 'Usage: `/new <path>`\n\n' +
1527
+ 'Create a new directory under `TGAGENTP_ROOT`, initialize git, and start a server.\n' +
1528
+ 'The path is relative to the root directory.');
1529
+ return;
1530
+ }
1531
+ const targetDir = path.resolve(TGAGENTP_ROOT, relPath);
1532
+ if (!targetDir.startsWith(TGAGENTP_ROOT)) {
1533
+ await sendLongMessage(token, chatState,
1534
+ '❌ Path must be within `TGAGENTP_ROOT`.\n' +
1535
+ `Requested: \`${relPath}\``);
1536
+ return;
1537
+ }
1538
+ if (fs.existsSync(targetDir)) {
1539
+ await sendLongMessage(token, chatState,
1540
+ `❌ Directory already exists: \`${relPath}\`.\n` +
1541
+ 'Use `/serve` to start a server in an existing directory.');
1542
+ return;
1543
+ }
1544
+ try {
1545
+ fs.mkdirSync(targetDir, { recursive: true });
1546
+ } catch (err) {
1547
+ await sendLongMessage(token, chatState,
1548
+ `❌ Failed to create directory: ${err.message}`);
1549
+ return;
1550
+ }
1551
+ const gitResult = spawnSync('git', ['init'], { cwd: targetDir, encoding: 'utf8', timeout: 10000 });
1552
+ if (gitResult.status !== 0) {
1553
+ const err = gitResult.stderr || gitResult.stdout || 'unknown error';
1554
+ await sendLongMessage(token, chatState,
1555
+ `❌ Failed to initialize git:\n\`\`\`\n${err}\n\`\`\``);
1556
+ return;
1557
+ }
1558
+ await sendLongMessage(token, chatState,
1559
+ `🚀 Starting server for \`${relPath}\`...`);
1560
+ const result = spawnSync('ocmux', ['serve', targetDir], { encoding: 'utf8', timeout: 15000 });
1561
+ if (result.status !== 0) {
1562
+ const err = result.stderr || result.stdout || 'unknown error';
1563
+ await sendLongMessage(token, chatState,
1564
+ `❌ Failed to start server:\n\`\`\`\n${err}\n\`\`\``);
1565
+ return;
1566
+ }
1567
+ const url = (result.stdout || '').trim();
1568
+ if (!url) {
1569
+ await sendLongMessage(token, chatState,
1570
+ '❌ Server started but did not return a URL.');
1571
+ return;
1572
+ }
1573
+ await setServerForChat(chatState, url);
1574
+ await sendLongMessage(token, chatState,
1575
+ `✅ Directory created and server started.\nURL: \`${url}\`\nDir: \`${targetDir}\``);
1576
+ log.info(`[chat ${chatId}] Created ${targetDir} → ${url}`);
1577
+ }
1578
+
1579
+ async function cmdMarkdown(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat, replyTo, replyToMsgId) {
1580
+ let entry = null;
1581
+ if (replyToMsgId) {
1582
+ entry = getResponseByMessageId(chatId, chatState.threadId, replyToMsgId);
1583
+ if (!entry) {
1584
+ await sendLongMessage(token, chatState, '❌ Could not find the original response for this message. It may be too old or was not sent by tgagentp.');
1585
+ return;
1586
+ }
1587
+ } else {
1588
+ entry = getRecentResponse(chatId, chatState.threadId, 1);
1589
+ if (!entry) {
1590
+ await sendLongMessage(token, chatState, '❌ No recent responses available. Send a message to the agent first.');
1591
+ return;
1592
+ }
1593
+ }
1594
+ const fileData = Buffer.from(entry.text, 'utf8');
1595
+ const fileName = 'response-' + Date.now() + '.md';
1596
+ await sendTelegramDocument(token, chatState, fileData, fileName, 'Original markdown response');
1597
+ }
1598
+
1311
1599
  const COMMANDS = {
1312
1600
  'help': cmdHelp,
1313
1601
  'servers': cmdServers,
@@ -1327,6 +1615,9 @@ const COMMANDS = {
1327
1615
  'reject': cmdReject,
1328
1616
  'always': cmdAlways,
1329
1617
  'answer': cmdAnswer,
1618
+ 'serve': cmdServe,
1619
+ 'new': cmdNew,
1620
+ 'markdown': cmdMarkdown,
1330
1621
  };
1331
1622
 
1332
1623
  // --------------------------------------------------------------------------
@@ -1335,7 +1626,8 @@ const COMMANDS = {
1335
1626
 
1336
1627
  const serverState = {};
1337
1628
  function getState(url) {
1338
- return serverState[url] = serverState[url] || { busy: false, serverDead: false, pendingResponse: null, activeSessionId: null, activeSessionName: null, activeAgentName: null, taskId: 0, pendingPermission: null, pendingQuestion: null, thinkingEnabled: false, messageQueue: [] };
1629
+ const key = normalizeUrl(url);
1630
+ return serverState[key] = serverState[key] || { busy: false, serverDead: false, pendingResponse: null, activeSessionId: null, activeSessionName: null, activeAgentName: null, taskId: 0, pendingPermission: null, pendingQuestion: null, thinkingEnabled: false, _thinkingBuf: '', messageQueue: [] };
1339
1631
  }
1340
1632
 
1341
1633
  function sessionLabel(st) {
@@ -1497,6 +1789,9 @@ async function main() {
1497
1789
  serverOwners[url] = { chatId: cs.chatId, threadId: cs.threadId };
1498
1790
  getState(url);
1499
1791
 
1792
+ // Send Telegram bridge awareness note to the new session
1793
+ sendGreeting(cs).catch(() => {});
1794
+
1500
1795
  // Auto-activate tmux window for this server
1501
1796
  const servers = listServers();
1502
1797
  const normalized = normalizeUrl(url);
@@ -1533,12 +1828,48 @@ async function main() {
1533
1828
  return null;
1534
1829
  };
1535
1830
 
1536
- const setActiveSessionForChat = (chatState, id) => {
1831
+ const setActiveSessionForChat = (chatState, id, name) => {
1537
1832
  const server = chatState.serverBase;
1833
+ const prevId = getState(server).activeSessionId;
1538
1834
  getState(server).activeSessionId = id;
1539
- log.info(`[chat ${chatState.chatId}] Switched to session: ${id}`);
1835
+ if (name) getState(server).activeSessionName = name;
1836
+ log.info(`[chat ${chatState.chatId}] Switched to session: ${name || id}`);
1837
+ if (id !== prevId) sendGreeting(chatState);
1540
1838
  };
1541
1839
 
1840
+ const TELEGRAM_GREETING_BODY = (sessionName) =>
1841
+ '[Telegram] The following information is for your awareness. Reply with only "Ack" and do not take any action:\n\n' +
1842
+ 'Your responses are being relayed through a Telegram bridge ' +
1843
+ '(current session: ' + sessionName + ').\n' +
1844
+ 'The user sees your messages and can reply from there. ' +
1845
+ 'Their messages appear here prefixed with [Telegram].\n\n' +
1846
+ 'You can use these structured commands in your own response text ' +
1847
+ '(on their own line, with no other content on that line). ' +
1848
+ 'Do NOT use CLI tools to generate them — just include them directly in your response:\n\n' +
1849
+ 'To send a file to the user:\n' +
1850
+ '[Telegram]{"command":"upload","path":"<relative-path>","msg":"<optional caption>"}\n\n' +
1851
+ 'To download a file the user sent:\n' +
1852
+ '[Telegram]{"command":"download","fileId":"<id>","path":"<relative-destination>"}\n\n' +
1853
+ 'For more details about a command:\n' +
1854
+ '[Telegram]{"command":"help","topic":"<upload|download|help>"}';
1855
+
1856
+ async function sendGreeting(chatState) {
1857
+ const st = getState(chatState.serverBase);
1858
+ if (!st.activeSessionId) {
1859
+ try {
1860
+ const discovered = await discoverActiveSession(chatState.serverBase);
1861
+ if (discovered) { st.activeSessionId = discovered.id; st.activeSessionName = discovered.name; }
1862
+ } catch {}
1863
+ }
1864
+ if (!st.activeSessionId) return;
1865
+ const greeting = TELEGRAM_GREETING_BODY(st.activeSessionName || st.activeSessionId);
1866
+ try {
1867
+ await sendToSession(chatState.serverBase, st.activeSessionId, greeting);
1868
+ } catch (err) {
1869
+ log.debug(`sendGreeting error: ${err.message}`);
1870
+ }
1871
+ }
1872
+
1542
1873
  // Start agentp gateway (tiny HTTP server for agentp --tg)
1543
1874
  const serverBaseResolver = () => {
1544
1875
  const entries = Object.entries(serverOwners);
@@ -1563,7 +1894,6 @@ async function main() {
1563
1894
  let answer;
1564
1895
  if (useSession) {
1565
1896
  const notifiedPerms = new Set();
1566
- let thinkingBuf = '';
1567
1897
  let sseConnected;
1568
1898
  const onConnected = Promise.race([
1569
1899
  new Promise((resolve) => { sseConnected = resolve; }),
@@ -1573,14 +1903,16 @@ async function main() {
1573
1903
  onText: null,
1574
1904
  onConnected: sseConnected,
1575
1905
  onThinking: (chunk) => {
1576
- if (!getState(url).thinkingEnabled) { thinkingBuf = ''; return; }
1577
- thinkingBuf += chunk;
1578
- if (thinkingBuf.length >= 50 || chunk.endsWith('\n')) {
1579
- const html = '💭 ' + thinkingBuf.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1906
+ const st2 = getState(url);
1907
+ st2._thinkingBuf += chunk;
1908
+ if (!st2.thinkingEnabled) return;
1909
+ const buf = st2._thinkingBuf;
1910
+ if (buf.length >= 50 || chunk.endsWith('\n')) {
1911
+ st2._thinkingBuf = '';
1912
+ const html = '💭 ' + buf.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1580
1913
  sendLongMessage(token, chatState, html, replyTo, threadId)
1581
1914
  .then(() => {})
1582
1915
  .catch(() => {});
1583
- thinkingBuf = '';
1584
1916
  }
1585
1917
  },
1586
1918
  onPermission: (perm) => {
@@ -1628,10 +1960,10 @@ async function main() {
1628
1960
  }, cancelRef).catch(() => {});
1629
1961
  await onConnected;
1630
1962
  answer = await sendToSession(url, sessionId, `[Telegram] ${text}`, st.activeAgentName, sessionCancelRef);
1631
- if (thinkingBuf) {
1632
- const html = '💭 ' + thinkingBuf.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1963
+ if (st.thinkingEnabled && st._thinkingBuf) {
1964
+ const html = '💭 ' + st._thinkingBuf.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1633
1965
  sendLongMessage(token, chatState, html, replyTo, threadId).catch(() => {});
1634
- thinkingBuf = '';
1966
+ st._thinkingBuf = '';
1635
1967
  }
1636
1968
  // Keep the SSE listener alive so subsequent permission/question events
1637
1969
  // that arrive after sendToSession returns are still forwarded to Telegram.
@@ -1649,9 +1981,13 @@ async function main() {
1649
1981
  if (url === chatServer) {
1650
1982
  if (answer && answer.trim()) {
1651
1983
  const trimmed = answer.trim();
1652
- msgLog('out', chatId, '', trimmed);
1653
- await sendLongMessage(token, chatState, trimmed, replyTo, threadId);
1654
- bufferMessage(chatId, threadId, 'assistant', trimmed);
1984
+ const cleaned = await processTelegramCommands(trimmed, url, token, chatState);
1985
+ msgLog('out', chatId, '', cleaned);
1986
+ if (cleaned) {
1987
+ const msgIds = await sendLongMessage(token, chatState, cleaned, replyTo, threadId);
1988
+ if (msgIds && msgIds.length > 0) recordResponseMessage(chatId, threadId, msgIds, cleaned);
1989
+ bufferMessage(chatId, threadId, 'assistant', cleaned);
1990
+ }
1655
1991
  }
1656
1992
  } else {
1657
1993
  getState(url).pendingResponse = answer;
@@ -1691,6 +2027,7 @@ async function main() {
1691
2027
  try { await sendLongMessage(token, chatState, msg, replyTo, threadId); } catch (_) {}
1692
2028
  }
1693
2029
  } finally {
2030
+ st._thinkingBuf = '';
1694
2031
  st.pendingPermission = null;
1695
2032
  // Only clear the reference if the SSE listener was destroyed or finished.
1696
2033
  // If it is still running, keep the reference so the next message call
@@ -2029,21 +2366,6 @@ async function main() {
2029
2366
  }
2030
2367
  try {
2031
2368
  const notification = await handleFileUpload(token, msg, projectDir);
2032
- // Verify the file is actually on disk before notifying
2033
- const relativePath = notification.match(/`([^`]+)`/)?.[1];
2034
- if (relativePath) {
2035
- const fullPath = path.join(projectDir, relativePath);
2036
- let retries = 5;
2037
- while (!fs.existsSync(fullPath) && retries > 0) {
2038
- await new Promise(r => setTimeout(r, 100));
2039
- retries--;
2040
- }
2041
- if (!fs.existsSync(fullPath)) {
2042
- log.error(`File not found after upload: ${fullPath}`);
2043
- await sendLongMessage(token, cs, `❌ File upload failed: file not saved to disk.`);
2044
- continue;
2045
- }
2046
- }
2047
2369
  const userText = msg.caption || msg.text || '';
2048
2370
  const fullMessage = notification + (userText ? '\n\nUser message: ' + userText : '');
2049
2371
  log.debug(`[chat ${chatId}] ${from}: [file upload]`);
@@ -2144,11 +2466,16 @@ async function main() {
2144
2466
  fileName = 'photo.jpg';
2145
2467
  fileSize = largest.file_size || 0;
2146
2468
  }
2147
- const fileData = await downloadTelegramFile(token, fileId);
2148
- ensureSharedDir(projectDir);
2149
- const relativePath = saveUploadedFile(projectDir, fileName, fileData);
2150
2469
  const sizeStr = fileSize > 0 ? ` (${formatFileSize(fileSize)})` : '';
2151
- await sendLongMessage(token, cs, `📎 File downloaded: \`${relativePath}\`${sizeStr}`);
2470
+ const notification = `📎 File available: \`${fileName}\`${sizeStr}\n` +
2471
+ `Use \`[Telegram]{"command":"download","fileId":"${fileId}","path":"<destination>"}\` to save it.`;
2472
+ await sendLongMessage(token, cs, notification);
2473
+ // Also send the notification to the agent's session so it knows the fileId
2474
+ const st = getState(cs.serverBase);
2475
+ if (st.activeSessionId) {
2476
+ sendToSession(cs.serverBase, st.activeSessionId,
2477
+ '[Telegram] ' + notification).catch(() => {});
2478
+ }
2152
2479
  } catch (err) {
2153
2480
  log.error(`Download on reply error: ${err.message}`);
2154
2481
  await sendLongMessage(token, cs, `❌ File download failed: ${err.message}`);
@@ -2186,7 +2513,12 @@ async function main() {
2186
2513
  try {
2187
2514
  const answer = await executeTuiCommand(paneId, cs.serverBase, tuiCmd);
2188
2515
  if (answer && answer.trim()) {
2189
- await sendLongMessage(token, cs, answer.trim());
2516
+ const cleaned = await processTelegramCommands(answer.trim(), cs.serverBase, token, cs);
2517
+ if (cleaned) {
2518
+ await sendLongMessage(token, cs, cleaned);
2519
+ } else {
2520
+ await sendLongMessage(token, cs, `✅ \`${tuiCmd}\` submitted.`);
2521
+ }
2190
2522
  } else {
2191
2523
  await sendLongMessage(token, cs, `✅ \`${tuiCmd}\` submitted.`);
2192
2524
  }
@@ -2206,22 +2538,23 @@ async function main() {
2206
2538
  const cmdArgs = parts.slice(1);
2207
2539
  const handler = cmds[cmd];
2208
2540
  if (handler) {
2209
- // Disconnected guard: only /help, /servers, /force-switch, /start, /comment, and /shutdown work without a server
2210
- if (!cs.serverBase && cmd !== 'help' && cmd !== 'servers' && cmd !== 'force-switch' && cmd !== 'start' && cmd !== 'comment' && cmd !== 'shutdown') {
2541
+ // Disconnected guard: only /help, /servers, /force-switch, /start, /comment, /shutdown, /serve, and /new work without a server
2542
+ if (!cs.serverBase && cmd !== 'help' && cmd !== 'servers' && cmd !== 'force-switch' && cmd !== 'start' && cmd !== 'comment' && cmd !== 'shutdown' && cmd !== 'serve' && cmd !== 'new') {
2211
2543
  await sendLongMessage(token, cs,
2212
2544
  '🔌 Not connected to any server.\n\n' +
2213
2545
  'Use `/servers` to see available servers and `/servers switch <name>` to connect.');
2214
2546
  continue;
2215
2547
  }
2216
2548
  try {
2217
- await handler(token, chatId, cmdArgs, cs, setServerForChat, setActiveSessionForChat);
2549
+ const replyToMsgId = msg.reply_to_message ? msg.reply_to_message.message_id : 0;
2550
+ await handler(token, chatId, cmdArgs, cs, setServerForChat, setActiveSessionForChat, replyTo, replyToMsgId);
2218
2551
  } catch (err) {
2219
2552
  log.error(`Error handling /${cmd}: ${err.message}`);
2220
2553
  try { await sendLongMessage(token, cs, `Error: ${err.message}`); } catch (_) {}
2221
2554
  }
2222
2555
  } else {
2223
2556
  await sendLongMessage(token, cs,
2224
- `Unknown command: /${cmd}\n\nAvailable commands: /help, /status, /servers, /sessions, /agents, /models, /think, /record, /queue, /flush, /note, /comment, /disconnect`);
2557
+ `Unknown command: /${cmd}\n\nAvailable commands: /help, /status, /servers, /sessions, /agents, /models, /think, /record, /queue, /flush, /note, /comment, /disconnect, /serve, /new, /markdown`);
2225
2558
  }
2226
2559
  continue;
2227
2560
  }
@@ -2290,7 +2623,11 @@ process.on('SIGINT', () => {
2290
2623
  process.exit(0);
2291
2624
  });
2292
2625
 
2293
- main().catch((err) => {
2294
- console.error('Error:', err.message);
2295
- process.exit(1);
2296
- });
2626
+ if (require.main === module) {
2627
+ main().catch((err) => {
2628
+ console.error('Error:', err.message);
2629
+ process.exit(1);
2630
+ });
2631
+ }
2632
+
2633
+ module.exports = { processTelegramCommands, parseTelegramLine, getHelpText, normalizeUrl };
@@ -104,10 +104,13 @@ async function sendLongMessage(token, chatIdOrState, text, replyTo, threadId) {
104
104
  ? chatIdOrState.chatId : chatIdOrState;
105
105
  const effectiveThreadId = threadId || (typeof chatIdOrState === 'object' ? chatIdOrState.threadId : undefined);
106
106
  const chunks = splitMessage(text);
107
+ let msgIds = [];
107
108
  for (let i = 0; i < chunks.length; i++) {
108
109
  const html = convertMarkdownToHtml(chunks[i]);
109
- await sendTelegramMessage(token, chatId, html, 'HTML', i === 0 ? replyTo : undefined, effectiveThreadId);
110
+ const result = await sendTelegramMessage(token, chatId, html, 'HTML', i === 0 ? replyTo : undefined, effectiveThreadId);
111
+ if (result && result.result) msgIds.push(result.result.message_id);
110
112
  }
113
+ return msgIds;
111
114
  }
112
115
 
113
116
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentp",
3
- "version": "0.11.6",
3
+ "version": "0.11.8",
4
4
  "description": "Pipe prompts into a running OpenCode TUI session and stream the final answer",
5
5
  "bin": {
6
6
  "agentp": "bin/agentp",