@deadragdoll/tellymcp 0.0.3 → 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/README-ru.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # TellyMCP
2
2
 
3
- [English](README.md) | [Русский](README-ru.md) | [Standalone Guide](STANDALONE.md) | [Standalone RU](STANDALONE-ru.md)
3
+ [English](README.md) | [Русский](README-ru.md) | [Standalone Guide](STANDALONE.md) | [Standalone RU](STANDALONE-ru.md) | [Release Notes](VERSION.md)
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/%40deadragdoll%2Ftellymcp)](https://www.npmjs.com/package/@deadragdoll/tellymcp)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/%40deadragdoll%2Ftellymcp)](https://www.npmjs.com/package/@deadragdoll/tellymcp)
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # TellyMCP
2
2
 
3
- [English](README.md) | [Русский](README-ru.md) | [Standalone Guide](STANDALONE.md) | [Standalone RU](STANDALONE-ru.md)
3
+ [English](README.md) | [Русский](README-ru.md) | [Standalone Guide](STANDALONE.md) | [Standalone RU](STANDALONE-ru.md) | [Release Notes](VERSION.md)
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/%40deadragdoll%2Ftellymcp)](https://www.npmjs.com/package/@deadragdoll/tellymcp)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/%40deadragdoll%2Ftellymcp)](https://www.npmjs.com/package/@deadragdoll/tellymcp)
package/VERSION.md ADDED
@@ -0,0 +1,115 @@
1
+ # TellyMCP Release Notes
2
+
3
+ Public, user-facing release notes for published versions of `@deadragdoll/tellymcp`.
4
+
5
+ For detailed engineering history, refactors, and internal development notes, see [CHANGELOG.md](CHANGELOG.md).
6
+
7
+ ## 0.0.3
8
+
9
+ ### Added
10
+ - Standalone CLI workflow:
11
+ - `tellymcp init <client|gateway|both>`
12
+ - `tellymcp run`
13
+ - `tellymcp doctor`
14
+ - `tellymcp mcp --help`
15
+ - Standalone and public installation guides:
16
+ - [STANDALONE.md](STANDALONE.md)
17
+ - [STANDALONE-ru.md](STANDALONE-ru.md)
18
+ - Browser runtime helper:
19
+ - `tellymcp browser install`
20
+ - Public README set for GitHub and npm:
21
+ - [README.md](README.md)
22
+ - [README-ru.md](README-ru.md)
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`
33
+
34
+ ### Changed
35
+ - Default installation path is now npm-first:
36
+ - `npm install -g @deadragdoll/tellymcp`
37
+ - Standalone client mode is documented first, before gateway/both deployment.
38
+ - `tmux` is now documented as a strongly recommended prerequisite for the full experience:
39
+ - Live View
40
+ - nudges
41
+ - direct terminal control from Telegram
42
+ - Environment examples were split into dedicated client and gateway variants.
43
+ - Package build/publish flow now validates itself before packing/publishing.
44
+ - CLI now shows package version directly in banners and startup output.
45
+
46
+ ### Collaboration
47
+ - Project collaboration works across local and remote sessions.
48
+ - `Collab` now includes:
49
+ - `Broadcast`
50
+ - `History`
51
+ - `Delete`
52
+ - `Ask` and `Share` semantics were clarified:
53
+ - `Ask` tells the selected session to do the work and reply back
54
+ - `Share` tells the current session to send something to the selected session
55
+
56
+ ### Live View
57
+ - Telegram Mini App Live View supports:
58
+ - fullscreen/expand launch policy
59
+ - bottom toolbar layout
60
+ - `Esc`
61
+ - `Tab`
62
+ - `Ctrl+C`
63
+ - `Backspace`
64
+ - `Up`
65
+ - `Down`
66
+ - `Enter`
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.
80
+
81
+ ### Browser
82
+ - Browser tools use Playwright Chromium.
83
+ - Headless mode is the recommended default for remote and SSH-based environments.
84
+ - `doctor` now helps detect missing browser runtime and connectivity issues.
85
+
86
+ ### Compatibility
87
+ - Gateway and clients now compare:
88
+ - package version
89
+ - protocol version
90
+ - capabilities
91
+ - `TOOLS.md` sync now detects outdated or missing local instructions and asks the session to refresh them.
92
+
93
+ ### Removed
94
+ - Legacy Go/HTTP tmux proxy path was removed.
95
+ - Direct product path is now local `tmux` only.
96
+
97
+ ## Next entry template
98
+
99
+ Copy this block for the next published version:
100
+
101
+ ```md
102
+ ## x.y.z
103
+
104
+ ### Added
105
+ - ...
106
+
107
+ ### Changed
108
+ - ...
109
+
110
+ ### Fixed
111
+ - ...
112
+
113
+ ### Removed
114
+ - ...
115
+ ```
package/dist/cli.js CHANGED
@@ -11,8 +11,10 @@ const node_net_1 = __importDefault(require("node:net"));
11
11
  const dotenv_1 = require("dotenv");
12
12
  const picocolors_1 = __importDefault(require("picocolors"));
13
13
  const ws_1 = __importDefault(require("ws"));
14
+ const versionHandshake_1 = require("./services/features/telegram-mcp/src/shared/lib/version/versionHandshake");
14
15
  const distDir = __dirname;
15
16
  const packageRoot = node_path_1.default.resolve(distDir, "..");
17
+ const cliPackageVersion = (0, versionHandshake_1.getTellyMcpPackageVersion)(__dirname);
16
18
  function getTmuxStatus() {
17
19
  const result = (0, node_child_process_1.spawnSync)("tmux", ["-V"], {
18
20
  encoding: "utf8",
@@ -27,7 +29,7 @@ function getTmuxStatus() {
27
29
  return { found: false };
28
30
  }
29
31
  function printBanner(title, subtitle) {
30
- process.stdout.write(`${picocolors_1.default.bold(picocolors_1.default.cyan("TellyMCP"))} ${picocolors_1.default.dim(title)}\n`);
32
+ process.stdout.write(`${picocolors_1.default.bold(picocolors_1.default.cyan("TellyMCP"))} ${picocolors_1.default.bold(picocolors_1.default.white(`v${cliPackageVersion}`))} ${picocolors_1.default.dim(title)}\n`);
31
33
  if (subtitle) {
32
34
  process.stdout.write(`${picocolors_1.default.dim(subtitle)}\n`);
33
35
  }
@@ -666,6 +668,7 @@ function runRuntime(args) {
666
668
  if (!(0, node_fs_1.existsSync)(servicesPath)) {
667
669
  fail(`Missing compiled services: ${servicesPath}`);
668
670
  }
671
+ printBanner("run", "Starting packaged runtime");
669
672
  const tmux = getTmuxStatus();
670
673
  if (tmux.found) {
671
674
  process.stdout.write(`${picocolors_1.default.green("tmux detected:")} ${tmux.version}\n`);
@@ -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,
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TELEGRAM_MCP_RUNTIME_SERVICE_NAME = void 0;
4
4
  const runtime_1 = require("./src/app/bootstrap/runtime");
5
+ const versionHandshake_1 = require("./src/shared/lib/version/versionHandshake");
5
6
  exports.TELEGRAM_MCP_RUNTIME_SERVICE_NAME = "telegramMcp.runtime";
6
7
  const TelegramMcpRuntimeService = {
7
8
  name: exports.TELEGRAM_MCP_RUNTIME_SERVICE_NAME,
@@ -17,11 +18,17 @@ const TelegramMcpRuntimeService = {
17
18
  },
18
19
  },
19
20
  async started() {
20
- this.logger.info("Starting telegram_mcp runtime service");
21
+ this.logger.info("Starting telegram_mcp runtime service", {
22
+ packageVersion: (0, versionHandshake_1.getTellyMcpPackageVersion)(__dirname),
23
+ protocolVersion: versionHandshake_1.TELLYMCP_PROTOCOL_VERSION,
24
+ });
21
25
  this.runtime = await (0, runtime_1.createAppRuntime)({
22
26
  callBroker: (actionName, params, options) => this.broker.call(actionName, params, options),
23
27
  });
24
- this.logger.info("telegram_mcp runtime service is ready");
28
+ this.logger.info("telegram_mcp runtime service is ready", {
29
+ packageVersion: (0, versionHandshake_1.getTellyMcpPackageVersion)(__dirname),
30
+ protocolVersion: versionHandshake_1.TELLYMCP_PROTOCOL_VERSION,
31
+ });
25
32
  },
26
33
  async stopped() {
27
34
  if (!this.runtime) {
@@ -77,6 +77,8 @@ async function createAppRuntime(input) {
77
77
  logger.info("Telegram transport ready");
78
78
  await telegramTransport.recoverPendingInboxNudges();
79
79
  logger.info("Startup inbox nudge recovery completed");
80
+ await telegramTransport.sendStartupNotifications();
81
+ logger.info("Startup Telegram notifications completed");
80
82
  const gatewayHttpService = new gatewayHttpService_1.GatewayHttpService(config, input.callBroker);
81
83
  return {
82
84
  config,
@@ -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);
@@ -17,6 +17,7 @@ const collabSemantics_1 = require("./collabSemantics");
17
17
  const collabUi_1 = require("./collabUi");
18
18
  const proxyFetch_1 = require("./proxyFetch");
19
19
  const client_1 = require("../tmux/client");
20
+ const versionHandshake_1 = require("../../lib/version/versionHandshake");
20
21
  const LOCAL_INDEX_FILE_NAME = "LOCAL_INDEX.md";
21
22
  function trimTrailingSlashes(value) {
22
23
  return value.replace(/\/+$/u, "");
@@ -25,6 +26,14 @@ function normalizeBasePath(value) {
25
26
  const trimmed = trimTrailingSlashes(value.trim());
26
27
  return trimmed.startsWith("/") ? trimmed || "/" : `/${trimmed || ""}`;
27
28
  }
29
+ function joinHttpPath(prefix, suffix) {
30
+ const normalizedPrefix = prefix ? normalizeBasePath(prefix) : "";
31
+ const normalizedSuffix = normalizeBasePath(suffix);
32
+ if (!normalizedPrefix || normalizedPrefix === "/") {
33
+ return normalizedSuffix;
34
+ }
35
+ return `${normalizedPrefix}${normalizedSuffix}`.replace(/\/{2,}/gu, "/");
36
+ }
28
37
  function resolveWebAppPublicBaseUrl(config) {
29
38
  if (!config.webapp.publicUrl) {
30
39
  return null;
@@ -755,6 +764,92 @@ class TelegramTransport {
755
764
  recoveredSessions: recoveredCount,
756
765
  });
757
766
  }
767
+ async sendStartupNotifications() {
768
+ const packageVersion = (0, versionHandshake_1.getTellyMcpPackageVersion)(__dirname);
769
+ const sessions = await this.sessionStore.listSessions();
770
+ const groupedRecipients = new Map();
771
+ for (const session of sessions) {
772
+ const binding = await this.bindingStore.getBinding(session.sessionId);
773
+ if (!binding) {
774
+ continue;
775
+ }
776
+ const key = `${binding.telegramChatId}:${binding.telegramUserId}`;
777
+ const current = groupedRecipients.get(key);
778
+ if (current) {
779
+ current.sessionIds.push(session.sessionId);
780
+ current.sessionLabels.push(session.label ?? session.sessionId);
781
+ continue;
782
+ }
783
+ groupedRecipients.set(key, {
784
+ binding: {
785
+ telegramChatId: binding.telegramChatId,
786
+ telegramUserId: binding.telegramUserId,
787
+ },
788
+ sessionIds: [session.sessionId],
789
+ sessionLabels: [session.label ?? session.sessionId],
790
+ });
791
+ }
792
+ if (groupedRecipients.size === 0) {
793
+ this.logger.info("Skipping startup notifications because no Telegram sessions are paired");
794
+ return;
795
+ }
796
+ const runtimePort = this.config.distributed.mode === "gateway" || this.config.distributed.mode === "both"
797
+ ? Number(process.env.PORT || this.config.mcp.httpPort)
798
+ : this.config.mcp.httpPort;
799
+ const rootPrefix = this.config.distributed.mode === "gateway" || this.config.distributed.mode === "both"
800
+ ? normalizeBasePath(process.env.ROOT_PREFIX || "/api")
801
+ : "";
802
+ const localMcpPath = this.config.distributed.mode === "gateway" || this.config.distributed.mode === "both"
803
+ ? joinHttpPath(rootPrefix, this.config.mcp.httpPath)
804
+ : this.config.mcp.httpPath;
805
+ const localWebappPath = this.config.distributed.mode === "gateway" || this.config.distributed.mode === "both"
806
+ ? joinHttpPath(rootPrefix, this.config.webapp.basePath)
807
+ : this.config.webapp.basePath;
808
+ const localMcpUrl = `http://${this.config.mcp.httpHost}:${runtimePort}${localMcpPath}`;
809
+ const localWebappUrl = `http://${this.config.mcp.httpHost}:${runtimePort}${localWebappPath}`;
810
+ for (const recipientGroup of groupedRecipients.values()) {
811
+ const primarySessionId = recipientGroup.sessionIds[0];
812
+ if (!primarySessionId) {
813
+ continue;
814
+ }
815
+ const uniqueSessionLabels = Array.from(new Set(recipientGroup.sessionLabels)).sort();
816
+ const startupMessage = [
817
+ "✅ TellyMCP запущен.",
818
+ `Версия: ${packageVersion}`,
819
+ `Протокол: ${versionHandshake_1.TELLYMCP_PROTOCOL_VERSION}`,
820
+ `Режим: ${this.config.distributed.mode}`,
821
+ ...(this.config.telegram.botUsername
822
+ ? [`Бот: @${this.config.telegram.botUsername.replace(/^@/u, "")}`]
823
+ : []),
824
+ `Сессии: ${uniqueSessionLabels.join(", ")}`,
825
+ `MCP: ${localMcpUrl}`,
826
+ ...(this.config.webapp.enabled ? [`WebApp: ${localWebappUrl}`] : []),
827
+ ...(this.config.distributed.gatewayPublicUrl
828
+ ? [`Gateway: ${this.config.distributed.gatewayPublicUrl}`]
829
+ : []),
830
+ ...(this.config.distributed.gatewayWsUrl
831
+ ? [`Gateway WS: ${this.config.distributed.gatewayWsUrl}`]
832
+ : []),
833
+ `Browser: ${this.config.browser.enabled ? (this.config.browser.headless ? "enabled, headless" : "enabled, headed") : "disabled"}`,
834
+ "Напиши /menu, чтобы открыть меню сессий.",
835
+ ].join("\n");
836
+ try {
837
+ await this.sendNotification({
838
+ sessionId: primarySessionId,
839
+ sessionLabel: "TellyMCP",
840
+ recipient: recipientGroup.binding,
841
+ message: startupMessage,
842
+ });
843
+ }
844
+ catch (error) {
845
+ this.logger.warn("Failed to deliver Telegram startup notification", {
846
+ telegramChatId: recipientGroup.binding.telegramChatId,
847
+ telegramUserId: recipientGroup.binding.telegramUserId,
848
+ error: error instanceof Error ? error.message : String(error),
849
+ });
850
+ }
851
+ }
852
+ }
758
853
  async sendRequest(input) {
759
854
  const text = (0, messageFormat_1.formatTelegramMessage)(input, {
760
855
  maxQuestionChars: this.config.telegram.maxQuestionChars,
@@ -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.3",
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": {
@@ -14,6 +14,7 @@
14
14
  "README-ru.md",
15
15
  "STANDALONE.md",
16
16
  "STANDALONE-ru.md",
17
+ "VERSION.md",
17
18
  "CHANGELOG.md",
18
19
  "TOOLS.md",
19
20
  ".env.example.client",