@deadragdoll/tellymcp 0.0.4 → 0.0.6
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-ru.md +63 -6
- package/README.md +63 -40
- package/VERSION.md +22 -0
- package/dist/services/features/telegram-mcp/gateway-socket.service.js +13 -2
- package/dist/services/features/telegram-mcp/src/app/http.js +18 -2
- package/dist/services/features/telegram-mcp/src/app/webapp/assets.js +99 -7
- package/dist/services/features/telegram-mcp/src/app/webapp/tmux.js +2 -1
- package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +1 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/tmux/client.js +8 -2
- package/package.json +1 -1
package/README-ru.md
CHANGED
|
@@ -7,16 +7,73 @@
|
|
|
7
7
|
[](https://nodejs.org/)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
10
|
-
TellyMCP — это Telegram
|
|
10
|
+
TellyMCP — это self-hosted Telegram control plane для coding agents.
|
|
11
11
|
|
|
12
|
-
Он
|
|
12
|
+
Он привязывает реальные agent-сессии к Telegram, делает их доступными с телефона и даёт им работать вместе между локальными и удалёнными машинами.
|
|
13
|
+
|
|
14
|
+
Он не завязан на одного вендора или один coding assistant. Если агент умеет работать с MCP server, он может использовать TellyMCP.
|
|
15
|
+
|
|
16
|
+
## Зачем он нужен
|
|
17
|
+
|
|
18
|
+
Coding agents полезны ровно до того момента, пока они не остаются одни в терминале:
|
|
19
|
+
|
|
20
|
+
- им нужно уточнение, пока тебя нет за компьютером
|
|
21
|
+
- им нужен approval перед рискованным действием
|
|
22
|
+
- им нужно передать скриншот, файл или note между сессиями
|
|
23
|
+
- им нужно быстро подключить человека или другого агента, не ломая общий workflow
|
|
24
|
+
|
|
25
|
+
TellyMCP даёт каждой сессии мобильную панель управления и collaboration layer:
|
|
26
|
+
|
|
27
|
+
- `Live` tmux view и лёгкое управление из Telegram
|
|
28
|
+
- session-scoped inbox и уведомления
|
|
29
|
+
- workspace-aware handoff для файлов и note
|
|
30
|
+
- локальную и удалённую коллаборацию между сессиями
|
|
31
|
+
- поддержку mixed agent setups, если они умеют говорить по MCP
|
|
32
|
+
|
|
33
|
+
## Ключевые идеи продукта
|
|
34
|
+
|
|
35
|
+
- `Live` tmux view и управление внутри Telegram Mini App
|
|
36
|
+
- `Collab`-сценарии для локальных и удалённых agent-сессий
|
|
37
|
+
- `.mcp-xchange` как workspace-level handoff шина для note, файлов и скриншотов
|
|
38
|
+
- MCP-native pairing сессий и session-scoped tools
|
|
39
|
+
- optional gateway mode для multi-machine и multi-bot проектов
|
|
40
|
+
|
|
41
|
+
## Human-in-the-loop — это только один слой системы
|
|
42
|
+
|
|
43
|
+
Telegram HITL здесь тоже есть, но он не исчерпывает продукт:
|
|
13
44
|
|
|
14
45
|
- задавать человеку уточняющие вопросы через Telegram
|
|
15
46
|
- получать несвязанные входящие сообщения позже через inbox
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
47
|
+
- уведомлять человека о прогрессе, blockers и approvals
|
|
48
|
+
|
|
49
|
+
## Что отличает TellyMCP от простого Telegram bot bridge
|
|
50
|
+
|
|
51
|
+
- он завязан на сессии, а не только на чат
|
|
52
|
+
- он понимает локальные и удалённые collaboration targets
|
|
53
|
+
- у него есть live terminal surface, а не только обмен сообщениями
|
|
54
|
+
- он передаёт файлы через workspace-aware exchange paths, а не просто через ad hoc upload
|
|
55
|
+
- он может работать как standalone node или как gateway-backed control plane
|
|
56
|
+
|
|
57
|
+
## Типовые сценарии
|
|
58
|
+
|
|
59
|
+
- держать долгоживущего агента доступным с телефона
|
|
60
|
+
- запускать рядом разных агентов, если каждый умеет подключаться по MCP
|
|
61
|
+
- подруливать tmux-сессией без ноутбука
|
|
62
|
+
- маршрутизировать работу между `frontend`, `backend`, `review` и другими локальными сессиями
|
|
63
|
+
- работать с удалёнными сессиями через gateway-backed project
|
|
64
|
+
- передавать note, скриншоты и реальные файлы через `.mcp-xchange`
|
|
65
|
+
- проверять локальный веб-интерфейс через `browser_*` tools и отправлять результат обратно в Telegram
|
|
66
|
+
|
|
67
|
+
## Группы инструментов
|
|
68
|
+
|
|
69
|
+
- pairing и session context
|
|
70
|
+
- Telegram ask/notify/inbox
|
|
71
|
+
- `Live` tmux control
|
|
72
|
+
- browser inspection и screenshots
|
|
73
|
+
- partner notes и partner files
|
|
74
|
+
- tools sync и version checks
|
|
75
|
+
|
|
76
|
+
Полный список MCP tools лучше держать ниже по README и в самом MCP server, а не на первом экране.
|
|
20
77
|
|
|
21
78
|
## Prerequisites
|
|
22
79
|
|
package/README.md
CHANGED
|
@@ -7,50 +7,73 @@
|
|
|
7
7
|
[](https://nodejs.org/)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
10
|
-
TellyMCP is a Telegram
|
|
10
|
+
TellyMCP is a self-hosted Telegram control plane for coding agents.
|
|
11
11
|
|
|
12
|
-
It lets
|
|
12
|
+
It pairs real agent sessions with Telegram, keeps them reachable from mobile, and lets them collaborate across local and remote machines.
|
|
13
|
+
|
|
14
|
+
It is not tied to one vendor or one coding assistant. If your agent can talk to an MCP server, it can use TellyMCP.
|
|
15
|
+
|
|
16
|
+
## Why it exists
|
|
17
|
+
|
|
18
|
+
Coding agents are useful until they leave the terminal:
|
|
19
|
+
|
|
20
|
+
- they need clarification while you are away from the desk
|
|
21
|
+
- they need approval before doing something risky
|
|
22
|
+
- they need screenshots, files, or notes passed between sessions
|
|
23
|
+
- they need a human or another agent to unblock work without breaking flow
|
|
24
|
+
|
|
25
|
+
TellyMCP gives each session a mobile control surface and a collaboration layer:
|
|
26
|
+
|
|
27
|
+
- `Live` tmux view and light control from Telegram
|
|
28
|
+
- session-scoped inbox and notifications
|
|
29
|
+
- workspace-aware file and note handoffs
|
|
30
|
+
- local and remote session collaboration
|
|
31
|
+
- support for mixed agent setups, as long as they speak MCP
|
|
32
|
+
|
|
33
|
+
## Core ideas
|
|
34
|
+
|
|
35
|
+
- `Live` tmux view and control inside Telegram Mini App
|
|
36
|
+
- `Collab` flows for local and remote agent sessions
|
|
37
|
+
- `.mcp-xchange` as a workspace-level handoff bus for notes, files, and screenshots
|
|
38
|
+
- MCP-native session pairing and session-scoped tools
|
|
39
|
+
- optional gateway mode for cross-machine and cross-bot projects
|
|
40
|
+
|
|
41
|
+
## Human-in-the-loop is one layer, not the whole product
|
|
42
|
+
|
|
43
|
+
Telegram HITL is still supported, but it is not the whole story:
|
|
13
44
|
|
|
14
45
|
- ask a human for clarification through Telegram
|
|
15
46
|
- receive unsolicited Telegram messages later through an inbox
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
- `
|
|
32
|
-
-
|
|
33
|
-
- `
|
|
34
|
-
- `
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
- `
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
- `browser_clear_logs`
|
|
47
|
-
- `browser_dom`
|
|
48
|
-
- `browser_computed_style`
|
|
49
|
-
- `browser_screenshot`
|
|
50
|
-
- `browser_close`
|
|
51
|
-
- `refresh_tools_markdown`
|
|
52
|
-
- `send_partner_note`
|
|
53
|
-
- `send_partner_file`
|
|
47
|
+
- notify a human about progress, blockers, and approvals
|
|
48
|
+
|
|
49
|
+
## What makes it different from a simple Telegram bot bridge
|
|
50
|
+
|
|
51
|
+
- it is session-based, not just chat-based
|
|
52
|
+
- it understands local and remote collaboration targets
|
|
53
|
+
- it has a live terminal surface, not only message exchange
|
|
54
|
+
- it moves files through workspace-aware exchange paths, not just ad hoc uploads
|
|
55
|
+
- it can run as a standalone node or as a gateway-backed control plane
|
|
56
|
+
|
|
57
|
+
## Typical use cases
|
|
58
|
+
|
|
59
|
+
- keep a long-running agent reachable from your phone
|
|
60
|
+
- run different agents side by side, as long as each one can connect over MCP
|
|
61
|
+
- steer a tmux-based session without opening a laptop
|
|
62
|
+
- route work between `frontend`, `backend`, `review`, or other local sessions
|
|
63
|
+
- collaborate with remote sessions through a gateway-backed project
|
|
64
|
+
- send notes, screenshots, and real files through `.mcp-xchange`
|
|
65
|
+
- inspect or screenshot a local web app with `browser_*` tools and send results back to Telegram
|
|
66
|
+
|
|
67
|
+
## Tool groups
|
|
68
|
+
|
|
69
|
+
- session pairing and context
|
|
70
|
+
- Telegram ask/notify/inbox
|
|
71
|
+
- `Live` tmux control
|
|
72
|
+
- browser inspection and screenshots
|
|
73
|
+
- partner notes and partner files
|
|
74
|
+
- tools sync and version checks
|
|
75
|
+
|
|
76
|
+
The full MCP tool surface is documented later in this README and through the MCP server itself.
|
|
54
77
|
|
|
55
78
|
## Prerequisites
|
|
56
79
|
|
package/VERSION.md
CHANGED
|
@@ -21,6 +21,15 @@ For detailed engineering history, refactors, and internal development notes, see
|
|
|
21
21
|
- [README.md](README.md)
|
|
22
22
|
- [README-ru.md](README-ru.md)
|
|
23
23
|
- Human-readable release notes in this file.
|
|
24
|
+
- Telegram startup notice:
|
|
25
|
+
- version
|
|
26
|
+
- protocol
|
|
27
|
+
- mode
|
|
28
|
+
- paired sessions
|
|
29
|
+
- MCP/WebApp/Gateway endpoints
|
|
30
|
+
- Live text input button:
|
|
31
|
+
- `[txt]`
|
|
32
|
+
- sends literal text to tmux without pressing `Enter`
|
|
24
33
|
|
|
25
34
|
### Changed
|
|
26
35
|
- Default installation path is now npm-first:
|
|
@@ -32,6 +41,7 @@ For detailed engineering history, refactors, and internal development notes, see
|
|
|
32
41
|
- direct terminal control from Telegram
|
|
33
42
|
- Environment examples were split into dedicated client and gateway variants.
|
|
34
43
|
- Package build/publish flow now validates itself before packing/publishing.
|
|
44
|
+
- CLI now shows package version directly in banners and startup output.
|
|
35
45
|
|
|
36
46
|
### Collaboration
|
|
37
47
|
- Project collaboration works across local and remote sessions.
|
|
@@ -55,6 +65,18 @@ For detailed engineering history, refactors, and internal development notes, see
|
|
|
55
65
|
- `Down`
|
|
56
66
|
- `Enter`
|
|
57
67
|
- Live approval flow was added for remote project sessions.
|
|
68
|
+
- Live toolbar now includes:
|
|
69
|
+
- `/`
|
|
70
|
+
- `↑`
|
|
71
|
+
- `↓`
|
|
72
|
+
- `Enter`
|
|
73
|
+
- `⌫`
|
|
74
|
+
- `[txt]`
|
|
75
|
+
- `Tab`
|
|
76
|
+
- `Esc`
|
|
77
|
+
- `Ctrl+C`
|
|
78
|
+
- `Ctrl+C` now asks for confirmation before sending an interrupt to the agent.
|
|
79
|
+
- Mobile toolbar layout now wraps cleanly into two rows instead of collapsing into a centered stack.
|
|
58
80
|
|
|
59
81
|
### Browser
|
|
60
82
|
- Browser tools use Playwright Chromium.
|
|
@@ -554,15 +554,26 @@ const TelegramMcpGatewaySocketService = {
|
|
|
554
554
|
const action = typeof request.payload?.action === "string"
|
|
555
555
|
? request.payload.action
|
|
556
556
|
: "";
|
|
557
|
-
|
|
557
|
+
const text = typeof request.payload?.text === "string"
|
|
558
|
+
? request.payload.text
|
|
559
|
+
: "";
|
|
560
|
+
if (!["up", "down", "enter", "slash", "delete", "tab", "escape", "interrupt", "text"].includes(action)) {
|
|
558
561
|
throw new Error("Unsupported action");
|
|
559
562
|
}
|
|
563
|
+
if (action === "text" && (!text || text.length > 4000)) {
|
|
564
|
+
throw new Error("Text payload is required and must be <= 4000 characters");
|
|
565
|
+
}
|
|
560
566
|
const sessionId = request.local_session_id.trim();
|
|
561
567
|
const session = await runtime.sessionStore.getSession(sessionId);
|
|
562
568
|
if (!session?.tmuxTarget) {
|
|
563
569
|
throw new Error("tmux target is not configured for this session");
|
|
564
570
|
}
|
|
565
|
-
|
|
571
|
+
if (action === "text") {
|
|
572
|
+
await (0, tmux_1.sendTmuxLiteralText)(runtime.config.tmux, session.tmuxTarget, text);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
await (0, tmux_1.sendAllowedTmuxAction)(runtime.config.tmux, session.tmuxTarget, action);
|
|
576
|
+
}
|
|
566
577
|
return {
|
|
567
578
|
type: "live_response",
|
|
568
579
|
request_id: request.request_id,
|
|
@@ -428,10 +428,19 @@ function createMcpHttpHandler(runtime, input) {
|
|
|
428
428
|
typeof Reflect.get(body, "action") === "string"
|
|
429
429
|
? String(Reflect.get(body, "action"))
|
|
430
430
|
: "";
|
|
431
|
-
|
|
431
|
+
const text = body &&
|
|
432
|
+
typeof body === "object" &&
|
|
433
|
+
typeof Reflect.get(body, "text") === "string"
|
|
434
|
+
? String(Reflect.get(body, "text"))
|
|
435
|
+
: "";
|
|
436
|
+
if (!["up", "down", "enter", "slash", "delete", "tab", "escape", "interrupt", "text"].includes(action)) {
|
|
432
437
|
writeText(res, 400, "Unsupported action");
|
|
433
438
|
return;
|
|
434
439
|
}
|
|
440
|
+
if (action === "text" && (!text || text.length > 4000)) {
|
|
441
|
+
writeText(res, 400, "Text payload is required and must be <= 4000 characters");
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
435
444
|
const nowMs = Date.now();
|
|
436
445
|
if (nowMs - webAppSession.lastActionAtMs <
|
|
437
446
|
runtime.config.webapp.actionCooldownMs) {
|
|
@@ -445,6 +454,7 @@ function createMcpHttpHandler(runtime, input) {
|
|
|
445
454
|
clientUuid: relayTarget.clientUuid,
|
|
446
455
|
localSessionId: relayTarget.localSessionId,
|
|
447
456
|
action: action,
|
|
457
|
+
...(action === "text" ? { text } : {}),
|
|
448
458
|
});
|
|
449
459
|
webAppSessions.touchAction(webAppSession.token, nowMs);
|
|
450
460
|
writeJson(res, 200, {
|
|
@@ -477,12 +487,18 @@ function createMcpHttpHandler(runtime, input) {
|
|
|
477
487
|
return;
|
|
478
488
|
}
|
|
479
489
|
try {
|
|
480
|
-
|
|
490
|
+
if (action === "text") {
|
|
491
|
+
await (0, tmux_1.sendTmuxLiteralText)(runtime.config.tmux, session.tmuxTarget, text);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
await (0, tmux_1.sendAllowedTmuxAction)(runtime.config.tmux, session.tmuxTarget, action);
|
|
495
|
+
}
|
|
481
496
|
webAppSessions.touchAction(webAppSession.token, nowMs);
|
|
482
497
|
runtime.logger.info("Telegram WebApp action sent to tmux", {
|
|
483
498
|
sessionId: webAppSession.sessionId,
|
|
484
499
|
telegramUserId: webAppSession.telegramUserId,
|
|
485
500
|
action,
|
|
501
|
+
...(action === "text" ? { textLength: text.length } : {}),
|
|
486
502
|
});
|
|
487
503
|
writeJson(res, 200, {
|
|
488
504
|
ok: true,
|
|
@@ -42,7 +42,7 @@ body {
|
|
|
42
42
|
bottom: calc(42px + env(safe-area-inset-bottom, 0px));
|
|
43
43
|
z-index: 30;
|
|
44
44
|
display: flex;
|
|
45
|
-
justify-content:
|
|
45
|
+
justify-content: flex-start;
|
|
46
46
|
flex-wrap: wrap;
|
|
47
47
|
gap: 8px;
|
|
48
48
|
padding: 10px 14px;
|
|
@@ -52,6 +52,11 @@ body {
|
|
|
52
52
|
backdrop-filter: blur(14px);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
.toolbar-spacer {
|
|
56
|
+
flex: 1 1 auto;
|
|
57
|
+
min-width: 12px;
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
.btn {
|
|
56
61
|
appearance: none;
|
|
57
62
|
border: 1px solid var(--border);
|
|
@@ -73,6 +78,28 @@ body {
|
|
|
73
78
|
.btn:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
74
79
|
.btn.danger:hover { border-color: var(--danger); }
|
|
75
80
|
|
|
81
|
+
.btn.danger {
|
|
82
|
+
border-color: rgba(255, 116, 116, 0.55);
|
|
83
|
+
background: linear-gradient(180deg, rgba(78, 18, 24, 0.96) 0%, rgba(50, 14, 18, 0.98) 100%);
|
|
84
|
+
color: #ffd7d7;
|
|
85
|
+
box-shadow: inset 0 0 0 1px rgba(255, 116, 116, 0.12);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.btn.danger:hover {
|
|
89
|
+
border-color: rgba(255, 116, 116, 0.9);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.btn.primary {
|
|
93
|
+
border-color: rgba(87, 193, 255, 0.45);
|
|
94
|
+
background: linear-gradient(180deg, rgba(17, 45, 66, 0.96) 0%, rgba(12, 33, 48, 0.98) 100%);
|
|
95
|
+
color: #d9f3ff;
|
|
96
|
+
box-shadow: inset 0 0 0 1px rgba(87, 193, 255, 0.1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.btn.primary:hover {
|
|
100
|
+
border-color: rgba(87, 193, 255, 0.8);
|
|
101
|
+
}
|
|
102
|
+
|
|
76
103
|
.statusbar {
|
|
77
104
|
position: fixed;
|
|
78
105
|
left: 0;
|
|
@@ -138,6 +165,11 @@ body {
|
|
|
138
165
|
padding: 8px 10px;
|
|
139
166
|
}
|
|
140
167
|
|
|
168
|
+
.toolbar-spacer {
|
|
169
|
+
flex: 1 1 auto;
|
|
170
|
+
min-width: 12px;
|
|
171
|
+
}
|
|
172
|
+
|
|
141
173
|
.btn.compact {
|
|
142
174
|
min-width: 42px;
|
|
143
175
|
padding: 8px 10px;
|
|
@@ -169,6 +201,7 @@ const elements = {
|
|
|
169
201
|
status: document.querySelector("[data-role=status]"),
|
|
170
202
|
updated: document.querySelector("[data-role=updated]"),
|
|
171
203
|
interrupt: document.querySelector("[data-role=interrupt]"),
|
|
204
|
+
type: document.querySelector("[data-role=type]"),
|
|
172
205
|
esc: document.querySelector("[data-role=escape]"),
|
|
173
206
|
tab: document.querySelector("[data-role=tab]"),
|
|
174
207
|
slash: document.querySelector("[data-role=slash]"),
|
|
@@ -571,6 +604,47 @@ async function sendAction(action) {
|
|
|
571
604
|
}
|
|
572
605
|
}
|
|
573
606
|
|
|
607
|
+
async function sendTextInput(text) {
|
|
608
|
+
if (state.actionBusy || !state.token) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
state.actionBusy = true;
|
|
613
|
+
try {
|
|
614
|
+
const response = await fetch(config.basePath + "/api/action", {
|
|
615
|
+
method: "POST",
|
|
616
|
+
headers: {
|
|
617
|
+
"content-type": "application/json",
|
|
618
|
+
authorization: "Bearer " + state.token,
|
|
619
|
+
},
|
|
620
|
+
body: JSON.stringify({ action: "text", text }),
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
if (!response.ok) {
|
|
624
|
+
const text = await response.text();
|
|
625
|
+
throw new Error(text || "Failed to send text.");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
setStatus("Text sent");
|
|
629
|
+
await refreshVisibleBuffer();
|
|
630
|
+
} finally {
|
|
631
|
+
state.actionBusy = false;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function confirmInterrupt() {
|
|
636
|
+
return new Promise((resolve) => {
|
|
637
|
+
if (tg && typeof tg.showConfirm === "function") {
|
|
638
|
+
tg.showConfirm("Send Ctrl+C to the tmux session? This can stop the running agent.", (ok) => {
|
|
639
|
+
resolve(Boolean(ok));
|
|
640
|
+
});
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
resolve(window.confirm("Send Ctrl+C to the tmux session? This can stop the running agent."));
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
574
648
|
async function refreshVisibleBuffer() {
|
|
575
649
|
const payload = await fetchVisibleBuffer();
|
|
576
650
|
elements.terminal.innerHTML = renderAnsiToHtml(payload.content || "");
|
|
@@ -599,7 +673,22 @@ function startPolling() {
|
|
|
599
673
|
|
|
600
674
|
function bindUi() {
|
|
601
675
|
elements.interrupt.addEventListener("click", () => {
|
|
602
|
-
|
|
676
|
+
confirmInterrupt()
|
|
677
|
+
.then((ok) => {
|
|
678
|
+
if (!ok) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
return sendAction("interrupt");
|
|
682
|
+
})
|
|
683
|
+
.catch((error) => setStatus(error.message || String(error), true));
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
elements.type.addEventListener("click", () => {
|
|
687
|
+
const value = window.prompt("Send text to tmux without Enter:", "");
|
|
688
|
+
if (value === null || value.length === 0) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
sendTextInput(value).catch((error) => setStatus(error.message || String(error), true));
|
|
603
692
|
});
|
|
604
693
|
|
|
605
694
|
elements.esc.addEventListener("click", () => {
|
|
@@ -670,6 +759,7 @@ async function main() {
|
|
|
670
759
|
|
|
671
760
|
if (!bootstrapPayload.tmux_target) {
|
|
672
761
|
elements.interrupt.disabled = true;
|
|
762
|
+
elements.type.disabled = true;
|
|
673
763
|
elements.esc.disabled = true;
|
|
674
764
|
elements.tab.disabled = true;
|
|
675
765
|
elements.slash.disabled = true;
|
|
@@ -710,14 +800,16 @@ function renderWebAppHtml(input) {
|
|
|
710
800
|
<body>
|
|
711
801
|
<div class="app">
|
|
712
802
|
<div class="toolbar">
|
|
713
|
-
<button class="btn compact danger" data-role="interrupt" type="button">Ctrl+C</button>
|
|
714
|
-
<button class="btn compact" data-role="escape" type="button">Esc</button>
|
|
715
|
-
<button class="btn compact" data-role="tab" type="button">Tab</button>
|
|
716
803
|
<button class="btn compact" data-role="slash" type="button">/</button>
|
|
717
|
-
<button class="btn compact" data-role="delete" type="button">⌫</button>
|
|
718
804
|
<button class="btn compact" data-role="up" type="button">↑</button>
|
|
719
805
|
<button class="btn compact" data-role="down" type="button">↓</button>
|
|
720
|
-
<button class="btn compact" data-role="enter" type="button"
|
|
806
|
+
<button class="btn compact primary" data-role="enter" type="button">Enter</button>
|
|
807
|
+
<button class="btn compact" data-role="delete" type="button">⌫</button>
|
|
808
|
+
<button class="btn compact" data-role="type" type="button" title="Type text">🔤</button>
|
|
809
|
+
<button class="btn compact" data-role="tab" type="button">Tab</button>
|
|
810
|
+
<button class="btn compact" data-role="escape" type="button">Esc</button>
|
|
811
|
+
<span class="toolbar-spacer" aria-hidden="true"></span>
|
|
812
|
+
<button class="btn compact danger" data-role="interrupt" type="button">Ctrl+C</button>
|
|
721
813
|
</div>
|
|
722
814
|
<pre class="terminal" data-role="terminal">Waiting for tmux buffer…</pre>
|
|
723
815
|
<div class="statusbar">
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.sendAllowedTmuxAction = exports.isTmuxUnavailableError = exports.getTmuxWindowHeight = exports.captureVisibleTmuxPane = exports.captureTmuxPaneRange = void 0;
|
|
3
|
+
exports.sendTmuxLiteralText = exports.sendAllowedTmuxAction = exports.isTmuxUnavailableError = exports.getTmuxWindowHeight = exports.captureVisibleTmuxPane = exports.captureTmuxPaneRange = void 0;
|
|
4
4
|
var client_1 = require("../../shared/integrations/tmux/client");
|
|
5
5
|
Object.defineProperty(exports, "captureTmuxPaneRange", { enumerable: true, get: function () { return client_1.captureTmuxPaneRange; } });
|
|
6
6
|
Object.defineProperty(exports, "captureVisibleTmuxPane", { enumerable: true, get: function () { return client_1.captureVisibleTmuxPane; } });
|
|
7
7
|
Object.defineProperty(exports, "getTmuxWindowHeight", { enumerable: true, get: function () { return client_1.getTmuxWindowHeight; } });
|
|
8
8
|
Object.defineProperty(exports, "isTmuxUnavailableError", { enumerable: true, get: function () { return client_1.isTmuxUnavailableError; } });
|
|
9
9
|
Object.defineProperty(exports, "sendAllowedTmuxAction", { enumerable: true, get: function () { return client_1.sendAllowedTmuxAction; } });
|
|
10
|
+
Object.defineProperty(exports, "sendTmuxLiteralText", { enumerable: true, get: function () { return client_1.sendTmuxLiteralText; } });
|
|
@@ -173,6 +173,7 @@ class GatewayHttpService {
|
|
|
173
173
|
requestType: "action",
|
|
174
174
|
payload: {
|
|
175
175
|
action: input.action,
|
|
176
|
+
...(input.action === "text" ? { text: input.text ?? "" } : {}),
|
|
176
177
|
},
|
|
177
178
|
}, { meta: { internal_call: true } });
|
|
178
179
|
const response = unwrapLiveRelayResult(rawResponse);
|
|
@@ -14,6 +14,7 @@ exports.getTmuxWindowHeight = getTmuxWindowHeight;
|
|
|
14
14
|
exports.captureTmuxPaneRange = captureTmuxPaneRange;
|
|
15
15
|
exports.captureVisibleTmuxPane = captureVisibleTmuxPane;
|
|
16
16
|
exports.sendAllowedTmuxAction = sendAllowedTmuxAction;
|
|
17
|
+
exports.sendTmuxLiteralText = sendTmuxLiteralText;
|
|
17
18
|
exports.sendTmuxLiteralLine = sendTmuxLiteralLine;
|
|
18
19
|
const node_child_process_1 = require("node:child_process");
|
|
19
20
|
const promises_1 = require("node:fs/promises");
|
|
@@ -258,8 +259,8 @@ async function sendAllowedTmuxAction(config, target, action) {
|
|
|
258
259
|
: "Enter";
|
|
259
260
|
await execFileAsync("tmux", buildTmuxArgs(config, ["send-keys", "-t", target, key]));
|
|
260
261
|
}
|
|
261
|
-
async function
|
|
262
|
-
const normalized = text.replace(/\r?\n/g, " ")
|
|
262
|
+
async function sendTmuxLiteralText(config, target, text) {
|
|
263
|
+
const normalized = text.replace(/\r?\n/g, " ");
|
|
263
264
|
const bufferName = `telegram-mcp-${Date.now().toString(36)}`;
|
|
264
265
|
if (normalized.length > 0) {
|
|
265
266
|
try {
|
|
@@ -276,6 +277,11 @@ async function sendTmuxLiteralLine(config, target, text) {
|
|
|
276
277
|
finally {
|
|
277
278
|
await execFileAsync("tmux", buildTmuxArgs(config, ["delete-buffer", "-b", bufferName])).catch(() => undefined);
|
|
278
279
|
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function sendTmuxLiteralLine(config, target, text) {
|
|
283
|
+
await sendTmuxLiteralText(config, target, text);
|
|
284
|
+
if (text.length > 0) {
|
|
279
285
|
await delay(ENTER_AFTER_PASTE_DELAY_MS);
|
|
280
286
|
}
|
|
281
287
|
await execFileAsync("tmux", buildTmuxArgs(config, ["send-keys", "-t", target, SUBMIT_LINE_KEY]));
|