arena-bridge 0.6.10 → 0.7.1
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/CHANGELOG.md +26 -0
- package/README.md +41 -1
- package/package.json +1 -1
- package/src/approvals.mjs +12 -0
- package/src/cli.mjs +94 -0
- package/src/config.mjs +8 -0
- package/src/direct.mjs +30 -1
- package/src/engine.mjs +2 -1
- package/src/hub.mjs +1 -1
- package/src/mcp.mjs +1 -1
- package/src/prompt.mjs +10 -0
- package/src/telegram.mjs +446 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
The format follows [Keep a Changelog](https://keepachangelog.com/) and the project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## 0.7.1
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- The Telegram listener no longer gives up on a long poll when the network is slow: the request
|
|
11
|
+
ceiling is now generous enough for the 20-second long poll to answer.
|
|
12
|
+
|
|
13
|
+
## 0.7.0
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- **Telegram link.** The bridge can now talk to the user outside the agent chat:
|
|
17
|
+
- `notify_user` — send a message (progress, a question, a result); with
|
|
18
|
+
`{"wait_for_reply": true}` the call waits for the answer in Telegram.
|
|
19
|
+
- `telegram_inbox` — read the messages the user sent to the bot.
|
|
20
|
+
- `telegram_status` — the bot, the allowed chats, the number of unread messages.
|
|
21
|
+
- `arena-bridge telegram setup|test|status|inbox|poll` — the same link from the terminal.
|
|
22
|
+
- **Approvals from Telegram.** A pending request is announced in the chat right away, and
|
|
23
|
+
the user can settle it with `/approve <id>` or `/deny <id>` (the terminal still works:
|
|
24
|
+
`arena-bridge approve <id>`).
|
|
25
|
+
- The bridge listens for incoming bot messages on its own (the launchd service does it
|
|
26
|
+
automatically); the inbox lives in `~/.arena-bridge/telegram/`.
|
|
27
|
+
|
|
28
|
+
### Security
|
|
29
|
+
- The bot token stays in `~/.arena-bridge/config.json` (0600), which the agent cannot read.
|
|
30
|
+
- The bridge may only write to the chats listed in `--chat-id`; 20 messages per minute,
|
|
31
|
+
4000 characters per message.
|
|
32
|
+
|
|
7
33
|
## 0.6.10
|
|
8
34
|
|
|
9
35
|
### Fixed
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="license">
|
|
13
13
|
<img src="https://img.shields.io/badge/node-%3E%3D18-green.svg?style=flat-square" alt="node">
|
|
14
14
|
<img src="https://img.shields.io/badge/dependencies-0-brightgreen.svg?style=flat-square" alt="dependencies">
|
|
15
|
-
<img src="https://img.shields.io/badge/tests-
|
|
15
|
+
<img src="https://img.shields.io/badge/tests-147%20passed-brightgreen.svg?style=flat-square" alt="tests">
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
@@ -100,6 +100,7 @@ arena-bridge setup --root ~/projects
|
|
|
100
100
|
| Процессы | `start_process` `read_process_output` `write_process` `kill_process` | фоновые дев-серверы |
|
|
101
101
|
| Chrome | `chrome_launch` `chrome_profiles` `chrome_attach` `chrome_open` `chrome_navigate` `chrome_snapshot` `chrome_click` `chrome_type` `chrome_press` `chrome_screenshot` `chrome_evaluate` `chrome_tabs` `chrome_stop` | браузер через CDP |
|
|
102
102
|
| Десктоп | `mac_see` `mac_element` `mac_click` `mac_set` `mac_type` `mac_key` `mac_scroll` `mac_move` `mac_drag` `mac_menu` `mac_menu_click` `mac_wait` `mac_spaces` `mac_space_switch` `mac_apps` `mac_windows` `mac_app_launch` `mac_app_focus` `mac_app_quit` `mac_permissions` | macOS через Peekaboo |
|
|
103
|
+
| Telegram | `notify_user` `telegram_inbox` `telegram_status` | сообщения вам и ваши ответы |
|
|
103
104
|
|
|
104
105
|
Полный каталог с JSON-схемами: `GET /tools` или `arena-bridge tools`.
|
|
105
106
|
|
|
@@ -118,6 +119,7 @@ arena-bridge setup --root ~/projects
|
|
|
118
119
|
| кликнуть в нативном приложении | `mac_click` (id элемента, видимый текст или координаты) |
|
|
119
120
|
| напечатать | `mac_type`, затем `mac_key` для Enter/Tab/cmd+s |
|
|
120
121
|
| посмотреть файлы | `list_dir` (только верхний уровень; глубже — `{"recursive":true}`) |
|
|
122
|
+
| написать вам или спросить | `notify_user` (Telegram), ответ читать через `telegram_inbox` |
|
|
121
123
|
|
|
122
124
|
---
|
|
123
125
|
|
|
@@ -219,6 +221,43 @@ arena-bridge audit dir # где всё это лежит
|
|
|
219
221
|
|
|
220
222
|
---
|
|
221
223
|
|
|
224
|
+
## Связь с Telegram
|
|
225
|
+
|
|
226
|
+
Агент может писать вам в Telegram и читать ваши ответы — даже если вы сейчас не в чате с ним.
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
# один раз: привязать бота (токен от @BotFather и ваш chat id)
|
|
230
|
+
arena-bridge telegram setup --token 123456:AA... --chat-id 190771108
|
|
231
|
+
|
|
232
|
+
# проверить связь
|
|
233
|
+
arena-bridge telegram test
|
|
234
|
+
|
|
235
|
+
# посмотреть, что вам писали
|
|
236
|
+
arena-bridge telegram inbox
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Что это даёт:
|
|
240
|
+
- `notify_user` — агент пишет вам: «сделал», «нужно ваше решение», «вот результат».
|
|
241
|
+
С `{"wait_for_reply": true}` вызов ждёт вашего ответа прямо в Telegram.
|
|
242
|
+
- `telegram_inbox` — агент читает то, что вы написали боту: ответы, правки, поручения.
|
|
243
|
+
- `telegram_status` — бот, список чатов, число непрочитанных сообщений.
|
|
244
|
+
- **Подтверждения из Telegram.** Когда агенту нужно ваше «да», мост сам пишет вам:
|
|
245
|
+
`/approve <id>` — разрешить, `/deny <id>` — отказать. Из терминала тоже можно:
|
|
246
|
+
`arena-bridge approve <id>`.
|
|
247
|
+
|
|
248
|
+
Как это устроено и что защищено:
|
|
249
|
+
- токен бота лежит только в `~/.arena-bridge/config.json` (права `0600`); агент этот файл
|
|
250
|
+
прочитать не может — он в списке защищённых путей;
|
|
251
|
+
- писать можно только в те чаты, что перечислены в `--chat-id` (по умолчанию — ваш);
|
|
252
|
+
- не больше 20 сообщений в минуту, одно сообщение — до 4000 символов;
|
|
253
|
+
- входящие сообщения мост слушает сам (служба launchd — автоматически), входящие лежат
|
|
254
|
+
в `~/.arena-bridge/telegram/`.
|
|
255
|
+
|
|
256
|
+
Одно уточнение: для бота `api_id`/`api_hash` не нужны — они пригодятся только для режима
|
|
257
|
+
«личного аккаунта» (MTProto), который пока не включён.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
222
261
|
## Доверенное окно: работать без подтверждений
|
|
223
262
|
|
|
224
263
|
```bash
|
|
@@ -319,6 +358,7 @@ arena-bridge audit -n 50 # что происходило
|
|
|
319
358
|
| `service` | macOS-фоновый сервис: `install` / `uninstall` / `status` |
|
|
320
359
|
| `browser` | профили браузера агента: `list` / `open <name>` / `close <name>` |
|
|
321
360
|
| `rotate` | сменить агентский или девайсный токен |
|
|
361
|
+
| `telegram` | связь с Telegram: `setup` / `test` / `status` / `inbox` / `poll` |
|
|
322
362
|
|
|
323
363
|
---
|
|
324
364
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arena-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Give a cloud AI agent (Arena.ai Agent Mode, Claude, Cursor, ...) controlled access to your own machine over HTTP/MCP: files, shell, Chrome and the macOS desktop, with an approval gate, trust windows and an audit log.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/approvals.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import { APPROVALS_DIR, ensureDirs } from './config.mjs';
|
|
5
|
+
import { sendTelegram } from './telegram.mjs';
|
|
5
6
|
|
|
6
7
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
7
8
|
|
|
@@ -74,6 +75,17 @@ export async function requestApproval(tool, args, cfg) {
|
|
|
74
75
|
};
|
|
75
76
|
fs.writeFileSync(fileOf(id), JSON.stringify(rec, null, 2), { mode: 0o600 });
|
|
76
77
|
|
|
78
|
+
// tell the user about it right away, so a pending request is not a dead end
|
|
79
|
+
if (cfg.telegram?.enabled && cfg.telegram?.botToken) {
|
|
80
|
+
sendTelegram(cfg, {
|
|
81
|
+
title: 'Approval needed',
|
|
82
|
+
text:
|
|
83
|
+
`Tool: ${tool}\n` +
|
|
84
|
+
`id: ${id}\n` +
|
|
85
|
+
`Reply /approve ${id} or /deny ${id} in Telegram, or run: arena-bridge approve ${id}`,
|
|
86
|
+
}).catch(() => {});
|
|
87
|
+
}
|
|
88
|
+
|
|
77
89
|
const waitSec = Math.min(cfg.approvalWaitSec ?? 90, cfg.approvalTtlSec ?? 900);
|
|
78
90
|
const deadline = now + waitSec * 1000;
|
|
79
91
|
while (Date.now() < deadline) {
|
package/src/cli.mjs
CHANGED
|
@@ -17,6 +17,13 @@ import { findPeekaboo } from './mac.mjs';
|
|
|
17
17
|
import { findChrome, listProfiles, resolveProfile, sanitizeProfile } from './browser.mjs';
|
|
18
18
|
import { packageInfo, latestVersion, compareVersions, selfUpdate } from './selfupdate.mjs';
|
|
19
19
|
import { serviceStatus, serviceInstall, serviceUninstall } from './service.mjs';
|
|
20
|
+
import {
|
|
21
|
+
sendTelegram,
|
|
22
|
+
telegramSettings,
|
|
23
|
+
readInbox,
|
|
24
|
+
inboxState,
|
|
25
|
+
startPoller,
|
|
26
|
+
} from './telegram.mjs';
|
|
20
27
|
|
|
21
28
|
const REPEATABLE_FLAGS = new Set(['root']);
|
|
22
29
|
|
|
@@ -74,6 +81,12 @@ Commands:
|
|
|
74
81
|
--paths p1,p2 only these paths (bash is not covered)
|
|
75
82
|
--off | --status
|
|
76
83
|
audit [-n N] Show the audit log
|
|
84
|
+
telegram Talk to the user in Telegram
|
|
85
|
+
setup --token <bot token> --chat-id <id>
|
|
86
|
+
test send a test message
|
|
87
|
+
status show the bot, the chats, the unread count
|
|
88
|
+
inbox [-n N] show the messages the user sent
|
|
89
|
+
poll listen for messages in the foreground
|
|
77
90
|
doctor Check the installation and configuration
|
|
78
91
|
url Print the public URL the agent should use
|
|
79
92
|
version Show the installed version and the latest one on npm
|
|
@@ -583,6 +596,85 @@ function cmdRotate(args) {
|
|
|
583
596
|
);
|
|
584
597
|
}
|
|
585
598
|
|
|
599
|
+
async function cmdTelegram(args) {
|
|
600
|
+
const cfg = loadConfig();
|
|
601
|
+
const sub = args._[1] || 'status';
|
|
602
|
+
|
|
603
|
+
if (sub === 'setup') {
|
|
604
|
+
const t = { ...(cfg.telegram || {}) };
|
|
605
|
+
if (args.flags.token) t.botToken = String(args.flags.token);
|
|
606
|
+
if (args.flags['chat-id']) {
|
|
607
|
+
t.chatIds = String(args.flags['chat-id'])
|
|
608
|
+
.split(',')
|
|
609
|
+
.map((v) => v.trim())
|
|
610
|
+
.filter(Boolean);
|
|
611
|
+
}
|
|
612
|
+
if (args.flags['api-id']) t.apiId = String(args.flags['api-id']);
|
|
613
|
+
if (args.flags['api-hash']) t.apiHash = String(args.flags['api-hash']);
|
|
614
|
+
if (args.flags['no-poll']) t.poll = false;
|
|
615
|
+
if (args.flags.poll) t.poll = true;
|
|
616
|
+
t.enabled = !!(t.botToken && (t.chatIds || []).length);
|
|
617
|
+
cfg.telegram = t;
|
|
618
|
+
saveConfig(cfg);
|
|
619
|
+
console.log(t.enabled ? '✓ Telegram configured' : '✗ both --token and --chat-id are required');
|
|
620
|
+
if (t.enabled) {
|
|
621
|
+
const res = await sendTelegram(cfg, {
|
|
622
|
+
title: 'arena-bridge',
|
|
623
|
+
text: 'Telegram is connected. The agent can now message you and read your answers.',
|
|
624
|
+
});
|
|
625
|
+
console.log(res.ok ? '✓ test message sent' : `✗ ${res.error}: ${res.reason || ''}`);
|
|
626
|
+
}
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (sub === 'test') {
|
|
631
|
+
const res = await sendTelegram(cfg, {
|
|
632
|
+
title: 'arena-bridge',
|
|
633
|
+
text: 'Test message from the bridge. If you can read this, the link works.',
|
|
634
|
+
});
|
|
635
|
+
console.log(res.ok ? '✓ message sent' : `✗ ${res.error}: ${res.reason || ''}`);
|
|
636
|
+
if (!res.ok) process.exitCode = 1;
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (sub === 'inbox') {
|
|
641
|
+
const limit = Math.min(100, Number(args.flags.n) || 20);
|
|
642
|
+
const items = readInbox({ limit });
|
|
643
|
+
if (!items.length) console.log('(no messages yet)');
|
|
644
|
+
for (const i of items) {
|
|
645
|
+
console.log(`#${i.id} ${i.from} ${i.read ? '' : '(new) '}${i.text}`);
|
|
646
|
+
}
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (sub === 'poll') {
|
|
651
|
+
const s = telegramSettings(cfg);
|
|
652
|
+
if (!s.enabled) {
|
|
653
|
+
console.log('✗ Telegram is not configured. Run: arena-bridge telegram setup --token <bot token> --chat-id <id>');
|
|
654
|
+
process.exitCode = 1;
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
console.log('listening for Telegram messages (Ctrl+C to stop)…');
|
|
658
|
+
const poller = startPoller(cfg, {
|
|
659
|
+
onMessage: async (item) => console.log(`#${item.id} ${item.from}: ${item.text}`),
|
|
660
|
+
});
|
|
661
|
+
await new Promise(() => {});
|
|
662
|
+
return poller;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const s = telegramSettings(cfg);
|
|
666
|
+
const st = inboxState();
|
|
667
|
+
console.log(`enabled : ${s.enabled}`);
|
|
668
|
+
console.log(`bot : ${st.bot?.username ? '@' + st.bot.username : '(unknown)'}`);
|
|
669
|
+
console.log(`chats : ${s.chatIds.join(', ') || '(none)'}`);
|
|
670
|
+
console.log(`polling : ${s.poll}`);
|
|
671
|
+
console.log(`messages : ${st.total} (${st.unread} unread)`);
|
|
672
|
+
if (st.lastError) console.log(`last error: ${st.lastError}`);
|
|
673
|
+
if (!s.enabled) {
|
|
674
|
+
console.log('\nRun: arena-bridge telegram setup --token <bot token> --chat-id <your id>');
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
586
678
|
function cmdDoctor() {
|
|
587
679
|
const cfg = loadConfig();
|
|
588
680
|
const checks = [];
|
|
@@ -638,6 +730,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
638
730
|
return cmdApprove(args);
|
|
639
731
|
case 'audit':
|
|
640
732
|
return cmdAudit(args);
|
|
733
|
+
case 'telegram':
|
|
734
|
+
return cmdTelegram(args);
|
|
641
735
|
case 'rotate':
|
|
642
736
|
return cmdRotate(args);
|
|
643
737
|
case 'trust':
|
package/src/config.mjs
CHANGED
|
@@ -48,6 +48,14 @@ export const DEFAULTS = {
|
|
|
48
48
|
maxUploadBytes: 16 * 1024 * 1024,
|
|
49
49
|
commandTimeoutSec: 120,
|
|
50
50
|
audit: true,
|
|
51
|
+
telegram: {
|
|
52
|
+
enabled: false, // notify the user in Telegram
|
|
53
|
+
botToken: null, // bot token from @BotFather
|
|
54
|
+
chatIds: [], // chats the bridge may write to
|
|
55
|
+
poll: true, // listen for incoming messages and approval commands
|
|
56
|
+
apiId: null, // optional MTProto app id (not used by the bot)
|
|
57
|
+
apiHash: null, // optional MTProto app hash (not used by the bot)
|
|
58
|
+
},
|
|
51
59
|
};
|
|
52
60
|
|
|
53
61
|
export function expandHome(p) {
|
package/src/direct.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { listApprovals, decide } from './approvals.mjs';
|
|
|
7
7
|
import { checkPath } from './policy.mjs';
|
|
8
8
|
import { loadConfig } from './config.mjs';
|
|
9
9
|
import { tailAudit, readArtifact, auditDir } from './audit.mjs';
|
|
10
|
+
import { startPoller, parseCommand, sendTelegram } from './telegram.mjs';
|
|
10
11
|
|
|
11
12
|
export function createDirectServer(cfg, engine) {
|
|
12
13
|
const mcpCtx = {
|
|
@@ -15,6 +16,27 @@ export function createDirectServer(cfg, engine) {
|
|
|
15
16
|
callTool: (name, args) => engine.call(name, args),
|
|
16
17
|
};
|
|
17
18
|
|
|
19
|
+
const handleTelegramMessage = async (item) => {
|
|
20
|
+
const cmd = parseCommand(item.text);
|
|
21
|
+
if (!cmd) return;
|
|
22
|
+
const current = loadConfig();
|
|
23
|
+
const say = (text) => sendTelegram(current, { text, chatId: item.chat_id });
|
|
24
|
+
if ((cmd.cmd === 'approve' || cmd.cmd === 'deny') && cmd.args) {
|
|
25
|
+
const res = decide(cmd.args, cmd.cmd === 'approve' ? 'approve' : 'deny', 'telegram');
|
|
26
|
+
await say(res.ok ? `${cmd.cmd === 'approve' ? 'Approved' : 'Denied'} ${cmd.args}` : `Cannot ${cmd.cmd}: ${res.error}`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (cmd.cmd === 'status') {
|
|
30
|
+
const pending = listApprovals().filter((a) => a.status === 'pending').length;
|
|
31
|
+
await say(`arena-bridge on ${cfg.device}\nPending approvals: ${pending}`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (cmd.cmd === 'help') {
|
|
35
|
+
await say('Commands: /approve <id>, /deny <id>, /status');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
18
40
|
const server = http.createServer(async (req, res) => {
|
|
19
41
|
let url;
|
|
20
42
|
try {
|
|
@@ -30,7 +52,7 @@ export function createDirectServer(cfg, engine) {
|
|
|
30
52
|
ok: true,
|
|
31
53
|
mode: 'direct',
|
|
32
54
|
device: cfg.device,
|
|
33
|
-
version: '0.
|
|
55
|
+
version: '0.7.1',
|
|
34
56
|
uptime_sec: Math.round(process.uptime()),
|
|
35
57
|
});
|
|
36
58
|
}
|
|
@@ -60,6 +82,10 @@ export function createDirectServer(cfg, engine) {
|
|
|
60
82
|
allowShell: cfg.allowShell,
|
|
61
83
|
approvalWaitSec: cfg.approvalWaitSec,
|
|
62
84
|
maxOutputBytes: cfg.maxOutputBytes,
|
|
85
|
+
telegram: {
|
|
86
|
+
enabled: !!cfg.telegram?.enabled,
|
|
87
|
+
chat_ids: cfg.telegram?.chatIds || [],
|
|
88
|
+
},
|
|
63
89
|
});
|
|
64
90
|
}
|
|
65
91
|
|
|
@@ -152,5 +178,8 @@ export function createDirectServer(cfg, engine) {
|
|
|
152
178
|
}
|
|
153
179
|
});
|
|
154
180
|
|
|
181
|
+
const poller = startPoller(cfg, { onMessage: handleTelegramMessage });
|
|
182
|
+
server.on('close', () => poller?.stop());
|
|
183
|
+
|
|
155
184
|
return server;
|
|
156
185
|
}
|
package/src/engine.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { loadConfig, CONFIG_PATH } from './config.mjs';
|
|
|
10
10
|
import { BrowserManager } from './browser.mjs';
|
|
11
11
|
import { macTools } from './mac.mjs';
|
|
12
12
|
import { trustState, trustAllows } from './trust.mjs';
|
|
13
|
+
import { telegramTools } from './telegram.mjs';
|
|
13
14
|
|
|
14
15
|
/* ------------------------------------------------------------------ *
|
|
15
16
|
* helpers
|
|
@@ -630,7 +631,7 @@ export class Engine {
|
|
|
630
631
|
}
|
|
631
632
|
|
|
632
633
|
catalog() {
|
|
633
|
-
return [...toolCatalog(), ...this.browser.tools(), ...macTools(this.cfg)];
|
|
634
|
+
return [...toolCatalog(), ...this.browser.tools(), ...macTools(this.cfg), ...telegramTools(this.cfg)];
|
|
634
635
|
}
|
|
635
636
|
|
|
636
637
|
async call(name, args = {}, { requireApproval } = {}) {
|
package/src/hub.mjs
CHANGED
package/src/mcp.mjs
CHANGED
|
@@ -48,7 +48,7 @@ export async function handleMcp(req, res, ctx) {
|
|
|
48
48
|
mcpResponse(id, {
|
|
49
49
|
protocolVersion: PROTOCOL_VERSION,
|
|
50
50
|
capabilities: { tools: { listChanged: false } },
|
|
51
|
-
serverInfo: { name: 'arena-bridge', version: '0.
|
|
51
|
+
serverInfo: { name: 'arena-bridge', version: '0.7.1' },
|
|
52
52
|
instructions:
|
|
53
53
|
'Tools operate the user\'s own machine. Dangerous operations may return ' +
|
|
54
54
|
'PROTECTED and require a local human approval.',
|
package/src/prompt.mjs
CHANGED
|
@@ -65,6 +65,7 @@ ${tools.join('\n')}
|
|
|
65
65
|
| wait for a control to appear or vanish | \`mac_wait\` |
|
|
66
66
|
| work on a separate virtual desktop | \`mac_spaces\`, \`mac_space_switch\` |
|
|
67
67
|
| look at files | \`list_dir\` (top level only; add \`{"recursive":true}\` to go deeper), \`read_file\`, \`search_files\` |
|
|
68
|
+
| tell the user something or ask a question | \`notify_user\` (Telegram), \`telegram_inbox\` to read the answer |
|
|
68
69
|
|
|
69
70
|
## Browser control (Chrome over CDP)
|
|
70
71
|
1. \`chrome_launch\` starts a browser that belongs to you — own profile, no personal data.
|
|
@@ -104,6 +105,15 @@ the accessibility trees that were read are stored next to the log.
|
|
|
104
105
|
- \`audit_artifact\` — fetch a saved artifact: a screenshot (base64 PNG) or an accessibility tree (JSON).
|
|
105
106
|
Use them when you need to look back at what was on the screen or what the interface contained.
|
|
106
107
|
|
|
108
|
+
## Talking to the user (Telegram)
|
|
109
|
+
When Telegram is configured for this bridge, you can reach the user outside this chat:
|
|
110
|
+
- \`notify_user\` — send a message (progress, a question, a finished result).
|
|
111
|
+
Add \`{"wait_for_reply": true}\` to keep the call open until the user answers in Telegram.
|
|
112
|
+
- \`telegram_inbox\` — read the messages the user sent in Telegram (answers, requests, corrections).
|
|
113
|
+
- \`telegram_status\` — the bot, the allowed chats, the number of unread messages.
|
|
114
|
+
- The user can settle a pending approval from Telegram: \`/approve <approval_id>\` or \`/deny <approval_id>\`.
|
|
115
|
+
Use it when a step takes long or when you need a decision: one message is enough, no need to repeat it.
|
|
116
|
+
|
|
107
117
|
## Approvals
|
|
108
118
|
${trustLine}
|
|
109
119
|
- When a call answers \`APPROVAL_PENDING\` it carries an \`approval_id\`: tell the user to run
|
package/src/telegram.mjs
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { CONFIG_DIR, ensureDirs, loadConfig } from './config.mjs';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_BASE = 'https://api.telegram.org';
|
|
6
|
+
const MAX_TEXT = 4000;
|
|
7
|
+
const RATE_LIMIT = 20;
|
|
8
|
+
const RATE_WINDOW_MS = 60_000;
|
|
9
|
+
|
|
10
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
11
|
+
|
|
12
|
+
/* ------------------------------------------------------------------ *
|
|
13
|
+
* settings
|
|
14
|
+
* ------------------------------------------------------------------ */
|
|
15
|
+
|
|
16
|
+
export function telegramSettings(cfg = {}) {
|
|
17
|
+
const t = cfg.telegram || {};
|
|
18
|
+
const baseUrl = String(t.baseUrl || process.env.TELEGRAM_API_BASE || DEFAULT_BASE).replace(/\/+$/, '');
|
|
19
|
+
return {
|
|
20
|
+
enabled: !!t.enabled && !!t.botToken,
|
|
21
|
+
botToken: t.botToken || null,
|
|
22
|
+
chatIds: (t.chatIds || []).map((v) => String(v)),
|
|
23
|
+
baseUrl,
|
|
24
|
+
poll: t.poll !== false,
|
|
25
|
+
apiId: t.apiId || null,
|
|
26
|
+
apiHash: t.apiHash || null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function telegramDir() {
|
|
31
|
+
return path.join(CONFIG_DIR, 'telegram');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function inboxPath() {
|
|
35
|
+
return path.join(telegramDir(), 'inbox.jsonl');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function statePath() {
|
|
39
|
+
return path.join(telegramDir(), 'state.json');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ensureTelegramDir() {
|
|
43
|
+
ensureDirs();
|
|
44
|
+
fs.mkdirSync(telegramDir(), { recursive: true, mode: 0o700 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readState() {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(fs.readFileSync(statePath(), 'utf8'));
|
|
50
|
+
} catch {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeState(state) {
|
|
56
|
+
ensureTelegramDir();
|
|
57
|
+
fs.writeFileSync(statePath(), JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ------------------------------------------------------------------ *
|
|
61
|
+
* bot api
|
|
62
|
+
* ------------------------------------------------------------------ */
|
|
63
|
+
|
|
64
|
+
export async function apiCall(cfg, method, payload = {}) {
|
|
65
|
+
const s = telegramSettings(cfg);
|
|
66
|
+
if (!s.botToken) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
error: 'telegram_not_configured',
|
|
70
|
+
reason: 'no bot token in the configuration',
|
|
71
|
+
hint: 'Run: arena-bridge telegram setup --token <bot token> --chat-id <your id>',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
let res;
|
|
75
|
+
let data = null;
|
|
76
|
+
try {
|
|
77
|
+
res = await fetch(`${s.baseUrl}/bot${s.botToken}/${method}`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: { 'Content-Type': 'application/json' },
|
|
80
|
+
body: JSON.stringify(payload),
|
|
81
|
+
signal: AbortSignal.timeout(90_000),
|
|
82
|
+
});
|
|
83
|
+
data = await res.json().catch(() => null);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
return { ok: false, error: 'telegram_unreachable', reason: e.message };
|
|
86
|
+
}
|
|
87
|
+
if (!res.ok || !data || data.ok !== true) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
error: 'telegram_api_error',
|
|
91
|
+
reason: data?.description || res.statusText || 'unknown error',
|
|
92
|
+
code: data?.error_code,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return { ok: true, result: data.result };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function allowedChat(cfg, chatId) {
|
|
99
|
+
const s = telegramSettings(cfg);
|
|
100
|
+
return s.chatIds.includes(String(chatId));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const sentAt = [];
|
|
104
|
+
|
|
105
|
+
export function rateOk(limit = RATE_LIMIT, windowMs = RATE_WINDOW_MS) {
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
while (sentAt.length && now - sentAt[0] > windowMs) sentAt.shift();
|
|
108
|
+
if (sentAt.length >= limit) return false;
|
|
109
|
+
sentAt.push(now);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Send a text message to one of the allowed chats.
|
|
115
|
+
*/
|
|
116
|
+
export async function sendTelegram(cfg, { text, chatId, title, silent } = {}) {
|
|
117
|
+
const s = telegramSettings(cfg);
|
|
118
|
+
if (!s.enabled) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
error: 'telegram_not_configured',
|
|
122
|
+
reason: 'Telegram is not configured for this bridge',
|
|
123
|
+
hint: 'Run: arena-bridge telegram setup --token <bot token> --chat-id <your id>',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const target = String(chatId ?? s.chatIds[0] ?? '');
|
|
127
|
+
if (!target) {
|
|
128
|
+
return { ok: false, error: 'telegram_no_chat', reason: 'no chat id configured' };
|
|
129
|
+
}
|
|
130
|
+
if (!allowedChat(cfg, target)) {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
error: 'telegram_chat_not_allowed',
|
|
134
|
+
reason: `chat ${target} is not on the allow list`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const body = String(text ?? '').trim();
|
|
138
|
+
if (!body) return { ok: false, error: 'telegram_empty_message', reason: 'the message is empty' };
|
|
139
|
+
if (body.length > MAX_TEXT) {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
error: 'telegram_message_too_long',
|
|
143
|
+
reason: `the message is ${body.length} characters, the limit is ${MAX_TEXT}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (!rateOk()) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
error: 'telegram_rate_limited',
|
|
150
|
+
reason: `more than ${RATE_LIMIT} messages per minute`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const text_ = title ? `${title}\n${body}` : body;
|
|
154
|
+
let res = await apiCall(cfg, 'sendMessage', {
|
|
155
|
+
chat_id: target,
|
|
156
|
+
text: text_,
|
|
157
|
+
disable_notification: !!silent,
|
|
158
|
+
});
|
|
159
|
+
if (!res.ok && /parse entities|Markdown/i.test(res.reason || '')) {
|
|
160
|
+
res = await apiCall(cfg, 'sendMessage', {
|
|
161
|
+
chat_id: target,
|
|
162
|
+
text: text_,
|
|
163
|
+
disable_notification: !!silent,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (!res.ok) return res;
|
|
167
|
+
return {
|
|
168
|
+
ok: true,
|
|
169
|
+
chat_id: res.result?.chat?.id ?? target,
|
|
170
|
+
message_id: res.result?.message_id ?? null,
|
|
171
|
+
sent_at: new Date().toISOString(),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ------------------------------------------------------------------ *
|
|
176
|
+
* inbox
|
|
177
|
+
* ------------------------------------------------------------------ */
|
|
178
|
+
|
|
179
|
+
export function appendInbox(item) {
|
|
180
|
+
ensureTelegramDir();
|
|
181
|
+
fs.appendFileSync(inboxPath(), JSON.stringify(item) + '\n', { mode: 0o600 });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function readInbox({ limit = 20 } = {}) {
|
|
185
|
+
let raw = '';
|
|
186
|
+
try {
|
|
187
|
+
raw = fs.readFileSync(inboxPath(), 'utf8');
|
|
188
|
+
} catch {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
const read = new Set(readState().read || []);
|
|
192
|
+
const items = raw
|
|
193
|
+
.split('\n')
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
.map((line) => {
|
|
196
|
+
try {
|
|
197
|
+
return JSON.parse(line);
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
.filter(Boolean);
|
|
203
|
+
return items.slice(-Math.max(1, limit)).map((i) => ({ ...i, read: read.has(String(i.id)) }));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function markInboxRead(ids) {
|
|
207
|
+
const state = readState();
|
|
208
|
+
const read = new Set(state.read || []);
|
|
209
|
+
for (const id of ids || []) read.add(String(id));
|
|
210
|
+
state.read = [...read].slice(-500);
|
|
211
|
+
writeState(state);
|
|
212
|
+
return state.read.length;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function inboxState() {
|
|
216
|
+
const state = readState();
|
|
217
|
+
let total = 0;
|
|
218
|
+
try {
|
|
219
|
+
total = fs.readFileSync(inboxPath(), 'utf8').split('\n').filter(Boolean).length;
|
|
220
|
+
} catch {
|
|
221
|
+
total = 0;
|
|
222
|
+
}
|
|
223
|
+
const read = new Set(state.read || []);
|
|
224
|
+
return {
|
|
225
|
+
total,
|
|
226
|
+
unread: Math.max(0, total - read.size),
|
|
227
|
+
bot: state.bot || null,
|
|
228
|
+
offset: state.offset || 0,
|
|
229
|
+
lastError: state.lastError || null,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function saveBotInfo(info) {
|
|
234
|
+
const state = readState();
|
|
235
|
+
state.bot = info;
|
|
236
|
+
writeState(state);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* ------------------------------------------------------------------ *
|
|
240
|
+
* commands
|
|
241
|
+
* ------------------------------------------------------------------ */
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Parse a chat command like "/approve 1234" into { cmd, args }.
|
|
245
|
+
*/
|
|
246
|
+
export function parseCommand(text) {
|
|
247
|
+
const raw = String(text ?? '').trim();
|
|
248
|
+
if (!raw.startsWith('/')) return null;
|
|
249
|
+
const [cmd, ...rest] = raw.slice(1).split(/\s+/);
|
|
250
|
+
return { cmd: cmd.toLowerCase(), args: rest.join(' ').trim() };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* ------------------------------------------------------------------ *
|
|
254
|
+
* poller
|
|
255
|
+
* ------------------------------------------------------------------ */
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Poll the bot for incoming messages and hand them to the callback.
|
|
259
|
+
* Returns a handle with stop().
|
|
260
|
+
*/
|
|
261
|
+
export function startPoller(cfg, { onMessage } = {}) {
|
|
262
|
+
const s = telegramSettings(cfg);
|
|
263
|
+
if (!s.enabled || !s.poll) return null;
|
|
264
|
+
let stopped = false;
|
|
265
|
+
let offset = readState().offset || 0;
|
|
266
|
+
|
|
267
|
+
const fresh = () => {
|
|
268
|
+
try {
|
|
269
|
+
return loadConfig();
|
|
270
|
+
} catch {
|
|
271
|
+
return cfg;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const loop = async () => {
|
|
276
|
+
if (!readState().bot) {
|
|
277
|
+
const me = await apiCall(fresh(), 'getMe');
|
|
278
|
+
if (me.ok) saveBotInfo(me.result);
|
|
279
|
+
}
|
|
280
|
+
while (!stopped) {
|
|
281
|
+
try {
|
|
282
|
+
const res = await apiCall(fresh(), 'getUpdates', {
|
|
283
|
+
offset,
|
|
284
|
+
timeout: 20,
|
|
285
|
+
allowed_updates: ['message'],
|
|
286
|
+
});
|
|
287
|
+
if (!res.ok) {
|
|
288
|
+
const state = readState();
|
|
289
|
+
state.lastError = res.reason || res.error;
|
|
290
|
+
writeState(state);
|
|
291
|
+
await sleep(res.code === 409 ? 15000 : 5000);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const updates = res.result || [];
|
|
295
|
+
if (!updates.length) {
|
|
296
|
+
await sleep(400);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
for (const upd of updates) {
|
|
300
|
+
offset = upd.update_id + 1;
|
|
301
|
+
const state = readState();
|
|
302
|
+
state.offset = offset;
|
|
303
|
+
writeState(state);
|
|
304
|
+
const msg = upd.message;
|
|
305
|
+
if (!msg) continue;
|
|
306
|
+
const item = {
|
|
307
|
+
id: upd.update_id,
|
|
308
|
+
chat_id: msg.chat?.id ?? null,
|
|
309
|
+
from: msg.from?.username || msg.from?.first_name || 'unknown',
|
|
310
|
+
text: msg.text || '',
|
|
311
|
+
date: msg.date || null,
|
|
312
|
+
read: false,
|
|
313
|
+
};
|
|
314
|
+
appendInbox(item);
|
|
315
|
+
if (onMessage) {
|
|
316
|
+
try {
|
|
317
|
+
await onMessage(item);
|
|
318
|
+
} catch {
|
|
319
|
+
/* the callback must not kill the loop */
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
await sleep(5000);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
loop();
|
|
330
|
+
return {
|
|
331
|
+
stop() {
|
|
332
|
+
stopped = true;
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* ------------------------------------------------------------------ *
|
|
338
|
+
* tools
|
|
339
|
+
* ------------------------------------------------------------------ */
|
|
340
|
+
|
|
341
|
+
const S = (o) => ({ type: 'object', properties: o, required: [], additionalProperties: false });
|
|
342
|
+
|
|
343
|
+
async function waitForReply(cfg, sinceId, timeoutSec) {
|
|
344
|
+
const deadline = Date.now() + Math.max(1, timeoutSec) * 1000;
|
|
345
|
+
while (Date.now() < deadline) {
|
|
346
|
+
const items = readInbox({ limit: 20 });
|
|
347
|
+
const fresh = items.find((i) => Number(i.id) > sinceId);
|
|
348
|
+
if (fresh) return fresh;
|
|
349
|
+
await sleep(500);
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function telegramTools(cfg = {}) {
|
|
355
|
+
return [
|
|
356
|
+
{
|
|
357
|
+
name: 'notify_user',
|
|
358
|
+
description:
|
|
359
|
+
'Send a message to the user in Telegram. Use it to report progress, ask a question, or hand over ' +
|
|
360
|
+
'a result without waiting for the chat. Set wait_for_reply to keep the call open until the user ' +
|
|
361
|
+
'answers in Telegram.',
|
|
362
|
+
approval: false,
|
|
363
|
+
schema: S({
|
|
364
|
+
text: { type: 'string', description: 'the message text (up to 4000 characters)' },
|
|
365
|
+
title: { type: 'string', description: 'short first line, e.g. "Готово" or "Нужен ответ"' },
|
|
366
|
+
chat_id: { type: 'string', description: 'target chat id (default: the configured one)' },
|
|
367
|
+
silent: { type: 'boolean', description: 'send without a sound' },
|
|
368
|
+
wait_for_reply: { type: 'boolean', description: 'wait for the user to answer in Telegram' },
|
|
369
|
+
timeout_sec: { type: 'number', description: 'how long to wait for a reply (default 120, max 600)' },
|
|
370
|
+
}),
|
|
371
|
+
required: ['text'],
|
|
372
|
+
handler: async (args, ctx) => {
|
|
373
|
+
const c = ctx?.cfg || cfg;
|
|
374
|
+
const sent = await sendTelegram(c, {
|
|
375
|
+
text: args.text,
|
|
376
|
+
chatId: args.chat_id,
|
|
377
|
+
title: args.title,
|
|
378
|
+
silent: args.silent,
|
|
379
|
+
});
|
|
380
|
+
if (!sent.ok) return sent;
|
|
381
|
+
if (!args.wait_for_reply) return sent;
|
|
382
|
+
const since = Math.max(0, ...readInbox({ limit: 50 }).map((i) => Number(i.id) || 0));
|
|
383
|
+
const reply = await waitForReply(c, since, Math.min(600, Number(args.timeout_sec) || 120));
|
|
384
|
+
if (!reply) {
|
|
385
|
+
return {
|
|
386
|
+
...sent,
|
|
387
|
+
reply: null,
|
|
388
|
+
timed_out: true,
|
|
389
|
+
hint: 'The user did not answer in Telegram. Continue without the answer or ask again later.',
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
markInboxRead([reply.id]);
|
|
393
|
+
return { ...sent, reply: { from: reply.from, text: reply.text, at: reply.date } };
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
name: 'telegram_inbox',
|
|
398
|
+
description:
|
|
399
|
+
'Read the messages the user sent in Telegram. Use it to pick up an answer, a file name or a ' +
|
|
400
|
+
'command given outside the agent chat.',
|
|
401
|
+
approval: false,
|
|
402
|
+
schema: S({
|
|
403
|
+
limit: { type: 'number', description: 'how many messages to return (default 20)' },
|
|
404
|
+
mark_read: { type: 'boolean', description: 'mark the returned messages as read (default true)' },
|
|
405
|
+
}),
|
|
406
|
+
required: [],
|
|
407
|
+
handler: async (args, ctx) => {
|
|
408
|
+
const c = ctx?.cfg || cfg;
|
|
409
|
+
const limit = Math.min(100, Number(args.limit) || 20);
|
|
410
|
+
const items = readInbox({ limit });
|
|
411
|
+
if (args.mark_read !== false && items.length) markInboxRead(items.map((i) => i.id));
|
|
412
|
+
return { ok: true, count: items.length, messages: items };
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
name: 'telegram_status',
|
|
417
|
+
description: 'Show the Telegram link: the bot, the allowed chats and how many messages are unread.',
|
|
418
|
+
approval: false,
|
|
419
|
+
schema: S({}),
|
|
420
|
+
required: [],
|
|
421
|
+
handler: async (args, ctx) => {
|
|
422
|
+
const c = ctx?.cfg || cfg;
|
|
423
|
+
const s = telegramSettings(c);
|
|
424
|
+
if (!s.enabled) {
|
|
425
|
+
return {
|
|
426
|
+
ok: false,
|
|
427
|
+
error: 'telegram_not_configured',
|
|
428
|
+
reason: 'Telegram is not configured for this bridge',
|
|
429
|
+
hint: 'Run: arena-bridge telegram setup --token <bot token> --chat-id <your id>',
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
const st = inboxState();
|
|
433
|
+
return {
|
|
434
|
+
ok: true,
|
|
435
|
+
bot: st.bot?.username ? `@${st.bot.username}` : null,
|
|
436
|
+
bot_name: st.bot?.first_name || null,
|
|
437
|
+
chat_ids: s.chatIds,
|
|
438
|
+
total_messages: st.total,
|
|
439
|
+
unread: st.unread,
|
|
440
|
+
polling: s.poll,
|
|
441
|
+
last_error: st.lastError,
|
|
442
|
+
};
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
];
|
|
446
|
+
}
|