@deadragdoll/tellymcp 0.0.11 → 0.0.13
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 +48 -0
- package/README.md +47 -0
- package/TOOLS.md +232 -3
- package/VERSION.md +2 -2
- package/dist/cli.js +109 -1
- package/dist/services/features/telegram-mcp/browser.service.js +38 -1
- package/dist/services/features/telegram-mcp/gateway-socket.service.js +10 -0
- package/dist/services/features/telegram-mcp/mcp-server.service.js +12 -0
- package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +14 -0
- package/dist/services/features/telegram-mcp/src/app/config/env.js +13 -0
- package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +151 -2
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserClickTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserDomTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserFillTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserInjectScriptTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserListAttachedInstancesTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserListTabsTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserOpenTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserPressTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStartTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStatusTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStopTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserScreenshotTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +543 -9
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/browserRecordingBundle.js +502 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachRegistry.js +79 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js +559 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/types.js +2 -0
- package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +48 -0
- package/dist/services/features/telegram-mcp/src/shared/i18n/resources/en.js +5 -1
- package/dist/services/features/telegram-mcp/src/shared/i18n/resources/ru.js +5 -1
- package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +56 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +37 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportConsoleRegistry.js +13 -7
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportConstructorWiring.js +9 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportMenuShell.js +3 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportPayloadState.js +7 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportTerminalActions.js +231 -34
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportTerminalRuntime.js +61 -6
- package/dist/services/features/telegram-mcp/src/shared/lib/terminalPromptDetection.js +200 -28
- package/docs/STANDALONE-ru.md +42 -6
- package/docs/STANDALONE.md +42 -6
- package/package.json +6 -3
- package/packages/chrome-attach-extension/dist/background.js +1326 -0
- package/packages/chrome-attach-extension/dist/icon.svg +6 -0
- package/packages/chrome-attach-extension/dist/manifest.json +36 -0
- package/packages/chrome-attach-extension/dist/options.html +312 -0
- package/packages/chrome-attach-extension/dist/options.js +593 -0
- package/packages/chrome-attach-extension/dist/popup.html +93 -0
- package/packages/chrome-attach-extension/dist/popup.js +79 -0
- package/packages/chrome-attach-extension/dist/recorder-content.js +83 -0
- package/packages/chrome-attach-extension/dist/recorder-page.js +266 -0
- package/packages/firefox-attach-extension/README.md +13 -0
- package/packages/firefox-attach-extension/dist/background.js +1242 -0
- package/packages/firefox-attach-extension/dist/icon.svg +6 -0
- package/packages/firefox-attach-extension/dist/manifest.json +56 -0
- package/packages/firefox-attach-extension/dist/options.html +312 -0
- package/packages/firefox-attach-extension/dist/options.js +527 -0
- package/packages/firefox-attach-extension/dist/popup.html +93 -0
- package/packages/firefox-attach-extension/dist/popup.js +64 -0
- package/packages/firefox-attach-extension/dist/recorder-content.js +77 -0
- package/packages/firefox-attach-extension/dist/recorder-page.js +302 -0
|
@@ -26,6 +26,9 @@ function principalActiveSessionKey(principal) {
|
|
|
26
26
|
function principalActiveSessionMatchPattern(telegramUserId) {
|
|
27
27
|
return `${KEY_PREFIX}:principal:*:${telegramUserId}:active-session`;
|
|
28
28
|
}
|
|
29
|
+
function bindingMatchPattern() {
|
|
30
|
+
return `${KEY_PREFIX}:binding:*`;
|
|
31
|
+
}
|
|
29
32
|
function menuPayloadKey(key) {
|
|
30
33
|
return `${KEY_PREFIX}:menu-payload:${key}`;
|
|
31
34
|
}
|
|
@@ -41,6 +44,12 @@ function adminAuthorizedMatchPattern() {
|
|
|
41
44
|
function gatewayClientUuidKey() {
|
|
42
45
|
return `${KEY_PREFIX}:gateway:client-uuid`;
|
|
43
46
|
}
|
|
47
|
+
function browserAttachmentKey(sessionId) {
|
|
48
|
+
return `${KEY_PREFIX}:browser-attachment:${sessionId}`;
|
|
49
|
+
}
|
|
50
|
+
function browserRecordingKey(sessionId) {
|
|
51
|
+
return `${KEY_PREFIX}:browser-recording:${sessionId}`;
|
|
52
|
+
}
|
|
44
53
|
function outgoingDeliveryNoticeKey(deliveryUuid) {
|
|
45
54
|
return `${KEY_PREFIX}:gateway:outgoing-delivery:${deliveryUuid}`;
|
|
46
55
|
}
|
|
@@ -100,6 +109,8 @@ class RedisStateStore {
|
|
|
100
109
|
await this.clearXchangeFileMetas(sessionId);
|
|
101
110
|
await this.clearProjectMenuViewStates(sessionId);
|
|
102
111
|
await this.clearOutgoingDeliveryNoticesForSession(sessionId);
|
|
112
|
+
await this.clearBrowserAttachment(sessionId);
|
|
113
|
+
await this.clearBrowserRecording(sessionId);
|
|
103
114
|
await this.sessionAdapter.delete(sessionKey(sessionId));
|
|
104
115
|
}
|
|
105
116
|
async createPairCode(record, ttlSeconds) {
|
|
@@ -204,6 +215,31 @@ class RedisStateStore {
|
|
|
204
215
|
async listBoundSessionIdsForPrincipal(principal) {
|
|
205
216
|
return this.redis.smembers(principalSessionsKey(principal));
|
|
206
217
|
}
|
|
218
|
+
async listBoundPrincipals() {
|
|
219
|
+
const principals = new Map();
|
|
220
|
+
let cursor = "0";
|
|
221
|
+
do {
|
|
222
|
+
const [nextCursor, keys] = await this.redis.scan(cursor, "MATCH", bindingMatchPattern(), "COUNT", 100);
|
|
223
|
+
cursor = nextCursor;
|
|
224
|
+
for (const key of keys) {
|
|
225
|
+
const raw = await this.redis.get(key);
|
|
226
|
+
const binding = parseJson(raw);
|
|
227
|
+
if (!binding) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const telegramChatId = Number(binding.telegramChatId);
|
|
231
|
+
const telegramUserId = Number(binding.telegramUserId);
|
|
232
|
+
if (!Number.isFinite(telegramChatId) || !Number.isFinite(telegramUserId)) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
principals.set(`${telegramChatId}:${telegramUserId}`, {
|
|
236
|
+
telegramChatId,
|
|
237
|
+
telegramUserId,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
} while (cursor !== "0");
|
|
241
|
+
return Array.from(principals.values());
|
|
242
|
+
}
|
|
207
243
|
async resetRuntimeState() {
|
|
208
244
|
await this.redis.del(activeRequestKey());
|
|
209
245
|
await this.redis.del(queueKey());
|
|
@@ -312,6 +348,26 @@ class RedisStateStore {
|
|
|
312
348
|
async setGatewayClientUuid(clientUuid) {
|
|
313
349
|
await this.redis.set(gatewayClientUuidKey(), clientUuid);
|
|
314
350
|
}
|
|
351
|
+
async getBrowserAttachment(sessionId) {
|
|
352
|
+
const raw = await this.redis.get(browserAttachmentKey(sessionId));
|
|
353
|
+
return parseJson(raw);
|
|
354
|
+
}
|
|
355
|
+
async setBrowserAttachment(record) {
|
|
356
|
+
await this.redis.set(browserAttachmentKey(record.sessionId), JSON.stringify(record));
|
|
357
|
+
}
|
|
358
|
+
async clearBrowserAttachment(sessionId) {
|
|
359
|
+
await this.redis.del(browserAttachmentKey(sessionId));
|
|
360
|
+
}
|
|
361
|
+
async getBrowserRecording(sessionId) {
|
|
362
|
+
const raw = await this.redis.get(browserRecordingKey(sessionId));
|
|
363
|
+
return parseJson(raw);
|
|
364
|
+
}
|
|
365
|
+
async setBrowserRecording(record) {
|
|
366
|
+
await this.redis.set(browserRecordingKey(record.sessionId), JSON.stringify(record));
|
|
367
|
+
}
|
|
368
|
+
async clearBrowserRecording(sessionId) {
|
|
369
|
+
await this.redis.del(browserRecordingKey(sessionId));
|
|
370
|
+
}
|
|
315
371
|
async setProjectMenuViewState(state) {
|
|
316
372
|
await this.redis.set(projectMenuViewStateKey(state.projectUuid, state.sessionId), JSON.stringify(state));
|
|
317
373
|
}
|
|
@@ -43,6 +43,7 @@ class TelegramTransport {
|
|
|
43
43
|
lifecycleActions;
|
|
44
44
|
attachmentStore;
|
|
45
45
|
broadcastActions;
|
|
46
|
+
consoleRegistry;
|
|
46
47
|
context;
|
|
47
48
|
documentActions;
|
|
48
49
|
eventActions;
|
|
@@ -185,6 +186,7 @@ class TelegramTransport {
|
|
|
185
186
|
this.lifecycleActions = composition.lifecycleActions;
|
|
186
187
|
this.attachmentStore = composition.attachmentStore;
|
|
187
188
|
this.broadcastActions = composition.broadcastActions;
|
|
189
|
+
this.consoleRegistry = composition.consoleRegistry;
|
|
188
190
|
this.context = composition.context;
|
|
189
191
|
this.documentActions = composition.documentActions;
|
|
190
192
|
this.eventActions = composition.eventActions;
|
|
@@ -436,9 +438,44 @@ class TelegramTransport {
|
|
|
436
438
|
async sendStartupNotifications() {
|
|
437
439
|
await this.lifecycleActions.sendStartupNotifications(__dirname);
|
|
438
440
|
}
|
|
441
|
+
ensurePromptScanRunning() {
|
|
442
|
+
this.terminalRuntime.ensurePromptScanRunning();
|
|
443
|
+
}
|
|
444
|
+
pausePromptScan() {
|
|
445
|
+
this.terminalRuntime.pausePromptScan();
|
|
446
|
+
}
|
|
439
447
|
async sendAdminGatewayRegistrationNotifications(input) {
|
|
440
448
|
await this.requestFlow.sendAdminGatewayRegistrationNotifications(input);
|
|
441
449
|
}
|
|
450
|
+
async hydrateGatewayClientOwnerRoute(input) {
|
|
451
|
+
if (this.config.distributed.mode !== "gateway") {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const route = await this.callGatewayJson("/user/route", {
|
|
455
|
+
client_uuid: input.clientUuid,
|
|
456
|
+
...(input.gatewayUserUuid ? { gateway_user_uuid: input.gatewayUserUuid } : {}),
|
|
457
|
+
});
|
|
458
|
+
if (typeof route.telegram_user_id !== "number" ||
|
|
459
|
+
typeof route.telegram_chat_id !== "number") {
|
|
460
|
+
this.logger.debug("Skipping gateway owner route hydration because Telegram route is unavailable", {
|
|
461
|
+
clientUuid: input.clientUuid,
|
|
462
|
+
gatewayUserUuid: input.gatewayUserUuid ?? null,
|
|
463
|
+
});
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
await this.consoleRegistry.ensureScopedConsolesBound({
|
|
467
|
+
principal: {
|
|
468
|
+
telegramChatId: route.telegram_chat_id,
|
|
469
|
+
telegramUserId: route.telegram_user_id,
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
this.logger.info("Gateway owner route hydrated for live client", {
|
|
473
|
+
clientUuid: input.clientUuid,
|
|
474
|
+
gatewayUserUuid: input.gatewayUserUuid ?? null,
|
|
475
|
+
telegramChatId: route.telegram_chat_id,
|
|
476
|
+
telegramUserId: route.telegram_user_id,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
442
479
|
async sendRequest(input) {
|
|
443
480
|
return this.requestFlow.sendRequest(input);
|
|
444
481
|
}
|
|
@@ -53,12 +53,18 @@ class TransportConsoleRegistry {
|
|
|
53
53
|
for (const consoleSession of consoles) {
|
|
54
54
|
const relaySessionId = (0, relay_1.buildLiveRelaySessionId)(consoleSession.client_uuid, consoleSession.local_session_id);
|
|
55
55
|
relaySessionIds.push(relaySessionId);
|
|
56
|
-
await this.materializeRelaySession(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
await this.materializeRelaySession(input.ctx
|
|
57
|
+
? {
|
|
58
|
+
ctx: input.ctx,
|
|
59
|
+
principal: input.principal,
|
|
60
|
+
relaySessionId,
|
|
61
|
+
consoleSession,
|
|
62
|
+
}
|
|
63
|
+
: {
|
|
64
|
+
principal: input.principal,
|
|
65
|
+
relaySessionId,
|
|
66
|
+
consoleSession,
|
|
67
|
+
});
|
|
62
68
|
}
|
|
63
69
|
const existingActive = await this.host.bindingStore.getActiveSessionIdForPrincipal(input.principal);
|
|
64
70
|
const nextBound = new Set(await this.host.bindingStore.listBoundSessionIdsForPrincipal(input.principal));
|
|
@@ -119,7 +125,7 @@ class TransportConsoleRegistry {
|
|
|
119
125
|
sessionId: input.relaySessionId,
|
|
120
126
|
telegramChatId: input.principal.telegramChatId,
|
|
121
127
|
telegramUserId: input.principal.telegramUserId,
|
|
122
|
-
...(input.ctx
|
|
128
|
+
...(input.ctx?.from?.username
|
|
123
129
|
? { telegramUsername: input.ctx.from.username }
|
|
124
130
|
: {}),
|
|
125
131
|
linkedAt: new Date().toISOString(),
|
|
@@ -92,6 +92,11 @@ function buildTransportConstructorWiring(host) {
|
|
|
92
92
|
resolveLocaleForTelegramUserId: (userId) => context.resolveLocaleForTelegramUserId(userId),
|
|
93
93
|
sendNotification: (input) => requestFlow.sendNotification(input),
|
|
94
94
|
sendLiveViewLauncherMessage: (input) => liveActions.sendLauncherMessage(input),
|
|
95
|
+
callGatewayJson: (path, payload) => host.callGatewayJson(path, payload),
|
|
96
|
+
createTerminalPromptActionPayload: (sessionId, actions) => payloadState.createTerminalPromptActionPayload(sessionId, actions),
|
|
97
|
+
getMenuPayloadByKey: (key) => host.menuPayloadStore.getMenuPayload(key),
|
|
98
|
+
resolveLocaleForContext: (ctx) => context.resolveLocaleForContext(ctx),
|
|
99
|
+
sendChatMessage: (telegramChatId, text, options, meta) => outputActions.sendChatMessage(telegramChatId, text, options, meta),
|
|
95
100
|
t: (locale, key, vars) => context.t(locale, key, vars),
|
|
96
101
|
});
|
|
97
102
|
const terminalRuntime = new transportTerminalRuntime_1.TransportTerminalRuntime({
|
|
@@ -103,6 +108,9 @@ function buildTransportConstructorWiring(host) {
|
|
|
103
108
|
isTelegramEnabled: () => host.config.distributed.mode !== "client" && Boolean(host.config.telegram.botToken?.trim()),
|
|
104
109
|
terminalActions,
|
|
105
110
|
terminalNudgeDebounceTimers: host.terminalNudgeDebounceTimers,
|
|
111
|
+
ensureGatewayScopedConsolesBoundForPrincipal: async (principal) => {
|
|
112
|
+
await consoleRegistry.ensureScopedConsolesBound({ principal });
|
|
113
|
+
},
|
|
106
114
|
});
|
|
107
115
|
const menuFingerprints = new transportMenuFingerprints_1.TransportMenuFingerprints({
|
|
108
116
|
logger: host.logger,
|
|
@@ -560,6 +568,7 @@ function buildTransportConstructorWiring(host) {
|
|
|
560
568
|
handleProjectMemberNoteCallback: (ctx) => projectActions.handleProjectMemberNoteCallback(ctx),
|
|
561
569
|
handleProjectMemberLiveCallback: (ctx) => projectActions.handleProjectMemberLiveCallback(ctx),
|
|
562
570
|
handleLiveApprovalCallback: (ctx) => projectActions.handleLiveApprovalCallback(ctx),
|
|
571
|
+
handleTerminalPromptActionCallback: (ctx) => terminalActions.handlePromptActionCallback(ctx),
|
|
563
572
|
handleProjectDetailCallback: (ctx) => projectActions.handleProjectDetailCallback(ctx),
|
|
564
573
|
handleProjectDeleteCallback: (ctx) => projectActions.handleProjectDeleteCallback(ctx),
|
|
565
574
|
handleProjectLeaveCallback: (ctx) => projectActions.handleProjectLeaveCallback(ctx),
|
package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportMenuShell.js
CHANGED
|
@@ -41,6 +41,9 @@ class TransportMenuShell {
|
|
|
41
41
|
bot.callbackQuery(/^live-approval:(approve|deny):(.+)$/u, async (ctx) => {
|
|
42
42
|
await this.host.handleLiveApprovalCallback(ctx);
|
|
43
43
|
});
|
|
44
|
+
bot.callbackQuery(/^terminal-prompt-action:(\d+|enter|escape):(.+)$/u, async (ctx) => {
|
|
45
|
+
await this.host.handleTerminalPromptActionCallback(ctx);
|
|
46
|
+
});
|
|
44
47
|
bot.callbackQuery(/^project-detail:(.+)$/u, async (ctx) => {
|
|
45
48
|
await this.host.handleProjectDetailCallback(ctx);
|
|
46
49
|
});
|
|
@@ -77,6 +77,13 @@ class TransportPayloadState {
|
|
|
77
77
|
...(input.projectName ? { projectName: input.projectName } : {}),
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
|
+
async createTerminalPromptActionPayload(sessionId, actions) {
|
|
81
|
+
return this.createPayload({
|
|
82
|
+
kind: "terminal-prompt-action",
|
|
83
|
+
sessionId,
|
|
84
|
+
promptActions: actions,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
80
87
|
async createPartnerFileTargetPayload(sessionId, targetSessionId, title, filePath) {
|
|
81
88
|
return this.createPayload({
|
|
82
89
|
kind: "partner-file-target",
|
|
@@ -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
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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:
|
|
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.
|
|
352
|
-
.
|
|
353
|
-
.
|
|
354
|
-
|
|
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) {
|