agentp 0.11.7 → 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 +8 -4
- package/bin/tgagentp +259 -108
- package/lib/telegram-format.js +4 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
@@ -235,6 +235,8 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
|
|
|
235
235
|
| `/help [topic]` | Show general help or help for a topic (`servers`, `sessions`, `agents`, `models`, `allow`, `think`, `record`, `queue`) |
|
|
236
236
|
| `/servers` | List all running ocmux-served projects with URL + status (✅ idle / ⏳ busy) |
|
|
237
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` |
|
|
238
240
|
| `/sessions` | List recent sessions for the current server (max 50, with date headings) |
|
|
239
241
|
| `/sessions switch <number\|name>` | Switch active session by position or partial name match |
|
|
240
242
|
| `/agents` | List primary agents (▶ marker for the active one) |
|
|
@@ -251,6 +253,7 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
|
|
|
251
253
|
| `/reject` | Deny a permission request |
|
|
252
254
|
| `/always` | Approve and remember for the session |
|
|
253
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 |
|
|
254
257
|
| `/shutdown [force\|clear]` | (requires `--dev`) Stop tgagentp; `clear` also wipes saved connections |
|
|
255
258
|
|
|
256
259
|
#### TUI Command Passthrough
|
|
@@ -328,6 +331,7 @@ Chat-to-server directory mappings are saved to `/tmp/tgagentp-connections.json`
|
|
|
328
331
|
| `TGAGENTP_ALLOWED_CHAT_IDS` | Optional. Comma-separated Telegram chat IDs that are allowed to use the bot. |
|
|
329
332
|
| `TGAGENTP_PORT` | Optional. Agentp gateway listen port (default: `0` = random). |
|
|
330
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). |
|
|
331
335
|
|
|
332
336
|
## License
|
|
333
337
|
|
package/bin/tgagentp
CHANGED
|
@@ -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 {
|
|
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
|
-
//
|
|
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: \`${
|
|
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' +
|
|
@@ -557,6 +589,7 @@ function sessionName(s) {
|
|
|
557
589
|
}
|
|
558
590
|
|
|
559
591
|
function normalizeUrl(url) {
|
|
592
|
+
if (!url) return url;
|
|
560
593
|
return url.replace(/\/+$/, '').replace(/^http:\/\/127\.0\.0\.1:/, 'http://localhost:');
|
|
561
594
|
}
|
|
562
595
|
|
|
@@ -600,7 +633,7 @@ async function cmdSessions(token, chatId, args, chatState, setServerForChat, set
|
|
|
600
633
|
try {
|
|
601
634
|
const session = await createSession(server, title);
|
|
602
635
|
const name = session.title || session.slug || session.id;
|
|
603
|
-
setActiveSessionForChat(chatState, session.id);
|
|
636
|
+
setActiveSessionForChat(chatState, session.id, name);
|
|
604
637
|
selectSession(server, session.id);
|
|
605
638
|
await sendLongMessage(token, chatState,
|
|
606
639
|
`Created session: \`${name}\`\nIt is now the active session.`);
|
|
@@ -652,7 +685,7 @@ async function cmdSessions(token, chatId, args, chatState, setServerForChat, set
|
|
|
652
685
|
const num = parseInt(query, 10);
|
|
653
686
|
if (/^\d+$/.test(query) && num >= 1 && num <= sorted.length) {
|
|
654
687
|
const target = sorted[num - 1];
|
|
655
|
-
setActiveSessionForChat(
|
|
688
|
+
setActiveSessionForChat(chatState, target.id, sessionName(target));
|
|
656
689
|
selectSession(server, target.id);
|
|
657
690
|
await sendLongMessage(token, chatState,
|
|
658
691
|
`Switched to session #${num}: \`${sessionName(target)}\`\nSubsequent messages will be sent to this session.`);
|
|
@@ -674,7 +707,7 @@ async function cmdSessions(token, chatId, args, chatState, setServerForChat, set
|
|
|
674
707
|
return;
|
|
675
708
|
}
|
|
676
709
|
const target = matches[0];
|
|
677
|
-
setActiveSessionForChat(
|
|
710
|
+
setActiveSessionForChat(chatState, target.id);
|
|
678
711
|
selectSession(server, target.id);
|
|
679
712
|
await sendLongMessage(token, chatState,
|
|
680
713
|
`Switched to session: \`${sessionName(target)}\`\nSubsequent messages will be sent to this session.`);
|
|
@@ -704,10 +737,13 @@ async function cmdSessions(token, chatId, args, chatState, setServerForChat, set
|
|
|
704
737
|
return;
|
|
705
738
|
}
|
|
706
739
|
|
|
707
|
-
// Show
|
|
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);
|
|
708
744
|
const activeId = getState(server).activeSessionId;
|
|
709
745
|
const filtered = sessions.filter(s =>
|
|
710
|
-
(s
|
|
746
|
+
(hasPrimaryAgent(s) || s.id === activeId) && (!projectDir || s.directory === projectDir));
|
|
711
747
|
const sorted = sortedSessions(filtered, 50);
|
|
712
748
|
if (sorted.length === 0) {
|
|
713
749
|
await sendLongMessage(token, chatState, 'No active sessions.');
|
|
@@ -891,8 +927,6 @@ function startAgentpGateway(token, serverBaseResolver) {
|
|
|
891
927
|
}
|
|
892
928
|
if (req.url === '/send') {
|
|
893
929
|
handleSendRequest(req, res, token, serverBaseResolver);
|
|
894
|
-
} else if (req.url === '/send-file') {
|
|
895
|
-
handleSendFileRequest(req, res, token, serverBaseResolver);
|
|
896
930
|
} else {
|
|
897
931
|
res.writeHead(404);
|
|
898
932
|
res.end('Not Found');
|
|
@@ -944,7 +978,9 @@ function handleSendRequest(req, res, token, serverBaseResolver) {
|
|
|
944
978
|
const activeUrl = normalizeUrl(serverBaseResolver());
|
|
945
979
|
|
|
946
980
|
if (targetUrl === activeUrl) {
|
|
947
|
-
|
|
981
|
+
processTelegramCommands(text, server, token, owningChatId).then((cleaned) => {
|
|
982
|
+
if (cleaned) sendLongMessage(token, owningChatId, cleaned).catch(() => {});
|
|
983
|
+
});
|
|
948
984
|
} else {
|
|
949
985
|
if (!agentpQueues[server]) {
|
|
950
986
|
agentpQueues[server] = { messages: [], notifyTimer: null, notified: false };
|
|
@@ -969,72 +1005,122 @@ function handleSendRequest(req, res, token, serverBaseResolver) {
|
|
|
969
1005
|
});
|
|
970
1006
|
}
|
|
971
1007
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
+
}
|
|
989
1028
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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
|
+
};
|
|
993
1049
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
}
|
|
1050
|
+
function getHelpText(topic) {
|
|
1051
|
+
if (topic && TELEGRAM_COMMANDS_HELP[topic]) return TELEGRAM_COMMANDS_HELP[topic];
|
|
1052
|
+
return TELEGRAM_COMMANDS_HELP.all;
|
|
1053
|
+
}
|
|
999
1054
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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;
|
|
1006
1072
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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;
|
|
1012
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
|
+
}
|
|
1013
1112
|
|
|
1014
|
-
|
|
1015
|
-
const fileName = path.basename(fullPath);
|
|
1113
|
+
if (toStrip.size === 0) return text;
|
|
1016
1114
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
-
});
|
|
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();
|
|
1038
1124
|
}
|
|
1039
1125
|
|
|
1040
1126
|
async function cmdAllow(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
@@ -1490,6 +1576,26 @@ async function cmdNew(token, chatId, args, chatState, setServerForChat, setActiv
|
|
|
1490
1576
|
log.info(`[chat ${chatId}] Created ${targetDir} → ${url}`);
|
|
1491
1577
|
}
|
|
1492
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
|
+
|
|
1493
1599
|
const COMMANDS = {
|
|
1494
1600
|
'help': cmdHelp,
|
|
1495
1601
|
'servers': cmdServers,
|
|
@@ -1511,6 +1617,7 @@ const COMMANDS = {
|
|
|
1511
1617
|
'answer': cmdAnswer,
|
|
1512
1618
|
'serve': cmdServe,
|
|
1513
1619
|
'new': cmdNew,
|
|
1620
|
+
'markdown': cmdMarkdown,
|
|
1514
1621
|
};
|
|
1515
1622
|
|
|
1516
1623
|
// --------------------------------------------------------------------------
|
|
@@ -1519,7 +1626,8 @@ const COMMANDS = {
|
|
|
1519
1626
|
|
|
1520
1627
|
const serverState = {};
|
|
1521
1628
|
function getState(url) {
|
|
1522
|
-
|
|
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: [] };
|
|
1523
1631
|
}
|
|
1524
1632
|
|
|
1525
1633
|
function sessionLabel(st) {
|
|
@@ -1681,6 +1789,9 @@ async function main() {
|
|
|
1681
1789
|
serverOwners[url] = { chatId: cs.chatId, threadId: cs.threadId };
|
|
1682
1790
|
getState(url);
|
|
1683
1791
|
|
|
1792
|
+
// Send Telegram bridge awareness note to the new session
|
|
1793
|
+
sendGreeting(cs).catch(() => {});
|
|
1794
|
+
|
|
1684
1795
|
// Auto-activate tmux window for this server
|
|
1685
1796
|
const servers = listServers();
|
|
1686
1797
|
const normalized = normalizeUrl(url);
|
|
@@ -1717,12 +1828,48 @@ async function main() {
|
|
|
1717
1828
|
return null;
|
|
1718
1829
|
};
|
|
1719
1830
|
|
|
1720
|
-
const setActiveSessionForChat = (chatState, id) => {
|
|
1831
|
+
const setActiveSessionForChat = (chatState, id, name) => {
|
|
1721
1832
|
const server = chatState.serverBase;
|
|
1833
|
+
const prevId = getState(server).activeSessionId;
|
|
1722
1834
|
getState(server).activeSessionId = id;
|
|
1723
|
-
|
|
1835
|
+
if (name) getState(server).activeSessionName = name;
|
|
1836
|
+
log.info(`[chat ${chatState.chatId}] Switched to session: ${name || id}`);
|
|
1837
|
+
if (id !== prevId) sendGreeting(chatState);
|
|
1724
1838
|
};
|
|
1725
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
|
+
|
|
1726
1873
|
// Start agentp gateway (tiny HTTP server for agentp --tg)
|
|
1727
1874
|
const serverBaseResolver = () => {
|
|
1728
1875
|
const entries = Object.entries(serverOwners);
|
|
@@ -1813,7 +1960,7 @@ async function main() {
|
|
|
1813
1960
|
}, cancelRef).catch(() => {});
|
|
1814
1961
|
await onConnected;
|
|
1815
1962
|
answer = await sendToSession(url, sessionId, `[Telegram] ${text}`, st.activeAgentName, sessionCancelRef);
|
|
1816
|
-
if (st._thinkingBuf) {
|
|
1963
|
+
if (st.thinkingEnabled && st._thinkingBuf) {
|
|
1817
1964
|
const html = '💭 ' + st._thinkingBuf.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1818
1965
|
sendLongMessage(token, chatState, html, replyTo, threadId).catch(() => {});
|
|
1819
1966
|
st._thinkingBuf = '';
|
|
@@ -1834,9 +1981,13 @@ async function main() {
|
|
|
1834
1981
|
if (url === chatServer) {
|
|
1835
1982
|
if (answer && answer.trim()) {
|
|
1836
1983
|
const trimmed = answer.trim();
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
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
|
+
}
|
|
1840
1991
|
}
|
|
1841
1992
|
} else {
|
|
1842
1993
|
getState(url).pendingResponse = answer;
|
|
@@ -2215,21 +2366,6 @@ async function main() {
|
|
|
2215
2366
|
}
|
|
2216
2367
|
try {
|
|
2217
2368
|
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
2369
|
const userText = msg.caption || msg.text || '';
|
|
2234
2370
|
const fullMessage = notification + (userText ? '\n\nUser message: ' + userText : '');
|
|
2235
2371
|
log.debug(`[chat ${chatId}] ${from}: [file upload]`);
|
|
@@ -2330,11 +2466,16 @@ async function main() {
|
|
|
2330
2466
|
fileName = 'photo.jpg';
|
|
2331
2467
|
fileSize = largest.file_size || 0;
|
|
2332
2468
|
}
|
|
2333
|
-
const fileData = await downloadTelegramFile(token, fileId);
|
|
2334
|
-
ensureSharedDir(projectDir);
|
|
2335
|
-
const relativePath = saveUploadedFile(projectDir, fileName, fileData);
|
|
2336
2469
|
const sizeStr = fileSize > 0 ? ` (${formatFileSize(fileSize)})` : '';
|
|
2337
|
-
|
|
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
|
+
}
|
|
2338
2479
|
} catch (err) {
|
|
2339
2480
|
log.error(`Download on reply error: ${err.message}`);
|
|
2340
2481
|
await sendLongMessage(token, cs, `❌ File download failed: ${err.message}`);
|
|
@@ -2372,7 +2513,12 @@ async function main() {
|
|
|
2372
2513
|
try {
|
|
2373
2514
|
const answer = await executeTuiCommand(paneId, cs.serverBase, tuiCmd);
|
|
2374
2515
|
if (answer && answer.trim()) {
|
|
2375
|
-
await
|
|
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
|
+
}
|
|
2376
2522
|
} else {
|
|
2377
2523
|
await sendLongMessage(token, cs, `✅ \`${tuiCmd}\` submitted.`);
|
|
2378
2524
|
}
|
|
@@ -2400,14 +2546,15 @@ async function main() {
|
|
|
2400
2546
|
continue;
|
|
2401
2547
|
}
|
|
2402
2548
|
try {
|
|
2403
|
-
|
|
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);
|
|
2404
2551
|
} catch (err) {
|
|
2405
2552
|
log.error(`Error handling /${cmd}: ${err.message}`);
|
|
2406
2553
|
try { await sendLongMessage(token, cs, `Error: ${err.message}`); } catch (_) {}
|
|
2407
2554
|
}
|
|
2408
2555
|
} else {
|
|
2409
2556
|
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`);
|
|
2557
|
+
`Unknown command: /${cmd}\n\nAvailable commands: /help, /status, /servers, /sessions, /agents, /models, /think, /record, /queue, /flush, /note, /comment, /disconnect, /serve, /new, /markdown`);
|
|
2411
2558
|
}
|
|
2412
2559
|
continue;
|
|
2413
2560
|
}
|
|
@@ -2476,7 +2623,11 @@ process.on('SIGINT', () => {
|
|
|
2476
2623
|
process.exit(0);
|
|
2477
2624
|
});
|
|
2478
2625
|
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
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 };
|
package/lib/telegram-format.js
CHANGED
|
@@ -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 = {
|