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 +8 -17
- package/bin/ocmux +2 -1
- package/bin/tgagentp +336 -147
- package/lib/ocmux.js +47 -5
- 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
|
|
@@ -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
|
|
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
|
-
| `/
|
|
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 ? '--
|
|
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 {
|
|
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' +
|
|
@@ -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
|
-
|
|
533
|
-
|
|
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 =
|
|
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 ? '
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
-
|
|
1015
|
-
const fileName = path.basename(fullPath);
|
|
1122
|
+
if (toStrip.size === 0) return text;
|
|
1016
1123
|
|
|
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
|
-
});
|
|
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
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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
|
-
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
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
|
-
`•
|
|
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
|
-
'
|
|
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
|
-
`•
|
|
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
|
-
|
|
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
|
|
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 /
|
|
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
|
-
|
|
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(`•
|
|
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
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
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
|
-
//
|
|
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 ? '--
|
|
265
|
+
const serveFlags = printLogs ? '--print-logs' : '';
|
|
227
266
|
_tmux(['send-keys', '-t', `${SESSION}:${winIdx}.0`,
|
|
228
|
-
|
|
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(
|
|
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
|
};
|
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 = {
|