@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 CHANGED
@@ -7,16 +7,73 @@
7
7
  [![node >= 24](https://img.shields.io/badge/node-%3E%3D24-339933)](https://nodejs.org/)
8
8
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
9
 
10
- TellyMCP — это Telegram Human-in-the-Loop MCP server для coding agents.
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
- - привязывать несколько agent-сессий
17
- - работать с локальными и удалёнными партнёрскими сессиями
18
- - открывать Live tmux view внутри Telegram Mini App
19
- - обмениваться note, скриншотами и файлами через `.mcp-xchange`
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
  [![node >= 24](https://img.shields.io/badge/node-%3E%3D24-339933)](https://nodejs.org/)
8
8
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
9
 
10
- TellyMCP is a Telegram Human-in-the-Loop MCP server for coding agents.
10
+ TellyMCP is a self-hosted Telegram control plane for coding agents.
11
11
 
12
- It lets an agent:
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
- - pair multiple agent sessions
17
- - collaborate across local and remote sessions
18
- - open a live tmux view inside Telegram Mini App
19
- - exchange notes, screenshots, and files through `.mcp-xchange`
20
-
21
- Current tools:
22
-
23
- - `create_session_pair_code`
24
- - `clear_session_pairing`
25
- - `set_session_context`
26
- - `set_tmux_target`
27
- - `get_tmux_target`
28
- - `get_session_context`
29
- - `clear_session_context`
30
- - `rename_session`
31
- - `notify_telegram`
32
- - `get_telegram_inbox_count`
33
- - `get_telegram_inbox`
34
- - `delete_telegram_inbox_message`
35
- - `ask_user_telegram`
36
- - `browser_open`
37
- - `browser_reload`
38
- - `browser_click`
39
- - `browser_fill`
40
- - `browser_press`
41
- - `browser_wait_for`
42
- - `browser_wait_for_url`
43
- - `browser_console`
44
- - `browser_errors`
45
- - `browser_network_failures`
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
- if (!["up", "down", "enter", "slash", "delete", "tab", "escape", "interrupt"].includes(action)) {
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
- await (0, tmux_1.sendAllowedTmuxAction)(runtime.config.tmux, session.tmuxTarget, action);
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
- if (!["up", "down", "enter", "slash", "delete", "tab", "escape", "interrupt"].includes(action)) {
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
- await (0, tmux_1.sendAllowedTmuxAction)(runtime.config.tmux, session.tmuxTarget, action);
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: center;
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
- sendAction("interrupt").catch((error) => setStatus(error.message || String(error), true));
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">↵</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 sendTmuxLiteralLine(config, target, text) {
262
- const normalized = text.replace(/\r?\n/g, " ").trim();
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]));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deadragdoll/tellymcp",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "TellyMCP - Telegram Human-in-the-Loop MCP Server",
5
5
  "main": "dist/services/features/telegram-mcp/runtime.service.js",
6
6
  "bin": {