@deadragdoll/tellymcp 0.0.11 → 0.0.12

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.
Files changed (21) hide show
  1. package/README-ru.md +29 -0
  2. package/README.md +28 -0
  3. package/TOOLS.md +26 -0
  4. package/VERSION.md +2 -2
  5. package/dist/services/features/telegram-mcp/gateway-socket.service.js +10 -0
  6. package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +4 -0
  7. package/dist/services/features/telegram-mcp/src/features/browser/model/browserOpenTool.js +1 -1
  8. package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +58 -8
  9. package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +48 -0
  10. package/dist/services/features/telegram-mcp/src/shared/i18n/resources/en.js +5 -1
  11. package/dist/services/features/telegram-mcp/src/shared/i18n/resources/ru.js +5 -1
  12. package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +28 -0
  13. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +37 -0
  14. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportConsoleRegistry.js +13 -7
  15. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportConstructorWiring.js +9 -0
  16. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportMenuShell.js +3 -0
  17. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportPayloadState.js +7 -0
  18. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportTerminalActions.js +231 -34
  19. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportTerminalRuntime.js +61 -6
  20. package/dist/services/features/telegram-mcp/src/shared/lib/terminalPromptDetection.js +200 -28
  21. package/package.json +1 -1
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TransportTerminalActions = void 0;
4
+ const grammy_1 = require("grammy");
4
5
  const client_1 = require("../terminal/client");
6
+ const relay_1 = require("../../../app/webapp/relay");
5
7
  const terminalPromptDetection_1 = require("../../lib/terminalPromptDetection");
6
8
  const transportUtils_1 = require("./transportUtils");
7
9
  const TERMINAL_NUDGE_FAILURE_NOTICE_COOLDOWN_MS = 5 * 60 * 1000;
@@ -11,6 +13,74 @@ class TransportTerminalActions {
11
13
  constructor(host) {
12
14
  this.host = host;
13
15
  }
16
+ buildCapturePreview(capture) {
17
+ return capture
18
+ .split("\n")
19
+ .map((line) => line.trimEnd())
20
+ .filter((line) => line.trim().length > 0)
21
+ .slice(-6);
22
+ }
23
+ buildPromptActionTokens(detection) {
24
+ const actions = [];
25
+ const promptActions = detection.promptActions;
26
+ if (promptActions) {
27
+ actions.push(...promptActions.numberedOptions);
28
+ if (promptActions.hasEnter) {
29
+ actions.push("enter");
30
+ }
31
+ if (promptActions.hasEscape) {
32
+ actions.push("escape");
33
+ }
34
+ }
35
+ return Array.from(new Set(actions));
36
+ }
37
+ buildPromptActionKeyboard(payloadKey, actions) {
38
+ if (actions.length === 0) {
39
+ return null;
40
+ }
41
+ const keyboard = new grammy_1.InlineKeyboard();
42
+ for (const action of actions) {
43
+ const label = action === "enter"
44
+ ? "Enter"
45
+ : action === "escape"
46
+ ? "Esc"
47
+ : action;
48
+ keyboard.text(label, `terminal-prompt-action:${action}:${payloadKey}`);
49
+ }
50
+ return keyboard;
51
+ }
52
+ async sendPromptAction(sessionId, action) {
53
+ if (action === "enter" || action === "escape") {
54
+ await this.host.callGatewayJson("/live/action", {
55
+ session_id: sessionId,
56
+ action,
57
+ });
58
+ return;
59
+ }
60
+ await this.host.callGatewayJson("/live/action", {
61
+ session_id: sessionId,
62
+ action: "text",
63
+ text: action,
64
+ });
65
+ }
66
+ isExpectedRelayCaptureMiss(error) {
67
+ const message = error instanceof Error ? error.message : String(error);
68
+ return (/\bInvalid live relay view response\b/u.test(message) ||
69
+ /\bis not connected\b/u.test(message) ||
70
+ /\brequest is rejected\b/u.test(message) ||
71
+ /\brelay live capture did not return terminal content\b/u.test(message));
72
+ }
73
+ buildRelayTerminalTarget(sessionId) {
74
+ const relay = (0, relay_1.parseLiveRelaySessionId)(sessionId);
75
+ if (!relay) {
76
+ return null;
77
+ }
78
+ return `relay:${relay.clientUuid}/${relay.localSessionId}`;
79
+ }
80
+ extractTerminalTextFromGatewayMarkdown(markdownContent) {
81
+ const match = markdownContent.match(/```text\n([\s\S]*?)\n```/u);
82
+ return match?.[1] ?? markdownContent;
83
+ }
14
84
  async ensurePtyTargetForSession(sessionId, session) {
15
85
  const ensuredTarget = (0, client_1.ensureTerminalTargetForSession)(this.host.config.terminal, {
16
86
  sessionId,
@@ -243,17 +313,39 @@ class TransportTerminalActions {
243
313
  }
244
314
  }
245
315
  async scanPromptForSession(session) {
246
- if (!session.terminalTarget) {
316
+ const relayTerminalTarget = this.buildRelayTerminalTarget(session.sessionId);
317
+ if (!session.terminalTarget && !relayTerminalTarget) {
318
+ this.host.logger.debug("terminal prompt scan skipped", {
319
+ sessionId: session.sessionId,
320
+ sessionLabel: session.label,
321
+ isRelaySession: relayTerminalTarget !== null,
322
+ skipReason: "no_terminal_target",
323
+ });
247
324
  this.host.terminalPromptNoticeState.delete(session.sessionId);
248
325
  return;
249
326
  }
250
327
  const binding = await this.host.bindingStore.getBinding(session.sessionId);
251
328
  if (!binding) {
329
+ this.host.logger.debug("terminal prompt scan skipped", {
330
+ sessionId: session.sessionId,
331
+ sessionLabel: session.label,
332
+ isRelaySession: relayTerminalTarget !== null,
333
+ terminalTarget: session.terminalTarget ?? relayTerminalTarget,
334
+ skipReason: "no_binding",
335
+ });
252
336
  this.host.terminalPromptNoticeState.delete(session.sessionId);
253
337
  return;
254
338
  }
255
- let terminalTarget = session.terminalTarget;
339
+ let terminalTarget = session.terminalTarget ?? relayTerminalTarget;
256
340
  let capture;
341
+ this.host.logger.debug("terminal prompt scan started", {
342
+ sessionId: session.sessionId,
343
+ sessionLabel: session.label,
344
+ isRelaySession: relayTerminalTarget !== null,
345
+ terminalTarget,
346
+ strategy: this.host.config.terminal.promptScanStrategy,
347
+ minScore: this.host.config.terminal.promptScanMinScore,
348
+ });
257
349
  try {
258
350
  capture = await this.capturePromptBuffer(session);
259
351
  }
@@ -261,6 +353,7 @@ class TransportTerminalActions {
261
353
  if ((0, client_1.isTerminalUnavailableError)(error)) {
262
354
  this.host.logger.debug("terminal prompt scan skipped because terminal is unavailable", {
263
355
  sessionId: session.sessionId,
356
+ sessionLabel: session.label,
264
357
  terminalTarget,
265
358
  });
266
359
  return;
@@ -270,6 +363,7 @@ class TransportTerminalActions {
270
363
  if (!recoveredTarget) {
271
364
  this.host.logger.debug("terminal prompt scan skipped because target is invalid", {
272
365
  sessionId: session.sessionId,
366
+ sessionLabel: session.label,
273
367
  terminalTarget,
274
368
  });
275
369
  return;
@@ -281,6 +375,19 @@ class TransportTerminalActions {
281
375
  });
282
376
  }
283
377
  else {
378
+ if (relayTerminalTarget &&
379
+ this.isExpectedRelayCaptureMiss(error)) {
380
+ this.host.logger.debug("terminal prompt scan skipped because relay capture is unavailable", {
381
+ sessionId: session.sessionId,
382
+ sessionLabel: session.label,
383
+ terminalTarget,
384
+ error: error instanceof Error
385
+ ? (error.stack ?? error.message)
386
+ : String(error),
387
+ });
388
+ this.host.terminalPromptNoticeState.delete(session.sessionId);
389
+ return;
390
+ }
284
391
  this.host.logger.warn("terminal prompt scan capture failed", {
285
392
  sessionId: session.sessionId,
286
393
  terminalTarget,
@@ -291,6 +398,15 @@ class TransportTerminalActions {
291
398
  return;
292
399
  }
293
400
  }
401
+ const capturePreview = this.buildCapturePreview(capture);
402
+ this.host.logger.debug("terminal prompt buffer captured", {
403
+ sessionId: session.sessionId,
404
+ sessionLabel: session.label,
405
+ terminalTarget,
406
+ captureChars: capture.length,
407
+ captureLines: capture.split("\n").length,
408
+ previewTail: capturePreview,
409
+ });
294
410
  const detection = (0, terminalPromptDetection_1.detectTerminalInteractivePrompt)(capture, {
295
411
  strategy: this.host.config.terminal.promptScanStrategy,
296
412
  minScore: this.host.config.terminal.promptScanMinScore,
@@ -298,9 +414,11 @@ class TransportTerminalActions {
298
414
  if (!detection) {
299
415
  this.host.logger.debug("terminal prompt scan found no interactive prompt", {
300
416
  sessionId: session.sessionId,
417
+ sessionLabel: session.label,
301
418
  terminalTarget,
302
419
  strategy: this.host.config.terminal.promptScanStrategy,
303
420
  minScore: this.host.config.terminal.promptScanMinScore,
421
+ previewTail: capturePreview,
304
422
  });
305
423
  this.host.terminalPromptNoticeState.delete(session.sessionId);
306
424
  return;
@@ -308,9 +426,35 @@ class TransportTerminalActions {
308
426
  if (!this.shouldSendPromptNotice(session.sessionId, detection)) {
309
427
  return;
310
428
  }
311
- await this.notifyPromptDetected(session, binding, detection, terminalTarget);
429
+ await this.notifyPromptDetected(session, binding, detection, terminalTarget ?? relayTerminalTarget ?? "unknown");
312
430
  }
313
431
  async capturePromptBuffer(session) {
432
+ const relay = (0, relay_1.parseLiveRelaySessionId)(session.sessionId);
433
+ if (relay) {
434
+ this.host.logger.debug("terminal prompt relay capture requested", {
435
+ sessionId: session.sessionId,
436
+ clientUuid: relay.clientUuid,
437
+ localSessionId: relay.localSessionId,
438
+ });
439
+ const output = await this.host.callGatewayJson("/live/capture-buffer", {
440
+ session_id: session.sessionId,
441
+ scope: {
442
+ mode: "visible",
443
+ },
444
+ });
445
+ const markdownContent = typeof output.markdown_content === "string"
446
+ ? output.markdown_content
447
+ : "";
448
+ if (!markdownContent.trim()) {
449
+ this.host.logger.warn("terminal prompt relay capture returned empty content", {
450
+ sessionId: session.sessionId,
451
+ clientUuid: relay.clientUuid,
452
+ localSessionId: relay.localSessionId,
453
+ });
454
+ throw new Error("relay live capture did not return terminal content");
455
+ }
456
+ return this.extractTerminalTextFromGatewayMarkdown(markdownContent);
457
+ }
314
458
  const target = session.terminalTarget;
315
459
  if (!target) {
316
460
  throw new Error("terminal target is not configured");
@@ -322,23 +466,18 @@ class TransportTerminalActions {
322
466
  }
323
467
  shouldSendPromptNotice(sessionId, detection) {
324
468
  const existing = this.host.terminalPromptNoticeState.get(sessionId);
325
- const nowMs = Date.now();
326
- const cooldownMs = this.host.config.terminal.promptScanCooldownSeconds * 1000;
327
- if (existing &&
328
- existing.fingerprint === detection.fingerprint &&
329
- nowMs - existing.sentAtMs < cooldownMs) {
330
- this.host.logger.debug("terminal prompt detected but notification is on cooldown", {
469
+ if (existing && existing.fingerprint === detection.fingerprint) {
470
+ this.host.logger.debug("terminal prompt detected but fingerprint is unchanged", {
331
471
  sessionId,
332
472
  fingerprint: detection.fingerprint,
333
473
  score: detection.score,
334
474
  reasons: detection.reasons,
335
- cooldownSeconds: this.host.config.terminal.promptScanCooldownSeconds,
336
475
  });
337
476
  return false;
338
477
  }
339
478
  this.host.terminalPromptNoticeState.set(sessionId, {
340
479
  fingerprint: detection.fingerprint,
341
- sentAtMs: nowMs,
480
+ sentAtMs: Date.now(),
342
481
  });
343
482
  return true;
344
483
  }
@@ -348,30 +487,25 @@ class TransportTerminalActions {
348
487
  }
349
488
  const locale = await this.host.resolveLocaleForTelegramUserId(binding.telegramUserId);
350
489
  const sessionLabel = session.label ?? session.sessionId;
351
- const excerpt = detection.matchedLines
352
- .slice(-TERMINAL_PROMPT_SCAN_MATCHED_LINES_LIMIT)
353
- .join("\n");
354
- await this.host.sendNotification({
490
+ const excerpt = detection.excerpt
491
+ .split("\n")
492
+ .slice(-Math.max(TERMINAL_PROMPT_SCAN_MATCHED_LINES_LIMIT + 2, 8))
493
+ .join("\n") || detection.matchedLines.slice(-TERMINAL_PROMPT_SCAN_MATCHED_LINES_LIMIT).join("\n");
494
+ const promptActionTokens = this.buildPromptActionTokens(detection);
495
+ const payloadKey = promptActionTokens.length > 0
496
+ ? await this.host.createTerminalPromptActionPayload(session.sessionId, promptActionTokens)
497
+ : null;
498
+ const replyMarkup = payloadKey
499
+ ? this.buildPromptActionKeyboard(payloadKey, promptActionTokens)
500
+ : null;
501
+ await this.host.sendChatMessage(binding.telegramChatId, [
502
+ this.host.t(locale, "menu:notices.terminal.prompt_detected_title", {
503
+ sessionName: sessionLabel,
504
+ }),
505
+ excerpt,
506
+ ].join("\n"), replyMarkup ? { reply_markup: replyMarkup } : {}, {
507
+ kind: "notification",
355
508
  sessionId: session.sessionId,
356
- sessionLabel: "TellyMCP",
357
- recipient: {
358
- telegramChatId: binding.telegramChatId,
359
- telegramUserId: binding.telegramUserId,
360
- },
361
- message: [
362
- this.host.t(locale, "menu:notices.terminal.prompt_detected_title", {
363
- sessionName: sessionLabel,
364
- }),
365
- this.host.t(locale, "menu:notices.terminal.prompt_detected_score", {
366
- score: detection.score,
367
- }),
368
- this.host.t(locale, "menu:notices.terminal.prompt_detected_target", {
369
- terminalTarget: terminalTarget,
370
- }),
371
- this.host.t(locale, "menu:notices.terminal.prompt_detected_hint"),
372
- this.host.t(locale, "menu:notices.terminal.prompt_detected_excerpt"),
373
- excerpt,
374
- ].join("\n"),
375
509
  });
376
510
  try {
377
511
  await this.host.sendLiveViewLauncherMessage({
@@ -407,6 +541,69 @@ class TransportTerminalActions {
407
541
  excerpt,
408
542
  });
409
543
  }
544
+ async handlePromptActionCallback(ctx) {
545
+ const locale = await this.host.resolveLocaleForContext(ctx);
546
+ const data = ctx.callbackQuery?.data ?? "";
547
+ const match = data.match(/^terminal-prompt-action:(\d+|enter|escape):(.+)$/u);
548
+ if (!match) {
549
+ await ctx.answerCallbackQuery({
550
+ text: this.host.t(locale, "menu:notices.terminal.prompt_action_invalid"),
551
+ show_alert: true,
552
+ });
553
+ return;
554
+ }
555
+ const [, actionRaw, payloadKeyRaw] = match;
556
+ const payloadKey = payloadKeyRaw?.trim();
557
+ const action = actionRaw?.trim().toLowerCase();
558
+ if (!payloadKey || !action) {
559
+ await ctx.answerCallbackQuery({
560
+ text: this.host.t(locale, "menu:notices.terminal.prompt_action_invalid"),
561
+ show_alert: true,
562
+ });
563
+ return;
564
+ }
565
+ const payload = await this.host.getMenuPayloadByKey(payloadKey);
566
+ const storedActions = Array.isArray(payload?.promptActions)
567
+ ? payload.promptActions
568
+ .map((value) => (typeof value === "string" ? value.trim().toLowerCase() : ""))
569
+ .filter((value) => value.length > 0)
570
+ : [];
571
+ if (!payload ||
572
+ payload.kind !== "terminal-prompt-action" ||
573
+ !payload.sessionId ||
574
+ !storedActions.includes(action)) {
575
+ await ctx.answerCallbackQuery({
576
+ text: this.host.t(locale, "menu:notices.terminal.prompt_action_stale"),
577
+ show_alert: true,
578
+ });
579
+ return;
580
+ }
581
+ try {
582
+ await this.sendPromptAction(String(payload.sessionId), action);
583
+ await ctx.answerCallbackQuery({
584
+ text: this.host.t(locale, "menu:notices.terminal.prompt_action_sent", {
585
+ action: action === "enter"
586
+ ? "Enter"
587
+ : action === "escape"
588
+ ? "Esc"
589
+ : action,
590
+ }),
591
+ });
592
+ }
593
+ catch (error) {
594
+ this.host.logger.warn("terminal prompt action callback failed", {
595
+ sessionId: String(payload.sessionId),
596
+ action,
597
+ error: error instanceof Error
598
+ ? (error.stack ?? error.message)
599
+ : String(error),
600
+ });
601
+ await ctx.answerCallbackQuery({
602
+ text: this.host.t(locale, "menu:notices.terminal.prompt_action_failed"),
603
+ show_alert: true,
604
+ });
605
+ }
606
+ }
410
607
  async captureBuffer(session, scope) {
411
608
  const target = session.terminalTarget;
412
609
  if (!target) {
@@ -25,9 +25,35 @@ class TransportTerminalRuntime {
25
25
  }
26
26
  startPromptScan() {
27
27
  if (!this.host.config.terminal.promptScanEnabled) {
28
+ this.host.logger.debug("terminal prompt scan disabled", {
29
+ promptScanEnabled: this.host.config.terminal.promptScanEnabled,
30
+ });
31
+ return;
32
+ }
33
+ if (this.host.config.distributed.mode === "gateway") {
34
+ this.host.logger.info("terminal prompt scan armed for live clients", {
35
+ intervalSeconds: this.host.config.terminal.promptScanIntervalSeconds,
36
+ cooldownSeconds: this.host.config.terminal.promptScanCooldownSeconds,
37
+ strategy: this.host.config.terminal.promptScanStrategy,
38
+ minScore: this.host.config.terminal.promptScanMinScore,
39
+ });
40
+ return;
41
+ }
42
+ this.ensurePromptScanRunning();
43
+ }
44
+ clearTerminalPromptScanTimer() {
45
+ if (this.terminalPromptScanTimer) {
46
+ clearInterval(this.terminalPromptScanTimer);
47
+ this.terminalPromptScanTimer = undefined;
48
+ }
49
+ }
50
+ ensurePromptScanRunning() {
51
+ if (!this.host.config.terminal.promptScanEnabled) {
52
+ return;
53
+ }
54
+ if (this.terminalPromptScanTimer) {
28
55
  return;
29
56
  }
30
- this.clearTerminalPromptScanTimer();
31
57
  const intervalMs = this.host.config.terminal.promptScanIntervalSeconds * 1000;
32
58
  const timer = setInterval(() => {
33
59
  void this.runTerminalPromptScanCycle().catch((error) => {
@@ -54,11 +80,12 @@ class TransportTerminalRuntime {
54
80
  });
55
81
  });
56
82
  }
57
- clearTerminalPromptScanTimer() {
58
- if (this.terminalPromptScanTimer) {
59
- clearInterval(this.terminalPromptScanTimer);
60
- this.terminalPromptScanTimer = undefined;
83
+ pausePromptScan() {
84
+ if (!this.terminalPromptScanTimer) {
85
+ return;
61
86
  }
87
+ this.clearTerminalPromptScanTimer();
88
+ this.host.logger.info("terminal prompt scan paused", {});
62
89
  }
63
90
  scheduleTerminalNudgeForInboxMessage(sessionId, session) {
64
91
  if (!this.host.config.terminal.nudgeEnabled) {
@@ -153,15 +180,43 @@ class TransportTerminalRuntime {
153
180
  }
154
181
  }
155
182
  async runTerminalPromptScanCycle() {
156
- if (!this.host.config.terminal.promptScanEnabled || this.terminalPromptScanInFlight) {
183
+ if (!this.host.config.terminal.promptScanEnabled) {
184
+ this.host.logger.debug("terminal prompt scan cycle skipped", {
185
+ skipReason: "disabled",
186
+ });
187
+ return;
188
+ }
189
+ if (this.terminalPromptScanInFlight) {
190
+ this.host.logger.debug("terminal prompt scan cycle skipped", {
191
+ skipReason: "in_flight",
192
+ });
157
193
  return;
158
194
  }
159
195
  this.terminalPromptScanInFlight = true;
160
196
  try {
197
+ if (this.host.ensureGatewayScopedConsolesBoundForPrincipal) {
198
+ const principals = await this.host.bindingStore.listBoundPrincipals();
199
+ this.host.logger.debug("terminal prompt scan syncing bound gateway principals", {
200
+ principalCount: principals.length,
201
+ });
202
+ for (const principal of principals) {
203
+ await this.host.ensureGatewayScopedConsolesBoundForPrincipal(principal);
204
+ }
205
+ }
161
206
  const sessions = await this.host.sessionStore.listSessions();
207
+ this.host.logger.debug("terminal prompt scan cycle started", {
208
+ sessionCount: sessions.length,
209
+ });
210
+ if (sessions.length === 0) {
211
+ this.host.logger.debug("terminal prompt scan cycle found no sessions");
212
+ return;
213
+ }
162
214
  for (const session of sessions) {
163
215
  await this.actions.scanPromptForSession(session);
164
216
  }
217
+ this.host.logger.debug("terminal prompt scan cycle completed", {
218
+ sessionCount: sessions.length,
219
+ });
165
220
  }
166
221
  finally {
167
222
  this.terminalPromptScanInFlight = false;