@deadragdoll/tellymcp 0.0.4 → 0.0.5

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/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.5",
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": {