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 +12 -4
- package/bin/tgagentp +456 -119
- 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
|
|
|
@@ -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 {
|
|
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
|
-
//
|
|
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: \`${
|
|
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
|
-
'`/
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
-
|
|
968
|
-
const fileName = path.basename(fullPath);
|
|
1113
|
+
if (toStrip.size === 0) return text;
|
|
969
1114
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1577
|
-
|
|
1578
|
-
if (
|
|
1579
|
-
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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 (
|
|
1632
|
-
const html = '💭 ' +
|
|
1963
|
+
if (st.thinkingEnabled && st._thinkingBuf) {
|
|
1964
|
+
const html = '💭 ' + st._thinkingBuf.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1633
1965
|
sendLongMessage(token, chatState, html, replyTo, threadId).catch(() => {});
|
|
1634
|
-
|
|
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
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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
|
-
|
|
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
|
|
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 /
|
|
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
|
-
|
|
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
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
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 };
|
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 = {
|