@deadragdoll/tellymcp 0.0.8 → 0.0.9
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 +21 -1
- package/README.md +5 -1
- package/TOOLS.md +3 -1
- package/VERSION.md +32 -0
- package/dist/services/features/telegram-mcp/src/app/http.js +8 -1
- package/dist/services/features/telegram-mcp/src/app/providers/mcp/server.js +2 -1
- package/dist/services/features/telegram-mcp/src/app/webapp/assets.js +201 -60
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +93 -6
- package/package.json +1 -1
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) | [Release Notes](VERSION.md)
|
|
3
|
+
[English](README.md) | [Русский](README-ru.md) | [Standalone Guide](STANDALONE.md) | [Standalone RU](STANDALONE-ru.md) | [Screenshots](screenshots/README.md) | [Gallery](screenshots/GALLERY.md) | [Release Notes](VERSION.md)
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@deadragdoll/tellymcp)
|
|
6
6
|
[](https://www.npmjs.com/package/@deadragdoll/tellymcp)
|
|
@@ -239,6 +239,26 @@ tellymcp run --env .env
|
|
|
239
239
|
|
|
240
240
|
- `https://your-host.example/api/mcp`
|
|
241
241
|
|
|
242
|
+
## Mini App / Live
|
|
243
|
+
|
|
244
|
+
Если `WEBAPP_ENABLED=true`, в меню сессии появляется `🖥 Live`.
|
|
245
|
+
|
|
246
|
+
Mini App:
|
|
247
|
+
|
|
248
|
+
- обслуживается этим же Node service по `WEBAPP_BASE_PATH`
|
|
249
|
+
- в `client`-режиме тоже может открываться через общий gateway domain
|
|
250
|
+
- показывает видимую область tmux pane и отправляет ограниченный набор control actions
|
|
251
|
+
- валидирует Telegram `initData` на сервере
|
|
252
|
+
- умеет открываться в режимах:
|
|
253
|
+
- `default`
|
|
254
|
+
- `expand`
|
|
255
|
+
- `fullscreen` с fallback на `expand`
|
|
256
|
+
- после успешного bootstrap удаляет временное launcher-сообщение
|
|
257
|
+
- автоматически восстанавливается после короткого рестарта шлюза:
|
|
258
|
+
- краткий `502/503` переживается обычным polling
|
|
259
|
+
- если in-process WebApp session на шлюзе была потеряна и приходит `401/403`, Mini App сама делает повторный bootstrap
|
|
260
|
+
- в типовом restart-case переоткрывать `Live` не нужно
|
|
261
|
+
|
|
242
262
|
## Docker: только для инфраструктуры или для `gateway`-only
|
|
243
263
|
|
|
244
264
|
Docker больше не является основным способом запуска TellyMCP, но один container path поддерживается:
|
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) | [Release Notes](VERSION.md)
|
|
3
|
+
[English](README.md) | [Русский](README-ru.md) | [Standalone Guide](STANDALONE.md) | [Standalone RU](STANDALONE-ru.md) | [Screenshots](screenshots/README.md) | [Gallery](screenshots/GALLERY.md) | [Release Notes](VERSION.md)
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@deadragdoll/tellymcp)
|
|
6
6
|
[](https://www.npmjs.com/package/@deadragdoll/tellymcp)
|
|
@@ -521,6 +521,10 @@ The Mini App:
|
|
|
521
521
|
- requires the Telegram user from `initData` to match the bound session user
|
|
522
522
|
- resolves the active session from the bound Telegram user, so a session id in the URL is not required for normal use
|
|
523
523
|
- deletes the temporary `Open Live View` launcher message after successful Mini App bootstrap
|
|
524
|
+
- auto-recovers after a short gateway restart:
|
|
525
|
+
- short `502/503` periods are tolerated by polling
|
|
526
|
+
- expired in-process WebApp sessions (`401/403`) trigger an automatic re-bootstrap
|
|
527
|
+
- in normal restart cases the user does not need to reopen `Live`
|
|
524
528
|
- allows only a fixed control set:
|
|
525
529
|
- `Esc`
|
|
526
530
|
- `Tab`
|
package/TOOLS.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# Tools
|
|
2
2
|
|
|
3
|
-
Version: `2026-05-
|
|
3
|
+
Version: `2026-05-18.1`
|
|
4
4
|
|
|
5
5
|
Gateway/client runtime compatibility:
|
|
6
6
|
|
|
7
7
|
- `TOOLS.md` hash sync and runtime version handshake are separate checks.
|
|
8
|
+
- The authoritative freshness check for `TOOLS.md` is the content hash, not this human-readable `Version:` line.
|
|
9
|
+
- Treat this `Version:` line only as a quick visual marker for humans and logs.
|
|
8
10
|
- `TOOLS.md` sync tells the agent to refresh instructions.
|
|
9
11
|
- `ws hello/hello_ack` checks `package_version`, `protocol_version`, and `capabilities`.
|
|
10
12
|
- If protocol major versions differ, gateway transport is blocked until the older side is upgraded.
|
package/VERSION.md
CHANGED
|
@@ -4,6 +4,34 @@ Public, user-facing release notes for published versions of `@deadragdoll/tellym
|
|
|
4
4
|
|
|
5
5
|
For detailed engineering history, refactors, and internal development notes, see [CHANGELOG.md](CHANGELOG.md).
|
|
6
6
|
|
|
7
|
+
## 0.0.9
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Public screenshot docs:
|
|
11
|
+
- full screenshot index in `screenshots/README.md`
|
|
12
|
+
- shorter public gallery in `screenshots/GALLERY.md`
|
|
13
|
+
- More explicit Live launch modes from Telegram:
|
|
14
|
+
- `Fullscreen`
|
|
15
|
+
- `Expand`
|
|
16
|
+
- `Default`
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- README and README-RU now surface screenshot links directly in the top navigation.
|
|
20
|
+
- The `Live` Mini App UI is more practical on phones:
|
|
21
|
+
- wrap/unwrap toggle moved into the status bar
|
|
22
|
+
- session name is shown as a compact state badge
|
|
23
|
+
- update timestamp is shorter and less noisy
|
|
24
|
+
- local `Live` screenshots are documented in a clearer flow near the launcher step
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- `Live` now reconnects more gracefully after a gateway restart:
|
|
28
|
+
- short `502/503` gaps recover through polling
|
|
29
|
+
- expired in-process WebApp sessions (`401/403`) trigger automatic re-bootstrap
|
|
30
|
+
- reopening the Mini App is no longer required in the normal restart case
|
|
31
|
+
- Runtime MCP metadata now reports the actual package version instead of stale hardcoded version data.
|
|
32
|
+
- Successful tmux nudges no longer overwrite `tmuxPaneId` with non-pane targets such as `backend:0.0`.
|
|
33
|
+
- If tmux itself is unavailable for a paired session with a saved tmux target, Telegram now receives an operational warning instead of leaving the signal only in backend logs.
|
|
34
|
+
|
|
7
35
|
## 0.0.8
|
|
8
36
|
|
|
9
37
|
### Added
|
|
@@ -32,6 +60,10 @@ For detailed engineering history, refactors, and internal development notes, see
|
|
|
32
60
|
- Stale tmux pane ids like `%1 -> %2` no longer require manual user understanding before the service can try to wake the session again.
|
|
33
61
|
- Broken tmux nudge targets are now visible to the user in Telegram, not only in backend logs.
|
|
34
62
|
- `Share` inbox instructions are now explicit enough to reduce the chance that one agent re-delegates the task back into the collaboration graph.
|
|
63
|
+
- `Live` Mini App now survives a normal gateway restart much better:
|
|
64
|
+
- short `502/503` periods recover through polling
|
|
65
|
+
- lost in-process WebApp sessions (`401/403`) trigger automatic re-bootstrap
|
|
66
|
+
- reopening the Mini App is no longer required in the normal restart case
|
|
35
67
|
|
|
36
68
|
## 0.0.3
|
|
37
69
|
|
|
@@ -105,6 +105,11 @@ function normalizePrefixedPathname(pathname) {
|
|
|
105
105
|
const stripped = pathname.slice(normalizedRootPrefix.length);
|
|
106
106
|
return stripped.startsWith("/") ? stripped : `/${stripped || ""}`;
|
|
107
107
|
}
|
|
108
|
+
function resolveLaunchModeOverride(value) {
|
|
109
|
+
return value === "default" || value === "expand" || value === "fullscreen"
|
|
110
|
+
? value
|
|
111
|
+
: null;
|
|
112
|
+
}
|
|
108
113
|
function normalizeBasePath(value) {
|
|
109
114
|
const trimmed = value.trim().replace(/\/+$/u, "");
|
|
110
115
|
if (!trimmed) {
|
|
@@ -152,9 +157,11 @@ function createMcpHttpHandler(runtime, input) {
|
|
|
152
157
|
writeText(res, 405, "Method not allowed");
|
|
153
158
|
return;
|
|
154
159
|
}
|
|
160
|
+
const launchMode = resolveLaunchModeOverride(requestUrl.searchParams.get("launchMode")) ??
|
|
161
|
+
runtime.config.webapp.launchMode;
|
|
155
162
|
writeHtml(res, 200, (0, assets_1.renderWebAppHtml)({
|
|
156
163
|
basePath: publicWebAppBasePath,
|
|
157
|
-
launchMode
|
|
164
|
+
launchMode,
|
|
158
165
|
}));
|
|
159
166
|
return;
|
|
160
167
|
}
|
|
@@ -3,10 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createMcpServer = createMcpServer;
|
|
4
4
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
5
5
|
const registry_1 = require("../../../shared/api/tool-registry/registry");
|
|
6
|
+
const versionHandshake_1 = require("../../../shared/lib/version/versionHandshake");
|
|
6
7
|
function createMcpServer(tools) {
|
|
7
8
|
const server = new mcp_js_1.McpServer({
|
|
8
9
|
name: "tellymcp",
|
|
9
|
-
version:
|
|
10
|
+
version: (0, versionHandshake_1.getTellyMcpPackageVersion)(__dirname),
|
|
10
11
|
});
|
|
11
12
|
(0, registry_1.registerTools)(server, tools);
|
|
12
13
|
return server;
|
|
@@ -100,6 +100,21 @@ body {
|
|
|
100
100
|
border-color: rgba(87, 193, 255, 0.8);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
.btn.active {
|
|
104
|
+
border-color: rgba(87, 193, 255, 0.85);
|
|
105
|
+
background: linear-gradient(180deg, rgba(20, 54, 77, 0.98) 0%, rgba(14, 39, 57, 1) 100%);
|
|
106
|
+
color: #d9f3ff;
|
|
107
|
+
box-shadow: inset 0 0 0 1px rgba(87, 193, 255, 0.16);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.btn.toggle {
|
|
111
|
+
border-color: rgba(87, 193, 255, 0.42);
|
|
112
|
+
background: linear-gradient(180deg, rgba(31, 38, 52, 0.98) 0%, rgba(21, 27, 38, 1) 100%);
|
|
113
|
+
color: #cfe9ff;
|
|
114
|
+
box-shadow: inset 0 0 0 1px rgba(87, 193, 255, 0.08);
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
}
|
|
117
|
+
|
|
103
118
|
.statusbar {
|
|
104
119
|
position: fixed;
|
|
105
120
|
left: 0;
|
|
@@ -127,11 +142,32 @@ body {
|
|
|
127
142
|
gap: 8px 16px;
|
|
128
143
|
}
|
|
129
144
|
|
|
145
|
+
.status-toggle {
|
|
146
|
+
min-width: 72px;
|
|
147
|
+
padding: 4px 8px;
|
|
148
|
+
border-radius: 9px;
|
|
149
|
+
font-size: 12px;
|
|
150
|
+
line-height: 1.1;
|
|
151
|
+
}
|
|
152
|
+
|
|
130
153
|
.session-label {
|
|
131
|
-
|
|
154
|
+
display: inline-flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
min-height: 24px;
|
|
157
|
+
padding: 3px 8px;
|
|
158
|
+
border-radius: 999px;
|
|
159
|
+
border: 1px solid rgba(76, 217, 100, 0.42);
|
|
160
|
+
background: rgba(26, 42, 31, 0.92);
|
|
161
|
+
color: #d6ffe0;
|
|
132
162
|
font-weight: 600;
|
|
133
163
|
}
|
|
134
164
|
|
|
165
|
+
.session-label.error {
|
|
166
|
+
border-color: rgba(255, 116, 116, 0.5);
|
|
167
|
+
background: rgba(57, 23, 26, 0.94);
|
|
168
|
+
color: #ffd8d8;
|
|
169
|
+
}
|
|
170
|
+
|
|
135
171
|
.ok {
|
|
136
172
|
color: var(--success);
|
|
137
173
|
}
|
|
@@ -146,6 +182,13 @@ body {
|
|
|
146
182
|
font: 13px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
147
183
|
}
|
|
148
184
|
|
|
185
|
+
.terminal.unwrap {
|
|
186
|
+
white-space: pre;
|
|
187
|
+
word-break: normal;
|
|
188
|
+
overflow-x: auto;
|
|
189
|
+
overflow-y: auto;
|
|
190
|
+
}
|
|
191
|
+
|
|
149
192
|
.ansi-bold {
|
|
150
193
|
font-weight: 700;
|
|
151
194
|
}
|
|
@@ -194,6 +237,8 @@ const state = {
|
|
|
194
237
|
timer: null,
|
|
195
238
|
actionBusy: false,
|
|
196
239
|
pollIntervalMs: 2000,
|
|
240
|
+
wrapEnabled: true,
|
|
241
|
+
recoverPromise: null,
|
|
197
242
|
};
|
|
198
243
|
|
|
199
244
|
const elements = {
|
|
@@ -201,6 +246,7 @@ const elements = {
|
|
|
201
246
|
status: document.querySelector("[data-role=status]"),
|
|
202
247
|
updated: document.querySelector("[data-role=updated]"),
|
|
203
248
|
interrupt: document.querySelector("[data-role=interrupt]"),
|
|
249
|
+
wrap: document.querySelector("[data-role=wrap]"),
|
|
204
250
|
type: document.querySelector("[data-role=type]"),
|
|
205
251
|
esc: document.querySelector("[data-role=escape]"),
|
|
206
252
|
tab: document.querySelector("[data-role=tab]"),
|
|
@@ -213,16 +259,80 @@ const elements = {
|
|
|
213
259
|
};
|
|
214
260
|
|
|
215
261
|
function setStatus(text, isError = false) {
|
|
216
|
-
|
|
217
|
-
elements.status.textContent = text + " - " + sessionName;
|
|
262
|
+
elements.status.textContent = text;
|
|
218
263
|
elements.status.classList.toggle("error", isError);
|
|
219
264
|
elements.status.classList.toggle("ok", !isError);
|
|
265
|
+
elements.session.classList.toggle("error", isError);
|
|
266
|
+
elements.session.classList.toggle("ok", !isError);
|
|
220
267
|
}
|
|
221
268
|
|
|
222
269
|
function setUpdated(text) {
|
|
223
270
|
elements.updated.textContent = text;
|
|
224
271
|
}
|
|
225
272
|
|
|
273
|
+
function createHttpError(message, status) {
|
|
274
|
+
const error = new Error(message);
|
|
275
|
+
error.status = status;
|
|
276
|
+
return error;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function shouldRecoverWebAppSession(error) {
|
|
280
|
+
return (
|
|
281
|
+
error &&
|
|
282
|
+
typeof error === "object" &&
|
|
283
|
+
(error.status === 401 || error.status === 403)
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function applyTmuxAvailability(hasTarget) {
|
|
288
|
+
elements.interrupt.disabled = !hasTarget;
|
|
289
|
+
elements.type.disabled = !hasTarget;
|
|
290
|
+
elements.esc.disabled = !hasTarget;
|
|
291
|
+
elements.tab.disabled = !hasTarget;
|
|
292
|
+
elements.slash.disabled = !hasTarget;
|
|
293
|
+
elements.del.disabled = !hasTarget;
|
|
294
|
+
elements.up.disabled = !hasTarget;
|
|
295
|
+
elements.down.disabled = !hasTarget;
|
|
296
|
+
elements.enter.disabled = !hasTarget;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function getWrapPreferenceKey() {
|
|
300
|
+
return "telegram-mcp-live-wrap";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function applyWrapMode(enabled) {
|
|
304
|
+
state.wrapEnabled = enabled;
|
|
305
|
+
elements.terminal.classList.toggle("unwrap", !enabled);
|
|
306
|
+
elements.wrap.classList.toggle("active", enabled);
|
|
307
|
+
elements.wrap.setAttribute("aria-pressed", enabled ? "true" : "false");
|
|
308
|
+
elements.wrap.textContent = enabled ? "Unwrap" : "Wrap";
|
|
309
|
+
elements.wrap.title = enabled
|
|
310
|
+
? "Wrapping enabled. Tap to switch to horizontal scroll."
|
|
311
|
+
: "Horizontal scroll enabled. Tap to wrap lines.";
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function loadWrapPreference() {
|
|
315
|
+
try {
|
|
316
|
+
const stored = window.localStorage.getItem(getWrapPreferenceKey());
|
|
317
|
+
if (stored === "off") {
|
|
318
|
+
applyWrapMode(false);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
} catch (_error) {
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
applyWrapMode(true);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function toggleWrapMode() {
|
|
328
|
+
const next = !state.wrapEnabled;
|
|
329
|
+
applyWrapMode(next);
|
|
330
|
+
try {
|
|
331
|
+
window.localStorage.setItem(getWrapPreferenceKey(), next ? "on" : "off");
|
|
332
|
+
} catch (_error) {
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
226
336
|
function formatCapturedAt(value) {
|
|
227
337
|
if (!value) {
|
|
228
338
|
return "never";
|
|
@@ -234,12 +344,10 @@ function formatCapturedAt(value) {
|
|
|
234
344
|
}
|
|
235
345
|
|
|
236
346
|
return new Intl.DateTimeFormat(undefined, {
|
|
237
|
-
year: "numeric",
|
|
238
|
-
month: "2-digit",
|
|
239
|
-
day: "2-digit",
|
|
240
347
|
hour: "2-digit",
|
|
241
348
|
minute: "2-digit",
|
|
242
349
|
second: "2-digit",
|
|
350
|
+
hour12: false,
|
|
243
351
|
}).format(date);
|
|
244
352
|
}
|
|
245
353
|
|
|
@@ -561,6 +669,49 @@ async function bootstrap() {
|
|
|
561
669
|
return response.json();
|
|
562
670
|
}
|
|
563
671
|
|
|
672
|
+
function applyBootstrapPayload(bootstrapPayload) {
|
|
673
|
+
state.token = bootstrapPayload.token;
|
|
674
|
+
state.sessionId = bootstrapPayload.session_id;
|
|
675
|
+
state.pollIntervalMs = bootstrapPayload.poll_interval_ms || state.pollIntervalMs;
|
|
676
|
+
elements.session.textContent =
|
|
677
|
+
bootstrapPayload.session_label || bootstrapPayload.session_id;
|
|
678
|
+
elements.session.hidden = false;
|
|
679
|
+
|
|
680
|
+
const hasTmuxTarget = Boolean(bootstrapPayload.tmux_target);
|
|
681
|
+
applyTmuxAvailability(hasTmuxTarget);
|
|
682
|
+
setStatus(hasTmuxTarget ? "Live" : "No tmux target", !hasTmuxTarget);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async function recoverWebAppSession() {
|
|
686
|
+
if (state.recoverPromise) {
|
|
687
|
+
return state.recoverPromise;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
state.recoverPromise = (async () => {
|
|
691
|
+
setStatus("Reconnecting...", true);
|
|
692
|
+
const bootstrapPayload = await bootstrap();
|
|
693
|
+
applyBootstrapPayload(bootstrapPayload);
|
|
694
|
+
return bootstrapPayload;
|
|
695
|
+
})().finally(() => {
|
|
696
|
+
state.recoverPromise = null;
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
return state.recoverPromise;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function withRecoveredSession(operation) {
|
|
703
|
+
try {
|
|
704
|
+
return await operation();
|
|
705
|
+
} catch (error) {
|
|
706
|
+
if (!shouldRecoverWebAppSession(error)) {
|
|
707
|
+
throw error;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
await recoverWebAppSession();
|
|
711
|
+
return operation();
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
564
715
|
async function fetchVisibleBuffer() {
|
|
565
716
|
const response = await fetch(config.basePath + "/api/view", {
|
|
566
717
|
method: "GET",
|
|
@@ -571,7 +722,7 @@ async function fetchVisibleBuffer() {
|
|
|
571
722
|
|
|
572
723
|
if (!response.ok) {
|
|
573
724
|
const text = await response.text();
|
|
574
|
-
throw
|
|
725
|
+
throw createHttpError(text || "Failed to fetch visible buffer.", response.status);
|
|
575
726
|
}
|
|
576
727
|
|
|
577
728
|
return response.json();
|
|
@@ -584,21 +735,22 @@ async function sendAction(action) {
|
|
|
584
735
|
|
|
585
736
|
state.actionBusy = true;
|
|
586
737
|
try {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
if (!response.ok) {
|
|
597
|
-
const text = await response.text();
|
|
598
|
-
throw new Error(text || "Failed to send action.");
|
|
599
|
-
}
|
|
738
|
+
await withRecoveredSession(async () => {
|
|
739
|
+
const response = await fetch(config.basePath + "/api/action", {
|
|
740
|
+
method: "POST",
|
|
741
|
+
headers: {
|
|
742
|
+
"content-type": "application/json",
|
|
743
|
+
authorization: "Bearer " + state.token,
|
|
744
|
+
},
|
|
745
|
+
body: JSON.stringify({ action }),
|
|
746
|
+
});
|
|
600
747
|
|
|
601
|
-
|
|
748
|
+
if (!response.ok) {
|
|
749
|
+
const text = await response.text();
|
|
750
|
+
throw createHttpError(text || "Failed to send action.", response.status);
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
await withRecoveredSession(refreshVisibleBuffer);
|
|
602
754
|
} finally {
|
|
603
755
|
state.actionBusy = false;
|
|
604
756
|
}
|
|
@@ -611,22 +763,26 @@ async function sendTextInput(text) {
|
|
|
611
763
|
|
|
612
764
|
state.actionBusy = true;
|
|
613
765
|
try {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
if (!response.ok) {
|
|
624
|
-
const text = await response.text();
|
|
625
|
-
throw new Error(text || "Failed to send text.");
|
|
626
|
-
}
|
|
766
|
+
await withRecoveredSession(async () => {
|
|
767
|
+
const response = await fetch(config.basePath + "/api/action", {
|
|
768
|
+
method: "POST",
|
|
769
|
+
headers: {
|
|
770
|
+
"content-type": "application/json",
|
|
771
|
+
authorization: "Bearer " + state.token,
|
|
772
|
+
},
|
|
773
|
+
body: JSON.stringify({ action: "text", text }),
|
|
774
|
+
});
|
|
627
775
|
|
|
776
|
+
if (!response.ok) {
|
|
777
|
+
const textResponse = await response.text();
|
|
778
|
+
throw createHttpError(
|
|
779
|
+
textResponse || "Failed to send text.",
|
|
780
|
+
response.status,
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
});
|
|
628
784
|
setStatus("Text sent");
|
|
629
|
-
await refreshVisibleBuffer
|
|
785
|
+
await withRecoveredSession(refreshVisibleBuffer);
|
|
630
786
|
} finally {
|
|
631
787
|
state.actionBusy = false;
|
|
632
788
|
}
|
|
@@ -646,9 +802,9 @@ function confirmInterrupt() {
|
|
|
646
802
|
}
|
|
647
803
|
|
|
648
804
|
async function refreshVisibleBuffer() {
|
|
649
|
-
const payload = await fetchVisibleBuffer
|
|
805
|
+
const payload = await withRecoveredSession(fetchVisibleBuffer);
|
|
650
806
|
elements.terminal.innerHTML = renderAnsiToHtml(payload.content || "");
|
|
651
|
-
setUpdated(
|
|
807
|
+
setUpdated(formatCapturedAt(payload.captured_at));
|
|
652
808
|
}
|
|
653
809
|
|
|
654
810
|
function stopPolling() {
|
|
@@ -672,6 +828,10 @@ function startPolling() {
|
|
|
672
828
|
}
|
|
673
829
|
|
|
674
830
|
function bindUi() {
|
|
831
|
+
elements.wrap.addEventListener("click", () => {
|
|
832
|
+
toggleWrapMode();
|
|
833
|
+
});
|
|
834
|
+
|
|
675
835
|
elements.interrupt.addEventListener("click", () => {
|
|
676
836
|
confirmInterrupt()
|
|
677
837
|
.then((ok) => {
|
|
@@ -748,32 +908,12 @@ async function applyLaunchMode() {
|
|
|
748
908
|
async function main() {
|
|
749
909
|
try {
|
|
750
910
|
await applyLaunchMode();
|
|
911
|
+
loadWrapPreference();
|
|
751
912
|
bindUi();
|
|
752
913
|
setStatus("Authorizing Mini App...");
|
|
753
914
|
const bootstrapPayload = await bootstrap();
|
|
754
|
-
|
|
755
|
-
state.sessionId = bootstrapPayload.session_id;
|
|
756
|
-
state.pollIntervalMs = bootstrapPayload.poll_interval_ms || state.pollIntervalMs;
|
|
757
|
-
elements.session.textContent =
|
|
758
|
-
bootstrapPayload.session_label || bootstrapPayload.session_id;
|
|
759
|
-
|
|
760
|
-
if (!bootstrapPayload.tmux_target) {
|
|
761
|
-
elements.interrupt.disabled = true;
|
|
762
|
-
elements.type.disabled = true;
|
|
763
|
-
elements.esc.disabled = true;
|
|
764
|
-
elements.tab.disabled = true;
|
|
765
|
-
elements.slash.disabled = true;
|
|
766
|
-
elements.del.disabled = true;
|
|
767
|
-
elements.up.disabled = true;
|
|
768
|
-
elements.down.disabled = true;
|
|
769
|
-
elements.enter.disabled = true;
|
|
770
|
-
setStatus("No tmux target", true);
|
|
771
|
-
}
|
|
772
|
-
|
|
915
|
+
applyBootstrapPayload(bootstrapPayload);
|
|
773
916
|
await refreshVisibleBuffer();
|
|
774
|
-
if (bootstrapPayload.tmux_target) {
|
|
775
|
-
setStatus("Connected");
|
|
776
|
-
}
|
|
777
917
|
startPolling();
|
|
778
918
|
} catch (error) {
|
|
779
919
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -818,7 +958,8 @@ function renderWebAppHtml(input) {
|
|
|
818
958
|
<span class="session-label" data-role="session" hidden>Live View</span>
|
|
819
959
|
</div>
|
|
820
960
|
<div class="status-right">
|
|
821
|
-
<
|
|
961
|
+
<button class="btn toggle status-toggle active" data-role="wrap" type="button" aria-pressed="true">Wrap</button>
|
|
962
|
+
<span data-role="updated">never</span>
|
|
822
963
|
</div>
|
|
823
964
|
</div>
|
|
824
965
|
</div>
|
|
@@ -1236,9 +1236,15 @@ class TelegramTransport {
|
|
|
1236
1236
|
...(input.project_name ? [`Проект: ${input.project_name}`] : []),
|
|
1237
1237
|
`Сессия: ${input.source_session_label} -> ${input.target_session_label}`,
|
|
1238
1238
|
"",
|
|
1239
|
-
"Открой Live по
|
|
1239
|
+
"Открой Live в нужном режиме по кнопкам ниже.",
|
|
1240
1240
|
].join("\n"), {
|
|
1241
|
-
reply_markup:
|
|
1241
|
+
reply_markup: this.buildLiveViewLaunchKeyboard((mode) => this.buildLiveViewUrlForSessionTarget({
|
|
1242
|
+
targetSessionId: input.target_session_id,
|
|
1243
|
+
targetClientUuid: input.target_client_uuid,
|
|
1244
|
+
targetLocalSessionId: input.target_local_session_id,
|
|
1245
|
+
sourceClientUuid: input.source_client_uuid,
|
|
1246
|
+
launchMode: mode,
|
|
1247
|
+
})),
|
|
1242
1248
|
}, {
|
|
1243
1249
|
kind: "notification",
|
|
1244
1250
|
sessionId: sourceSession.sessionId,
|
|
@@ -2533,6 +2539,12 @@ class TelegramTransport {
|
|
|
2533
2539
|
: String(error),
|
|
2534
2540
|
};
|
|
2535
2541
|
if ((0, client_1.isTmuxUnavailableError)(error)) {
|
|
2542
|
+
void this.sessionStore.getSession(sessionId).then((session) => {
|
|
2543
|
+
if (!session?.tmuxTarget) {
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
return this.notifyTmuxUnavailable(sessionId, session, error);
|
|
2547
|
+
});
|
|
2536
2548
|
this.logger.warn("tmux nudge skipped because tmux is unavailable", payload);
|
|
2537
2549
|
return;
|
|
2538
2550
|
}
|
|
@@ -2611,7 +2623,11 @@ class TelegramTransport {
|
|
|
2611
2623
|
await this.sessionStore.setSession({
|
|
2612
2624
|
...session,
|
|
2613
2625
|
tmuxTarget,
|
|
2614
|
-
...(tmuxTarget
|
|
2626
|
+
...(tmuxTarget.startsWith("%")
|
|
2627
|
+
? { tmuxPaneId: tmuxTarget }
|
|
2628
|
+
: session.tmuxPaneId
|
|
2629
|
+
? { tmuxPaneId: session.tmuxPaneId }
|
|
2630
|
+
: {}),
|
|
2615
2631
|
lastTmuxNudgeAt,
|
|
2616
2632
|
});
|
|
2617
2633
|
this.tmuxNudgeFailureNoticeAt.delete(sessionId);
|
|
@@ -2698,6 +2714,51 @@ class TelegramTransport {
|
|
|
2698
2714
|
});
|
|
2699
2715
|
}
|
|
2700
2716
|
}
|
|
2717
|
+
async notifyTmuxUnavailable(sessionId, session, error) {
|
|
2718
|
+
const binding = await this.bindingStore.getBinding(sessionId);
|
|
2719
|
+
if (!binding) {
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
const nowMs = Date.now();
|
|
2723
|
+
const lastNoticeAt = this.tmuxNudgeFailureNoticeAt.get(sessionId);
|
|
2724
|
+
if (lastNoticeAt &&
|
|
2725
|
+
nowMs - lastNoticeAt < TMUX_NUDGE_FAILURE_NOTICE_COOLDOWN_MS) {
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
this.tmuxNudgeFailureNoticeAt.set(sessionId, nowMs);
|
|
2729
|
+
const sessionLabel = session.label ?? sessionId;
|
|
2730
|
+
const tmuxTarget = session.tmuxTarget ?? "unknown";
|
|
2731
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2732
|
+
try {
|
|
2733
|
+
await this.sendNotification({
|
|
2734
|
+
sessionId,
|
|
2735
|
+
sessionLabel: "TellyMCP",
|
|
2736
|
+
recipient: {
|
|
2737
|
+
telegramChatId: binding.telegramChatId,
|
|
2738
|
+
telegramUserId: binding.telegramUserId,
|
|
2739
|
+
},
|
|
2740
|
+
message: [
|
|
2741
|
+
`⚠️ Автоматический tmux nudge для сессии ${sessionLabel} пропущен.`,
|
|
2742
|
+
"tmux сейчас недоступен на этой машине.",
|
|
2743
|
+
`tmux target: ${tmuxTarget}`,
|
|
2744
|
+
`Ошибка: ${errorMessage}`,
|
|
2745
|
+
"Обычно это значит, что tmux session/server не запущен или недоступен по текущему socket path.",
|
|
2746
|
+
"Запусти tmux и агента внутри него, либо обнови/сними tmux target для этой сессии.",
|
|
2747
|
+
].join("\n"),
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2750
|
+
catch (notifyError) {
|
|
2751
|
+
this.logger.warn("Failed to deliver tmux unavailable notification", {
|
|
2752
|
+
sessionId,
|
|
2753
|
+
tmuxTarget,
|
|
2754
|
+
telegramChatId: binding.telegramChatId,
|
|
2755
|
+
telegramUserId: binding.telegramUserId,
|
|
2756
|
+
notifyError: notifyError instanceof Error
|
|
2757
|
+
? (notifyError.stack ?? notifyError.message)
|
|
2758
|
+
: String(notifyError),
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2701
2762
|
async sendTypingForSession(sessionId) {
|
|
2702
2763
|
const binding = await this.bindingStore.getBinding(sessionId);
|
|
2703
2764
|
if (!binding) {
|
|
@@ -4084,9 +4145,14 @@ class TelegramTransport {
|
|
|
4084
4145
|
? (0, relay_1.buildLiveRelaySessionId)(clientUuid, activeSessionId)
|
|
4085
4146
|
: activeSessionId;
|
|
4086
4147
|
const url = new URL(`${baseUrl}/live/${encodeURIComponent(liveSessionId)}`);
|
|
4148
|
+
url.searchParams.set("launchMode", this.config.webapp.launchMode);
|
|
4087
4149
|
await ctx.answerCallbackQuery({ text: "Открываю Live View." });
|
|
4088
|
-
const sent = await this.replyText(ctx, `🖥 Live: ${session?.label ?? activeSessionId}`, { kind: "menu", sessionId: activeSessionId }, {
|
|
4089
|
-
reply_markup:
|
|
4150
|
+
const sent = await this.replyText(ctx, [`🖥 Live: ${session?.label ?? activeSessionId}`, "", "Выбери режим открытия:"].join("\n"), { kind: "menu", sessionId: activeSessionId }, {
|
|
4151
|
+
reply_markup: this.buildLiveViewLaunchKeyboard((mode) => {
|
|
4152
|
+
const modeUrl = new URL(url.toString());
|
|
4153
|
+
modeUrl.searchParams.set("launchMode", mode);
|
|
4154
|
+
return modeUrl.toString();
|
|
4155
|
+
}),
|
|
4090
4156
|
});
|
|
4091
4157
|
this.webAppLaunchRegistry.set(principal.telegramUserId, activeSessionId, this.config.webapp.initDataTtlSeconds, {
|
|
4092
4158
|
telegramChatId: principal.telegramChatId,
|
|
@@ -4113,7 +4179,28 @@ class TelegramTransport {
|
|
|
4113
4179
|
const liveSessionId = canUseRelay
|
|
4114
4180
|
? (0, relay_1.buildLiveRelaySessionId)(input.targetClientUuid, input.targetLocalSessionId, input.sourceClientUuid)
|
|
4115
4181
|
: (input.targetLocalSessionId ?? input.targetSessionId);
|
|
4116
|
-
|
|
4182
|
+
const url = new URL(`${baseUrl}/live/${encodeURIComponent(liveSessionId)}`);
|
|
4183
|
+
url.searchParams.set("launchMode", input.launchMode ?? this.config.webapp.launchMode);
|
|
4184
|
+
return url.toString();
|
|
4185
|
+
}
|
|
4186
|
+
buildLiveViewLaunchKeyboard(getUrl) {
|
|
4187
|
+
const keyboard = new grammy_1.InlineKeyboard();
|
|
4188
|
+
const modes = [
|
|
4189
|
+
{ mode: "fullscreen", label: "Fullscreen" },
|
|
4190
|
+
{ mode: "expand", label: "Expand" },
|
|
4191
|
+
{ mode: "default", label: "Default" },
|
|
4192
|
+
];
|
|
4193
|
+
for (const [index, { mode, label }] of modes.entries()) {
|
|
4194
|
+
const url = getUrl(mode);
|
|
4195
|
+
if (!url) {
|
|
4196
|
+
continue;
|
|
4197
|
+
}
|
|
4198
|
+
keyboard.webApp(label, url);
|
|
4199
|
+
if (index === 1) {
|
|
4200
|
+
keyboard.row();
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
return keyboard;
|
|
4117
4204
|
}
|
|
4118
4205
|
clearPendingInteractionsForContext(ctx) {
|
|
4119
4206
|
const principal = this.getPrincipalFromContext(ctx);
|
package/package.json
CHANGED