agentp 0.11.7 → 0.11.9

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
 
@@ -61,6 +61,8 @@ Options:
61
61
  - `--no-tg`: do not forward to Telegram
62
62
  - `--flush`: flush tgagentp's recorded buffer without prepending it to output
63
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`)
64
66
  - `--version`: show version
65
67
  - `--help`: show help message
66
68
 
@@ -150,8 +152,6 @@ Useful to grab recent answers without sending a new prompt.
150
152
 
151
153
  ## ocmux
152
154
 
153
- ## ocmux
154
-
155
155
  Manage OpenCode server + TUI in tmux for project directories.
156
156
 
157
157
  ```bash
@@ -233,24 +233,14 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
233
233
  | Command | Action |
234
234
  |---|---|
235
235
  | `/help [topic]` | Show general help or help for a topic (`servers`, `sessions`, `agents`, `models`, `allow`, `think`, `record`, `queue`) |
236
- | `/servers` | List all running ocmux-served projects with URL + status (✅ idle / ⏳ busy) |
236
+ | `/servers` | List all ocmux-served projects (▶ active, 🔌 disconnected, 💀 dead) |
237
237
  | `/servers switch <name> [--force]` | Switch active server; matches by full path, basename, or substring; `--force` takes over from another chat |
238
- | `/sessions` | List recent sessions for the current server (max 50, with date headings) |
239
- | `/sessions switch <number\|name>` | Switch active session by position or partial name match |
240
- | `/agents` | List primary agents (▶ marker for the active one) |
241
- | `/agents switch <name>` | Switch active agent — persists on session and refreshes TUI |
242
- | `/models` | List connected providers with model counts, context limits, and costs |
243
- | `/status` | Show current server path, URL, busy status, and active session |
244
- | `/cancel` | Cancel the current AI response for the active server |
245
- | `/think [on\|off\|switch]` | Toggle forwarding of model thinking messages to the chat |
246
- | `/record [stop]` | Toggle recording of Telegram conversation for agentp context; `/record stop` clears and stops |
247
- | `/queue <message>` | Queue a message when the server is busy; auto-sent when current task finishes |
248
- | `/flush` | Clear all queued messages (manual and auto-queued) |
249
- | `/resurrect` | Recover a dead server — restart processes in the same directory, reconnect chat |
238
+ | `/resurrect [path]` | Restart a crashed server from its `.ocmux.json`; accepts optional directory path |
250
239
  | `/allow` | Approve a permission request once |
251
240
  | `/reject` | Deny a permission request |
252
241
  | `/always` | Approve and remember for the session |
253
242
  | `/answer <number>` | Respond to a question asked by the AI (structured multiple-choice) |
243
+ | `/markdown` | Send the original markdown of the last response as a `.md` file; reply to a message to get that specific response |
254
244
  | `/shutdown [force\|clear]` | (requires `--dev`) Stop tgagentp; `clear` also wipes saved connections |
255
245
 
256
246
  #### TUI Command Passthrough
@@ -328,6 +318,7 @@ Chat-to-server directory mappings are saved to `/tmp/tgagentp-connections.json`
328
318
  | `TGAGENTP_ALLOWED_CHAT_IDS` | Optional. Comma-separated Telegram chat IDs that are allowed to use the bot. |
329
319
  | `TGAGENTP_PORT` | Optional. Agentp gateway listen port (default: `0` = random). |
330
320
  | `TGAGENTP_DEBOUNCE_MS` | Optional. Debounce interval for queued-message notifications (default: `5000`). |
321
+ | `TGAGENTP_ROOT` | Optional. Root directory for `/serve` and `/new` commands (must be writable). |
331
322
 
332
323
  ## License
333
324
 
package/bin/ocmux CHANGED
@@ -22,6 +22,7 @@ const {
22
22
  logfileFor,
23
23
  URL_RE,
24
24
  resurrectServer,
25
+ startServer,
25
26
  getActiveServer,
26
27
  } = require('../lib/ocmux');
27
28
 
@@ -332,7 +333,7 @@ function doNew(targetDir, longMode, printLogs) {
332
333
  try { fs.truncateSync(lf); } catch { /* may not exist yet */ }
333
334
 
334
335
  // Send the command to the interactive shell (like typing it manually)
335
- const serveFlags = printLogs ? '--port 0 --print-logs' : '--port 0';
336
+ const serveFlags = printLogs ? '--print-logs' : '';
336
337
  tmux(['send-keys', '-t', `${SESSION}:${winIdx}.0`,
337
338
  `opencode serve ${serveFlags} 2>&1 | tee '${lf}'`, 'Enter']);
338
339
 
package/bin/tgagentp CHANGED
@@ -18,7 +18,7 @@ const TGAGENTP_PORT = parseInt(process.env.TGAGENTP_PORT || '0', 10);
18
18
  const PORT_FILE = '/tmp/tgagentp-port';
19
19
  const TGAGENTP_DEBOUNCE_MS = parseInt(process.env.TGAGENTP_DEBOUNCE_MS || '5000', 10);
20
20
  const CONNECTIONS_FILE = '/tmp/tgagentp-connections.json';
21
- const { listServers, activateServer, resurrectServer, statefileFor, readState, tuiPaneId } = require('../lib/ocmux.js');
21
+ const { listServers, activateServer, resurrectServer, statefileFor, readState, tuiPaneId, serverStatus, listDeadServers } = require('../lib/ocmux.js');
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');
@@ -162,6 +162,33 @@ function flushRecorded(chatId, threadId) {
162
162
  return msgs;
163
163
  }
164
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
+
165
192
  // ---------------------------------------------------------------------------
166
193
  // Telegram Bot API helpers
167
194
  // ---------------------------------------------------------------------------
@@ -179,7 +206,7 @@ function parseAllowlist(raw) {
179
206
  // Returns empty string if the message isn't a reply or has no text.
180
207
  // Uses a dynamic code-fence length (max consecutive backticks + 1)
181
208
  // so nested backticks never break the structure.
182
- const { ensureSharedDir, saveUploadedFile, formatFileSize } = require('../lib/file-share.js');
209
+ const { formatFileSize } = require('../lib/file-share.js');
183
210
 
184
211
  function quoteReply(msg) {
185
212
  const rm = msg.reply_to_message;
@@ -202,7 +229,7 @@ function quoteReply(msg) {
202
229
  // ---------------------------------------------------------------------------
203
230
 
204
231
  // Handle an uploaded file from Telegram.
205
- // 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.
206
233
  async function handleFileUpload(token, msg, projectDir) {
207
234
  const doc = msg.document;
208
235
  const photo = msg.photo;
@@ -217,18 +244,15 @@ async function handleFileUpload(token, msg, projectDir) {
217
244
  fileName = doc.file_name || 'upload.bin';
218
245
  fileSize = doc.file_size || 0;
219
246
  } else if (photo) {
220
- // Use the largest photo
221
247
  const largest = photo[photo.length - 1];
222
248
  fileId = largest.file_id;
223
249
  fileName = 'photo.jpg';
224
250
  fileSize = largest.file_size || 0;
225
251
  }
226
252
 
227
- const fileData = await downloadTelegramFile(token, fileId);
228
- ensureSharedDir(projectDir);
229
- const relativePath = saveUploadedFile(projectDir, fileName, fileData);
230
253
  const sizeStr = fileSize > 0 ? ` (${formatFileSize(fileSize)})` : '';
231
- return `📎 File uploaded: \`${relativePath}\`${sizeStr}`;
254
+ return `📎 File uploaded: \`${fileName}\`${sizeStr}\n` +
255
+ `Use \`[Telegram]{"command":"download","fileId":"${fileId}","path":"<destination>"}\` to save it.`;
232
256
  }
233
257
 
234
258
  // ---------------------------------------------------------------------------
@@ -405,8 +429,15 @@ async function cmdHelp(token, chatId, args, chatState) {
405
429
  hint);
406
430
  return;
407
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
+ }
408
439
  await sendLongMessage(token, chatState,
409
- '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`, 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`.');
410
441
  return;
411
442
  }
412
443
  await sendLongMessage(token, chatState,
@@ -433,6 +464,7 @@ async function cmdHelp(token, chatId, args, chatState) {
433
464
  '`/comment <text>` ─ save a comment in the chat (not forwarded to the agent)\n' +
434
465
  '`/disconnect` ─ disconnect this chat from its server\n' +
435
466
  '`/allow` / `/reject` / `/always` ─ respond to permission requests\n' +
467
+ '`/markdown` ─ send the original markdown of the last response as a `.md` file\n' +
436
468
  '`/shutdown` ─ stop tgagentp (only with `--dev` flag)\n' +
437
469
  (tgagentpRootValid
438
470
  ? '`/serve <path>` ─ start a server in an existing directory\n' +
@@ -527,14 +559,21 @@ async function cmdServers(token, chatId, args, chatState, setServerForChat, setA
527
559
  return;
528
560
  }
529
561
 
530
- // Default: list servers
562
+ // Default: list servers (alive + dead)
531
563
  const servers = listServers();
532
- if (servers.length === 0) {
533
- await sendLongMessage(token, chatState, 'No servers running.');
564
+ // Also find dead servers from saved connections
565
+ let savedData;
566
+ try { savedData = JSON.parse(fs.readFileSync(CONNECTIONS_FILE, 'utf8')); } catch {}
567
+ const knownDirs = savedData && Array.isArray(savedData.connections)
568
+ ? savedData.connections.map(c => c.dir).filter(Boolean) : [];
569
+ const dead = listDeadServers(knownDirs);
570
+ const all = [...servers, ...dead.map(d => ({ url: d.url, dir: d.dir, index: d.windowIndex, status: 'dead' }))];
571
+ if (all.length === 0) {
572
+ await sendLongMessage(token, chatState, 'No servers found.');
534
573
  return;
535
574
  }
536
575
  const activeUrl = chatState.serverBase ? normalizeUrl(chatState.serverBase) : null;
537
- const lines = servers.map(s => {
576
+ const lines = all.map(s => {
538
577
  const normalizedUrl = normalizeUrl(s.url);
539
578
  const isActive = activeUrl && normalizedUrl === activeUrl;
540
579
  const isOwned = Object.keys(serverOwners).some(url => normalizeUrl(url) === normalizedUrl);
@@ -543,12 +582,14 @@ async function cmdServers(token, chatId, args, chatState, setServerForChat, setA
543
582
  bullet = '▶ ';
544
583
  } else if (isOwned) {
545
584
  bullet = ' · ';
585
+ } else if (s.status === 'dead') {
586
+ bullet = '💀 ';
546
587
  } else {
547
588
  bullet = '🔌 ';
548
589
  }
549
590
  return bullet + `${s.url} → ${s.dir} (${s.status})`;
550
591
  });
551
- const prefix = chatState.serverBase ? 'Active servers:' : 'Available servers:';
592
+ const prefix = chatState.serverBase ? 'All servers:' : 'Available servers:';
552
593
  await sendLongMessage(token, chatState, prefix + '\n' + lines.join('\n'));
553
594
  }
554
595
 
@@ -557,6 +598,7 @@ function sessionName(s) {
557
598
  }
558
599
 
559
600
  function normalizeUrl(url) {
601
+ if (!url) return url;
560
602
  return url.replace(/\/+$/, '').replace(/^http:\/\/127\.0\.0\.1:/, 'http://localhost:');
561
603
  }
562
604
 
@@ -600,7 +642,7 @@ async function cmdSessions(token, chatId, args, chatState, setServerForChat, set
600
642
  try {
601
643
  const session = await createSession(server, title);
602
644
  const name = session.title || session.slug || session.id;
603
- setActiveSessionForChat(chatState, session.id);
645
+ setActiveSessionForChat(chatState, session.id, name);
604
646
  selectSession(server, session.id);
605
647
  await sendLongMessage(token, chatState,
606
648
  `Created session: \`${name}\`\nIt is now the active session.`);
@@ -652,7 +694,7 @@ async function cmdSessions(token, chatId, args, chatState, setServerForChat, set
652
694
  const num = parseInt(query, 10);
653
695
  if (/^\d+$/.test(query) && num >= 1 && num <= sorted.length) {
654
696
  const target = sorted[num - 1];
655
- setActiveSessionForChat(chatId, target.id);
697
+ setActiveSessionForChat(chatState, target.id, sessionName(target));
656
698
  selectSession(server, target.id);
657
699
  await sendLongMessage(token, chatState,
658
700
  `Switched to session #${num}: \`${sessionName(target)}\`\nSubsequent messages will be sent to this session.`);
@@ -674,7 +716,7 @@ async function cmdSessions(token, chatId, args, chatState, setServerForChat, set
674
716
  return;
675
717
  }
676
718
  const target = matches[0];
677
- setActiveSessionForChat(chatId, target.id);
719
+ setActiveSessionForChat(chatState, target.id);
678
720
  selectSession(server, target.id);
679
721
  await sendLongMessage(token, chatState,
680
722
  `Switched to session: \`${sessionName(target)}\`\nSubsequent messages will be sent to this session.`);
@@ -704,10 +746,13 @@ async function cmdSessions(token, chatId, args, chatState, setServerForChat, set
704
746
  return;
705
747
  }
706
748
 
707
- // Show user sessions (have agent or directory) and always include the active session
749
+ // Show primary-agent sessions and always include the active session
750
+ const agents = await listAgents(server).catch(() => []);
751
+ const primaryAgents = new Set(agents.filter(a => a.mode === 'primary').map(a => a.name));
752
+ const hasPrimaryAgent = (s) => s.agent && primaryAgents.has(s.agent);
708
753
  const activeId = getState(server).activeSessionId;
709
754
  const filtered = sessions.filter(s =>
710
- (s.agent || s.directory || s.id === activeId) && (!projectDir || s.directory === projectDir));
755
+ (hasPrimaryAgent(s) || s.id === activeId) && (!projectDir || s.directory === projectDir));
711
756
  const sorted = sortedSessions(filtered, 50);
712
757
  if (sorted.length === 0) {
713
758
  await sendLongMessage(token, chatState, 'No active sessions.');
@@ -891,8 +936,6 @@ function startAgentpGateway(token, serverBaseResolver) {
891
936
  }
892
937
  if (req.url === '/send') {
893
938
  handleSendRequest(req, res, token, serverBaseResolver);
894
- } else if (req.url === '/send-file') {
895
- handleSendFileRequest(req, res, token, serverBaseResolver);
896
939
  } else {
897
940
  res.writeHead(404);
898
941
  res.end('Not Found');
@@ -944,7 +987,9 @@ function handleSendRequest(req, res, token, serverBaseResolver) {
944
987
  const activeUrl = normalizeUrl(serverBaseResolver());
945
988
 
946
989
  if (targetUrl === activeUrl) {
947
- sendLongMessage(token, owningChatId, text.trim()).catch(() => {});
990
+ processTelegramCommands(text, server, token, owningChatId).then((cleaned) => {
991
+ if (cleaned) sendLongMessage(token, owningChatId, cleaned).catch(() => {});
992
+ });
948
993
  } else {
949
994
  if (!agentpQueues[server]) {
950
995
  agentpQueues[server] = { messages: [], notifyTimer: null, notified: false };
@@ -969,72 +1014,122 @@ function handleSendRequest(req, res, token, serverBaseResolver) {
969
1014
  });
970
1015
  }
971
1016
 
972
- function handleSendFileRequest(req, res, token, serverBaseResolver) {
973
- let body = '';
974
- req.on('data', (chunk) => { body += chunk; });
975
- req.on('end', () => {
976
- try {
977
- let data;
978
- try { data = JSON.parse(body); } catch {
979
- res.writeHead(400);
980
- res.end('Bad Request');
981
- return;
982
- }
983
- const { filePath, server, message } = data;
984
- if (typeof filePath !== 'string' || typeof server !== 'string') {
985
- res.writeHead(400);
986
- res.end('Bad Request: filePath and server are required');
987
- return;
988
- }
1017
+ // Scan response text for [Telegram]{...} structured messages, process them,
1018
+ // and return the text with those lines removed (or replaced, for help commands).
1019
+ // Supported commands:
1020
+ // "upload" — send a file to the Telegram chat.
1021
+ // Format: [Telegram]{"command":"upload","path":"<rel-path>","msg":"<caption>"}
1022
+ // "download" — save a Telegram file to the project directory.
1023
+ // Format: [Telegram]{"command":"download","fileId":"<id>","path":"<rel-path>"}
1024
+ // "help" — get help about available commands.
1025
+ // Format: [Telegram]{"command":"help","topic":"<upload|download|help>"}
1026
+ // The [Telegram]{...} must be the only content on its line.
1027
+
1028
+ // Parse a single [Telegram]{...} line. Returns { payload } or null.
1029
+ function parseTelegramLine(line) {
1030
+ const m = line.match(/^\[Telegram\](\{.*\})\s*$/);
1031
+ if (!m) return null;
1032
+ let payload;
1033
+ try { payload = JSON.parse(m[1]); } catch { return null; }
1034
+ if (!payload || typeof payload !== 'object') return null;
1035
+ return payload;
1036
+ }
989
1037
 
990
- // Find the chat that owns this server
991
- const targetUrl = normalizeUrl(server);
992
- const owningChatId = Object.entries(serverOwners).find(([url]) => normalizeUrl(url) === targetUrl)?.[1];
1038
+ const TELEGRAM_COMMANDS_HELP = {
1039
+ all: 'Available commands:\n\n' +
1040
+ '• upload — send a file to Telegram.\n' +
1041
+ ' [Telegram]{"command":"upload","path":"<relative-path>","msg":"<optional caption>"}\n\n' +
1042
+ '• download — save a file from Telegram to the project.\n' +
1043
+ ' [Telegram]{"command":"download","fileId":"<telegram-file-id>","path":"<relative-destination>"}\n\n' +
1044
+ '• help — show this help or details for a specific command.\n' +
1045
+ ' [Telegram]{"command":"help","topic":"<command>"}',
1046
+ upload: 'Upload a file to the Telegram chat.\n\n' +
1047
+ 'Format: [Telegram]{"command":"upload","path":"<relative-path>","msg":"<optional caption>"}\n\n' +
1048
+ '- path: file path relative to the project root (required)\n' +
1049
+ '- msg: optional caption sent with the file',
1050
+ download: 'Download a file the user uploaded to Telegram.\n\n' +
1051
+ 'Format: [Telegram]{"command":"download","fileId":"<telegram-file-id>","path":"<relative-destination>"}\n\n' +
1052
+ '- fileId: the Telegram file identifier (provided when the user uploads a file)\n' +
1053
+ '- path: destination path relative to the project root (required)',
1054
+ help: 'Get help about Telegram commands.\n\n' +
1055
+ 'Format: [Telegram]{"command":"help","topic":"<command>"}\n\n' +
1056
+ '- topic: one of "upload", "download", "help". Omit for general help.',
1057
+ };
993
1058
 
994
- if (!owningChatId) {
995
- res.writeHead(200, { 'Content-Type': 'application/json' });
996
- res.end(JSON.stringify({ sent: false, reason: 'no owner' }));
997
- return;
998
- }
1059
+ function getHelpText(topic) {
1060
+ if (topic && TELEGRAM_COMMANDS_HELP[topic]) return TELEGRAM_COMMANDS_HELP[topic];
1061
+ return TELEGRAM_COMMANDS_HELP.all;
1062
+ }
999
1063
 
1000
- // Resolve the file path relative to the server directory
1001
- const dir = serverDir(server);
1002
- if (!dir) {
1003
- res.writeHead(200, { 'Content-Type': 'application/json' });
1004
- res.end(JSON.stringify({ sent: false, reason: 'no server dir' }));
1005
- return;
1064
+ async function processTelegramCommands(text, server, token, chatState) {
1065
+ if (!text) return text;
1066
+ const lines = text.split('\n');
1067
+ const toStrip = new Set();
1068
+ const filesSent = [];
1069
+ const dir = serverDir(server);
1070
+
1071
+ for (let i = 0; i < lines.length; i++) {
1072
+ const payload = parseTelegramLine(lines[i]);
1073
+ if (!payload) continue;
1074
+
1075
+ if (payload.command === 'upload' && payload.path) {
1076
+ toStrip.add(i);
1077
+ const resolved = dir ? path.resolve(dir, payload.path) : path.resolve(payload.path);
1078
+ if (dir && !resolved.startsWith(dir + path.sep)) {
1079
+ log.error(`processTelegramCommands: path outside project rejected: ${payload.path}`);
1080
+ continue;
1006
1081
  }
1007
- const fullPath = path.resolve(dir, filePath);
1008
- if (!fs.existsSync(fullPath)) {
1009
- res.writeHead(200, { 'Content-Type': 'application/json' });
1010
- res.end(JSON.stringify({ sent: false, reason: 'file not found' }));
1011
- return;
1082
+ try {
1083
+ if (!fs.existsSync(resolved)) {
1084
+ log.error(`processTelegramCommands: file not found: ${resolved}`);
1085
+ continue;
1086
+ }
1087
+ const fileData = fs.readFileSync(resolved);
1088
+ const fileName = path.basename(resolved);
1089
+ const caption = typeof payload.msg === 'string' ? payload.msg : undefined;
1090
+ await sendTelegramDocument(token, chatState, fileData, fileName, caption);
1091
+ filesSent.push(fileName);
1092
+ } catch (err) {
1093
+ log.error(`processTelegramCommands: failed to send file ${resolved}: ${err.message}`);
1094
+ }
1095
+ } else if (payload.command === 'download' && payload.fileId && payload.path) {
1096
+ toStrip.add(i);
1097
+ const resolved = dir ? path.resolve(dir, payload.path) : path.resolve(payload.path);
1098
+ if (dir && !resolved.startsWith(dir + path.sep)) {
1099
+ log.error(`processTelegramCommands: download path outside project rejected: ${payload.path}`);
1100
+ continue;
1012
1101
  }
1102
+ try {
1103
+ const fileData = await downloadTelegramFile(token, payload.fileId);
1104
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
1105
+ fs.writeFileSync(resolved, fileData);
1106
+ filesSent.push(path.basename(resolved));
1107
+ } catch (err) {
1108
+ log.error(`processTelegramCommands: failed to download file: ${err.message}`);
1109
+ }
1110
+ } else if (payload.command === 'help') {
1111
+ toStrip.add(i);
1112
+ const topic = typeof payload.topic === 'string' ? payload.topic : undefined;
1113
+ const st = getState(server);
1114
+ if (st.activeSessionId) {
1115
+ sendToSession(server, st.activeSessionId, getHelpText(topic)).catch(() => {});
1116
+ }
1117
+ } else {
1118
+ toStrip.add(i); // unrecognized command — strip silently
1119
+ }
1120
+ }
1013
1121
 
1014
- const fileData = fs.readFileSync(fullPath);
1015
- const fileName = path.basename(fullPath);
1122
+ if (toStrip.size === 0) return text;
1016
1123
 
1017
- // Send file to Telegram
1018
- sendTelegramDocument(token, owningChatId, fileData, fileName, message).then(() => {
1019
- // Clean up: only delete files from telegram-shared/downloads/
1020
- // to prevent accidental deletion of project files
1021
- const downloadsDir = path.join(dir, 'telegram-shared', 'downloads');
1022
- if (fullPath.startsWith(downloadsDir + path.sep)) {
1023
- try { fs.unlinkSync(fullPath); } catch (_) {}
1024
- }
1025
- res.writeHead(200, { 'Content-Type': 'application/json' });
1026
- res.end(JSON.stringify({ sent: true }));
1027
- }).catch((err) => {
1028
- log.error(`Failed to send file: ${err.message}`);
1029
- res.writeHead(500, { 'Content-Type': 'application/json' });
1030
- res.end(JSON.stringify({ sent: false, reason: err.message }));
1031
- });
1032
- } catch (err) {
1033
- log.error(`Agentp gateway handler error: ${err.message}`);
1034
- res.writeHead(500);
1035
- res.end('Internal Server Error');
1036
- }
1037
- });
1124
+ if (filesSent.length > 0) {
1125
+ log.info(`processTelegramCommands: processed ${filesSent.length} file(s): ${filesSent.join(', ')}`);
1126
+ }
1127
+
1128
+ const out = [];
1129
+ for (let i = 0; i < lines.length; i++) {
1130
+ if (!toStrip.has(i)) out.push(lines[i]);
1131
+ }
1132
+ return out.join('\n').replace(/\n{3,}/g, '\n\n').trim();
1038
1133
  }
1039
1134
 
1040
1135
  async function cmdAllow(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
@@ -1277,31 +1372,53 @@ async function cmdComment(token, chatId, args, chatState) {
1277
1372
  }
1278
1373
 
1279
1374
  async function cmdResurrect(token, chatId, args, chatState) {
1280
- const url = chatState.serverBase;
1281
- if (!url) {
1282
- await sendLongMessage(token, chatState, '🔌 Not connected to any server. Nothing to resurrect.');
1283
- return;
1284
- }
1285
-
1286
- let dir = serverDir(url);
1287
- if (!dir) {
1288
- // Fallback: look up directory from saved connections
1289
- try {
1290
- const savedData = JSON.parse(fs.readFileSync(CONNECTIONS_FILE, 'utf8'));
1291
- if (savedData && Array.isArray(savedData.connections)) {
1292
- const match = savedData.connections.find(c =>
1293
- String(c.chatId) === String(chatId) && (c.threadId || null) === (chatState.threadId || null)
1294
- );
1295
- if (match && match.dir) dir = match.dir;
1375
+ let dir = null;
1376
+ let url = null;
1377
+ const dirArg = args && args.length > 0 ? args.join(' ') : null;
1378
+ if (dirArg) {
1379
+ // User specified a directory: resolve it
1380
+ if (TGAGENTP_ROOT) {
1381
+ const resolved = path.resolve(TGAGENTP_ROOT, dirArg);
1382
+ if (resolved.startsWith(TGAGENTP_ROOT) && fs.existsSync(resolved)) {
1383
+ dir = resolved;
1296
1384
  }
1297
- } catch {}
1298
- }
1299
- if (!dir) {
1300
- await sendLongMessage(token, chatState,
1301
- 'Could not find the directory for this server.\n' +
1302
- 'The tmux window may have been removed.\n' +
1303
- 'Use `ocmux resurrect` from the terminal to recover it.');
1304
- return;
1385
+ }
1386
+ if (!dir) {
1387
+ // Resolve against home or absolute path
1388
+ const resolved = path.resolve(dirArg);
1389
+ if (fs.existsSync(resolved)) dir = resolved;
1390
+ }
1391
+ if (!dir) {
1392
+ await sendLongMessage(token, chatState, `❌ Directory not found: \`${dirArg}\``);
1393
+ return;
1394
+ }
1395
+ // Read the old URL from the state file
1396
+ const state = readState(statefileFor(dir));
1397
+ url = state ? state.url : null;
1398
+ } else {
1399
+ url = chatState.serverBase;
1400
+ if (!url) {
1401
+ await sendLongMessage(token, chatState, '🔌 Not connected to any server. Specify a directory or switch to a server first.');
1402
+ return;
1403
+ }
1404
+ dir = serverDir(url);
1405
+ if (!dir) {
1406
+ try {
1407
+ const savedData = JSON.parse(fs.readFileSync(CONNECTIONS_FILE, 'utf8'));
1408
+ if (savedData && Array.isArray(savedData.connections)) {
1409
+ const match = savedData.connections.find(c =>
1410
+ String(c.chatId) === String(chatId) && (c.threadId || null) === (chatState.threadId || null)
1411
+ );
1412
+ if (match && match.dir) dir = match.dir;
1413
+ }
1414
+ } catch {}
1415
+ }
1416
+ if (!dir) {
1417
+ await sendLongMessage(token, chatState,
1418
+ 'Could not find the directory for this server.\n' +
1419
+ 'Use `ocmux resurrect` from the terminal or specify a directory: `/resurrect <path>`.');
1420
+ return;
1421
+ }
1305
1422
  }
1306
1423
 
1307
1424
  await sendLongMessage(token, chatState,
@@ -1400,9 +1517,16 @@ async function cmdServe(token, chatId, args, chatState, setServerForChat, setAct
1400
1517
  }
1401
1518
  const sf = statefileFor(targetDir);
1402
1519
  if (fs.existsSync(sf)) {
1403
- await sendLongMessage(token, chatState,
1404
- `⚠️ Server already exists for \`${relPath}\`.\n` +
1405
- 'Use `/servers switch <name>` to connect to it.');
1520
+ const status = serverStatus(targetDir);
1521
+ if (status && status.status === 'dead') {
1522
+ await sendLongMessage(token, chatState,
1523
+ `💀 A dead server exists at \`${relPath}\` (${status.url}).\n` +
1524
+ `Use \`/resurrect ${relPath}\` to restart it.`);
1525
+ } else {
1526
+ await sendLongMessage(token, chatState,
1527
+ `⚠️ Server already exists for \`${relPath}\`.\n` +
1528
+ 'Use `/servers switch <name>` to connect to it.');
1529
+ }
1406
1530
  return;
1407
1531
  }
1408
1532
  await sendLongMessage(token, chatState,
@@ -1490,6 +1614,26 @@ async function cmdNew(token, chatId, args, chatState, setServerForChat, setActiv
1490
1614
  log.info(`[chat ${chatId}] Created ${targetDir} → ${url}`);
1491
1615
  }
1492
1616
 
1617
+ async function cmdMarkdown(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat, replyTo, replyToMsgId) {
1618
+ let entry = null;
1619
+ if (replyToMsgId) {
1620
+ entry = getResponseByMessageId(chatId, chatState.threadId, replyToMsgId);
1621
+ if (!entry) {
1622
+ await sendLongMessage(token, chatState, '❌ Could not find the original response for this message. It may be too old or was not sent by tgagentp.');
1623
+ return;
1624
+ }
1625
+ } else {
1626
+ entry = getRecentResponse(chatId, chatState.threadId, 1);
1627
+ if (!entry) {
1628
+ await sendLongMessage(token, chatState, '❌ No recent responses available. Send a message to the agent first.');
1629
+ return;
1630
+ }
1631
+ }
1632
+ const fileData = Buffer.from(entry.text, 'utf8');
1633
+ const fileName = 'response-' + Date.now() + '.md';
1634
+ await sendTelegramDocument(token, chatState, fileData, fileName, 'Original markdown response');
1635
+ }
1636
+
1493
1637
  const COMMANDS = {
1494
1638
  'help': cmdHelp,
1495
1639
  'servers': cmdServers,
@@ -1511,6 +1655,7 @@ const COMMANDS = {
1511
1655
  'answer': cmdAnswer,
1512
1656
  'serve': cmdServe,
1513
1657
  'new': cmdNew,
1658
+ 'markdown': cmdMarkdown,
1514
1659
  };
1515
1660
 
1516
1661
  // --------------------------------------------------------------------------
@@ -1519,7 +1664,8 @@ const COMMANDS = {
1519
1664
 
1520
1665
  const serverState = {};
1521
1666
  function getState(url) {
1522
- 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, _thinkingBuf: '', messageQueue: [] };
1667
+ const key = normalizeUrl(url);
1668
+ 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: [] };
1523
1669
  }
1524
1670
 
1525
1671
  function sessionLabel(st) {
@@ -1681,6 +1827,9 @@ async function main() {
1681
1827
  serverOwners[url] = { chatId: cs.chatId, threadId: cs.threadId };
1682
1828
  getState(url);
1683
1829
 
1830
+ // Send Telegram bridge awareness note to the new session
1831
+ sendGreeting(cs).catch(() => {});
1832
+
1684
1833
  // Auto-activate tmux window for this server
1685
1834
  const servers = listServers();
1686
1835
  const normalized = normalizeUrl(url);
@@ -1717,12 +1866,48 @@ async function main() {
1717
1866
  return null;
1718
1867
  };
1719
1868
 
1720
- const setActiveSessionForChat = (chatState, id) => {
1869
+ const setActiveSessionForChat = (chatState, id, name) => {
1721
1870
  const server = chatState.serverBase;
1871
+ const prevId = getState(server).activeSessionId;
1722
1872
  getState(server).activeSessionId = id;
1723
- log.info(`[chat ${chatState.chatId}] Switched to session: ${id}`);
1873
+ if (name) getState(server).activeSessionName = name;
1874
+ log.info(`[chat ${chatState.chatId}] Switched to session: ${name || id}`);
1875
+ if (id !== prevId) sendGreeting(chatState);
1724
1876
  };
1725
1877
 
1878
+ const TELEGRAM_GREETING_BODY = (sessionName) =>
1879
+ '[Telegram] The following information is for your awareness. Reply with only "Ack" and do not take any action:\n\n' +
1880
+ 'Your responses are being relayed through a Telegram bridge ' +
1881
+ '(current session: ' + sessionName + ').\n' +
1882
+ 'The user sees your messages and can reply from there. ' +
1883
+ 'Their messages appear here prefixed with [Telegram].\n\n' +
1884
+ 'You can use these structured commands in your own response text ' +
1885
+ '(on their own line, with no other content on that line). ' +
1886
+ 'Do NOT use CLI tools to generate them — just include them directly in your response:\n\n' +
1887
+ 'To send a file to the user:\n' +
1888
+ '[Telegram]{"command":"upload","path":"<relative-path>","msg":"<optional caption>"}\n\n' +
1889
+ 'To download a file the user sent:\n' +
1890
+ '[Telegram]{"command":"download","fileId":"<id>","path":"<relative-destination>"}\n\n' +
1891
+ 'For more details about a command:\n' +
1892
+ '[Telegram]{"command":"help","topic":"<upload|download|help>"}';
1893
+
1894
+ async function sendGreeting(chatState) {
1895
+ const st = getState(chatState.serverBase);
1896
+ if (!st.activeSessionId) {
1897
+ try {
1898
+ const discovered = await discoverActiveSession(chatState.serverBase);
1899
+ if (discovered) { st.activeSessionId = discovered.id; st.activeSessionName = discovered.name; }
1900
+ } catch {}
1901
+ }
1902
+ if (!st.activeSessionId) return;
1903
+ const greeting = TELEGRAM_GREETING_BODY(st.activeSessionName || st.activeSessionId);
1904
+ try {
1905
+ await sendToSession(chatState.serverBase, st.activeSessionId, greeting);
1906
+ } catch (err) {
1907
+ log.debug(`sendGreeting error: ${err.message}`);
1908
+ }
1909
+ }
1910
+
1726
1911
  // Start agentp gateway (tiny HTTP server for agentp --tg)
1727
1912
  const serverBaseResolver = () => {
1728
1913
  const entries = Object.entries(serverOwners);
@@ -1813,7 +1998,7 @@ async function main() {
1813
1998
  }, cancelRef).catch(() => {});
1814
1999
  await onConnected;
1815
2000
  answer = await sendToSession(url, sessionId, `[Telegram] ${text}`, st.activeAgentName, sessionCancelRef);
1816
- if (st._thinkingBuf) {
2001
+ if (st.thinkingEnabled && st._thinkingBuf) {
1817
2002
  const html = '💭 ' + st._thinkingBuf.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1818
2003
  sendLongMessage(token, chatState, html, replyTo, threadId).catch(() => {});
1819
2004
  st._thinkingBuf = '';
@@ -1834,9 +2019,13 @@ async function main() {
1834
2019
  if (url === chatServer) {
1835
2020
  if (answer && answer.trim()) {
1836
2021
  const trimmed = answer.trim();
1837
- msgLog('out', chatId, '', trimmed);
1838
- await sendLongMessage(token, chatState, trimmed, replyTo, threadId);
1839
- bufferMessage(chatId, threadId, 'assistant', trimmed);
2022
+ const cleaned = await processTelegramCommands(trimmed, url, token, chatState);
2023
+ msgLog('out', chatId, '', cleaned);
2024
+ if (cleaned) {
2025
+ const msgIds = await sendLongMessage(token, chatState, cleaned, replyTo, threadId);
2026
+ if (msgIds && msgIds.length > 0) recordResponseMessage(chatId, threadId, msgIds, cleaned);
2027
+ bufferMessage(chatId, threadId, 'assistant', cleaned);
2028
+ }
1840
2029
  }
1841
2030
  } else {
1842
2031
  getState(url).pendingResponse = answer;
@@ -1869,7 +2058,7 @@ async function main() {
1869
2058
  const msg = isConnError
1870
2059
  ? `⚠️ Server \`${url}\` is unreachable. The message has been requeued and will be sent when the server is available again.\n\n` +
1871
2060
  `**Options:**\n` +
1872
- `• Start the server: \`ocmux serve\`\n` +
2061
+ `• Restart it: \`/resurrect\`\n` +
1873
2062
  `• Switch to another server: \`/servers switch <name>\`\n` +
1874
2063
  `• Flush the queue: \`/flush\``
1875
2064
  : `Error: ${err.message}`;
@@ -2100,7 +2289,7 @@ async function main() {
2100
2289
  await sendLongMessage(token, cs,
2101
2290
  '🔄 Bot started. Previously connected to `' + dir + '`.\n' +
2102
2291
  '⚠️ The server at `' + state.url + '` is not reachable.\n' +
2103
- 'Options: start the server with `ocmux serve`, or switch with `/servers switch <name>`.');
2292
+ 'Use `/resurrect` to restart it, or `/servers switch <name>` to connect elsewhere.');
2104
2293
  } catch (_) {}
2105
2294
  }
2106
2295
  }
@@ -2215,21 +2404,6 @@ async function main() {
2215
2404
  }
2216
2405
  try {
2217
2406
  const notification = await handleFileUpload(token, msg, projectDir);
2218
- // Verify the file is actually on disk before notifying
2219
- const relativePath = notification.match(/`([^`]+)`/)?.[1];
2220
- if (relativePath) {
2221
- const fullPath = path.join(projectDir, relativePath);
2222
- let retries = 5;
2223
- while (!fs.existsSync(fullPath) && retries > 0) {
2224
- await new Promise(r => setTimeout(r, 100));
2225
- retries--;
2226
- }
2227
- if (!fs.existsSync(fullPath)) {
2228
- log.error(`File not found after upload: ${fullPath}`);
2229
- await sendLongMessage(token, cs, `❌ File upload failed: file not saved to disk.`);
2230
- continue;
2231
- }
2232
- }
2233
2407
  const userText = msg.caption || msg.text || '';
2234
2408
  const fullMessage = notification + (userText ? '\n\nUser message: ' + userText : '');
2235
2409
  log.debug(`[chat ${chatId}] ${from}: [file upload]`);
@@ -2251,7 +2425,7 @@ async function main() {
2251
2425
  `⚠️ Server \`${cs.serverBase}\` is not reachable.\n\n` +
2252
2426
  `Your file upload has been queued (#${qlen}). It will be sent when the server becomes available.\n\n` +
2253
2427
  `**Options:**\n` +
2254
- `• Start the server: \`ocmux serve\`\n` +
2428
+ `• Restart it: \`/resurrect\`\n` +
2255
2429
  `• Switch to another server: \`/servers switch <name>\`\n` +
2256
2430
  `• Flush the queue: \`/flush\``);
2257
2431
  continue;
@@ -2330,11 +2504,16 @@ async function main() {
2330
2504
  fileName = 'photo.jpg';
2331
2505
  fileSize = largest.file_size || 0;
2332
2506
  }
2333
- const fileData = await downloadTelegramFile(token, fileId);
2334
- ensureSharedDir(projectDir);
2335
- const relativePath = saveUploadedFile(projectDir, fileName, fileData);
2336
2507
  const sizeStr = fileSize > 0 ? ` (${formatFileSize(fileSize)})` : '';
2337
- await sendLongMessage(token, cs, `📎 File downloaded: \`${relativePath}\`${sizeStr}`);
2508
+ const notification = `📎 File available: \`${fileName}\`${sizeStr}\n` +
2509
+ `Use \`[Telegram]{"command":"download","fileId":"${fileId}","path":"<destination>"}\` to save it.`;
2510
+ await sendLongMessage(token, cs, notification);
2511
+ // Also send the notification to the agent's session so it knows the fileId
2512
+ const st = getState(cs.serverBase);
2513
+ if (st.activeSessionId) {
2514
+ sendToSession(cs.serverBase, st.activeSessionId,
2515
+ '[Telegram] ' + notification).catch(() => {});
2516
+ }
2338
2517
  } catch (err) {
2339
2518
  log.error(`Download on reply error: ${err.message}`);
2340
2519
  await sendLongMessage(token, cs, `❌ File download failed: ${err.message}`);
@@ -2372,7 +2551,12 @@ async function main() {
2372
2551
  try {
2373
2552
  const answer = await executeTuiCommand(paneId, cs.serverBase, tuiCmd);
2374
2553
  if (answer && answer.trim()) {
2375
- await sendLongMessage(token, cs, answer.trim());
2554
+ const cleaned = await processTelegramCommands(answer.trim(), cs.serverBase, token, cs);
2555
+ if (cleaned) {
2556
+ await sendLongMessage(token, cs, cleaned);
2557
+ } else {
2558
+ await sendLongMessage(token, cs, `✅ \`${tuiCmd}\` submitted.`);
2559
+ }
2376
2560
  } else {
2377
2561
  await sendLongMessage(token, cs, `✅ \`${tuiCmd}\` submitted.`);
2378
2562
  }
@@ -2392,22 +2576,23 @@ async function main() {
2392
2576
  const cmdArgs = parts.slice(1);
2393
2577
  const handler = cmds[cmd];
2394
2578
  if (handler) {
2395
- // Disconnected guard: only /help, /servers, /force-switch, /start, /comment, /shutdown, /serve, and /new work without a server
2396
- if (!cs.serverBase && cmd !== 'help' && cmd !== 'servers' && cmd !== 'force-switch' && cmd !== 'start' && cmd !== 'comment' && cmd !== 'shutdown' && cmd !== 'serve' && cmd !== 'new') {
2579
+ // Disconnected guard: only /help, /servers, /force-switch, /start, /comment, /shutdown, /serve, /new, and /resurrect work without a server
2580
+ if (!cs.serverBase && cmd !== 'help' && cmd !== 'servers' && cmd !== 'force-switch' && cmd !== 'start' && cmd !== 'comment' && cmd !== 'shutdown' && cmd !== 'serve' && cmd !== 'new' && cmd !== 'resurrect') {
2397
2581
  await sendLongMessage(token, cs,
2398
2582
  '🔌 Not connected to any server.\n\n' +
2399
2583
  'Use `/servers` to see available servers and `/servers switch <name>` to connect.');
2400
2584
  continue;
2401
2585
  }
2402
2586
  try {
2403
- await handler(token, chatId, cmdArgs, cs, setServerForChat, setActiveSessionForChat);
2587
+ const replyToMsgId = msg.reply_to_message ? msg.reply_to_message.message_id : 0;
2588
+ await handler(token, chatId, cmdArgs, cs, setServerForChat, setActiveSessionForChat, replyTo, replyToMsgId);
2404
2589
  } catch (err) {
2405
2590
  log.error(`Error handling /${cmd}: ${err.message}`);
2406
2591
  try { await sendLongMessage(token, cs, `Error: ${err.message}`); } catch (_) {}
2407
2592
  }
2408
2593
  } else {
2409
2594
  await sendLongMessage(token, cs,
2410
- `Unknown command: /${cmd}\n\nAvailable commands: /help, /status, /servers, /sessions, /agents, /models, /think, /record, /queue, /flush, /note, /comment, /disconnect, /serve, /new`);
2595
+ `Unknown command: /${cmd}\n\nAvailable commands: /help, /status, /servers, /sessions, /agents, /models, /think, /record, /queue, /flush, /note, /comment, /disconnect, /serve, /new, /markdown`);
2411
2596
  }
2412
2597
  continue;
2413
2598
  }
@@ -2441,7 +2626,7 @@ async function main() {
2441
2626
  const qlen = st.messageQueue.length;
2442
2627
  const servers = listServers();
2443
2628
  const suggestions = [];
2444
- suggestions.push(`• Start the server: \`ocmux serve\``);
2629
+ suggestions.push(`• Restart it: \`/resurrect\``);
2445
2630
  if (servers.length > 0) {
2446
2631
  suggestions.push(`• Switch to another server: \`/servers switch <name>\``);
2447
2632
  }
@@ -2476,7 +2661,11 @@ process.on('SIGINT', () => {
2476
2661
  process.exit(0);
2477
2662
  });
2478
2663
 
2479
- main().catch((err) => {
2480
- console.error('Error:', err.message);
2481
- process.exit(1);
2482
- });
2664
+ if (require.main === module) {
2665
+ main().catch((err) => {
2666
+ console.error('Error:', err.message);
2667
+ process.exit(1);
2668
+ });
2669
+ }
2670
+
2671
+ module.exports = { processTelegramCommands, parseTelegramLine, getHelpText, normalizeUrl };
package/lib/ocmux.js CHANGED
@@ -130,6 +130,37 @@ function listServers() {
130
130
  return rows;
131
131
  }
132
132
 
133
+ // Check the state of a server in a given directory.
134
+ // Returns null if no .ocmux.json exists, or an object with status.
135
+ function serverStatus(dir) {
136
+ const sf = statefileFor(dir);
137
+ const state = readState(sf);
138
+ if (!state || !state.url) return null;
139
+ const winIdx = windowByDir(dir);
140
+ const tui = winIdx != null ? tuiPaneId(winIdx) : null;
141
+ return {
142
+ dir,
143
+ url: state.url,
144
+ status: winIdx != null ? 'alive' : 'dead',
145
+ windowIndex: winIdx,
146
+ hasTui: tui != null,
147
+ };
148
+ }
149
+
150
+ // List dead servers by scanning known directories for orphaned .ocmux.json files.
151
+ // dirs: array of directories to check (e.g. from connections file).
152
+ // Returns entries with status 'dead' that are not already in listServers().
153
+ function listDeadServers(knownDirs) {
154
+ const alive = new Set(listServers().map(s => s.dir));
155
+ const dead = [];
156
+ for (const dir of knownDirs) {
157
+ if (alive.has(dir)) continue;
158
+ const status = serverStatus(dir);
159
+ if (status) dead.push(status);
160
+ }
161
+ return dead;
162
+ }
163
+
133
164
  // Switch tmux focus to the given server window and ensure the TUI pane is alive and zoomed.
134
165
  function activateServer(dir, index, url) {
135
166
  const winIdx = index;
@@ -182,6 +213,7 @@ function activateServer(dir, index, url) {
182
213
 
183
214
  // Recover a dead/crashed server: kill old window, remove state file,
184
215
  // create fresh server + TUI in the same directory.
216
+ // Delegates to startServer for the actual server creation.
185
217
  // Returns { url, dir } on success, throws on error.
186
218
  function resurrectServer(dir, printLogs) {
187
219
  const sf = path.join(dir, '.ocmux.json');
@@ -209,7 +241,14 @@ function resurrectServer(dir, printLogs) {
209
241
  // Remove old state file so we can create a fresh one
210
242
  try { fs.unlinkSync(sf); } catch {}
211
243
 
212
- // Create fresh server + TUI
244
+ // Start fresh server (same logic as ocmux serve)
245
+ return startServer(dir, printLogs);
246
+ }
247
+
248
+ // Start a new opencode server in the given directory.
249
+ // Creates a tmux window, starts opencode serve, waits for the URL,
250
+ // and writes .ocmux.json. Returns { url, dir } on success.
251
+ function startServer(dir, printLogs) {
213
252
  ensureSession();
214
253
  const lf = logfileFor(dir);
215
254
 
@@ -223,16 +262,16 @@ function resurrectServer(dir, printLogs) {
223
262
 
224
263
  try { fs.truncateSync(lf); } catch {}
225
264
 
226
- const serveFlags = printLogs ? '--port 0 --print-logs' : '--port 0';
265
+ const serveFlags = printLogs ? '--print-logs' : '';
227
266
  _tmux(['send-keys', '-t', `${SESSION}:${winIdx}.0`,
228
- `opencode serve ${serveFlags} 2>&1 | tee '${lf}'`, 'Enter']);
267
+ 'opencode serve' + (serveFlags ? ' ' + serveFlags : '') + ` 2>&1 | tee '${lf}'`, 'Enter']);
229
268
 
230
269
  let url = null;
231
270
  for (let i = 0; i < 50; i++) {
232
271
  sleep(0.2);
233
272
  const log = (() => { try { return fs.readFileSync(lf, 'utf8'); } catch { return ''; } })();
234
273
  const m = log.match(URL_RE);
235
- if (m) url = m[1];
274
+ if (m) { url = m[1]; break; }
236
275
  }
237
276
 
238
277
  if (!url) {
@@ -242,7 +281,7 @@ function resurrectServer(dir, printLogs) {
242
281
  }
243
282
 
244
283
  const newState = { url, logfile: lf, window_index: winIdx };
245
- fs.writeFileSync(sf, JSON.stringify(newState) + '\n');
284
+ fs.writeFileSync(path.join(dir, '.ocmux.json'), JSON.stringify(newState) + '\n');
246
285
 
247
286
  const splitResult = _tmux(['split-window', '-v', '-P', '-F', '#{pane_id}', '-t', `${SESSION}:${winIdx}`, '-c', dir]);
248
287
  const tuiPane = splitResult.stdout.trim();
@@ -278,4 +317,7 @@ module.exports = {
278
317
  ensureSession,
279
318
  pinWindowName,
280
319
  resurrectServer,
320
+ startServer,
321
+ serverStatus,
322
+ listDeadServers,
281
323
  };
@@ -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.7",
3
+ "version": "0.11.9",
4
4
  "description": "Pipe prompts into a running OpenCode TUI session and stream the final answer",
5
5
  "bin": {
6
6
  "agentp": "bin/agentp",