@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
package/README-ru.md CHANGED
@@ -272,6 +272,35 @@ tellymcp browser install
272
272
 
273
273
  Не подменяй browser workflow ad hoc shell-командами с Playwright, кроме случаев, когда ты отлаживаешь сам browser runtime.
274
274
 
275
+ ## Terminal Blockers
276
+
277
+ Gateway prompt scanner теперь живёт от live-client lifecycle:
278
+
279
+ - на старте gateway scanner только armed, но не крутится вхолостую
280
+ - реальная работа начинается после подключения live client
281
+ - relay console materialization идёт из `hello` и owner-route hydration, а не из `/menu`
282
+ - детект работает по хвосту захваченного terminal buffer
283
+
284
+ Основная эвристика blocker-а:
285
+
286
+ - подряд идущие numbered choices: `1.`, `2.`, `3.`
287
+ - рядом есть action hints вроде `press`, `input`, `choose`, `enter`, `esc`, `yes`, `no`
288
+ - в Telegram notice попадают и 1-2 строки контекста выше menu block
289
+
290
+ Когда blocker найден, gateway может отправить inline-кнопки:
291
+
292
+ - `1..N`
293
+ - `Enter`
294
+ - `Esc`
295
+
296
+ Эти кнопки отправляют в консоль ровно цифру или terminal action. Навигация маркером не используется.
297
+
298
+ Операционные заметки:
299
+
300
+ - одинаковый blocker fingerprint не перевысылается повторно
301
+ - relay capture miss для offline agent считается debug-only шумом
302
+ - `Storage` и `Screenshots` на gateway теперь relay-aware и читают metadata через gateway routes, а не через filesystem самого gateway
303
+
275
304
  ## Collaboration
276
305
 
277
306
  Проекты:
package/README.md CHANGED
@@ -272,6 +272,34 @@ tellymcp browser install
272
272
 
273
273
  Do not replace browser workflows with ad hoc shell Playwright commands unless you are debugging the runtime itself.
274
274
 
275
+ ## Terminal Blockers
276
+
277
+ Gateway prompt scanning is now live-client driven:
278
+
279
+ - the scanner is armed on gateway startup but starts working only after a live client connects
280
+ - relay console materialization happens from gateway hello/owner-route hydration, not from `/menu`
281
+ - prompt detection works on the tail of the captured terminal buffer
282
+
283
+ Primary blocker heuristic:
284
+
285
+ - contiguous numbered choices like `1.`, `2.`, `3.`
286
+ - nearby action hints such as `press`, `input`, `choose`, `enter`, `esc`, `yes`, `no`
287
+ - optional context lines above the menu block are included in the Telegram notice
288
+
289
+ When a blocker is detected, the gateway can send inline Telegram buttons for:
290
+
291
+ - `1..N`
292
+ - `Enter`
293
+ - `Esc`
294
+
295
+ Those buttons send exactly the digit or terminal action to the target console. No marker navigation is used.
296
+
297
+ Operational notes:
298
+
299
+ - repeated scans of the same blocker fingerprint do not resend the notice
300
+ - relay capture misses for offline agents are treated as debug-only noise
301
+ - `Storage` and `Screenshots` on the gateway are relay-aware and read console metadata through gateway routes instead of the gateway filesystem
302
+
275
303
  ## Collaboration Model
276
304
 
277
305
  Projects:
package/TOOLS.md CHANGED
@@ -38,6 +38,21 @@ Browser runtime rule:
38
38
  - If browser tools fail because the Playwright browser runtime is missing, install it with `tellymcp browser install`.
39
39
  - Do not stop at the installation error itself. Install the browser runtime first, then retry the browser tool.
40
40
 
41
+ Terminal prompt scan rules:
42
+
43
+ - gateway prompt scan is driven by live client lifecycle, not by manual menu entry
44
+ - the scanner works on the tail of the captured terminal buffer
45
+ - the main blocker signal is:
46
+ - a contiguous numbered choice block like `1.`, `2.`, `3.`
47
+ - nearby action-hint language such as `press`, `input`, `choose`, `enter`, `esc`, `yes`, `no`
48
+ - exact footer text is helpful but not required
49
+ - when a blocker notice contains inline buttons, those buttons intentionally send only:
50
+ - a digit `1..N`
51
+ - `Enter`
52
+ - `Esc`
53
+ - do not assume marker navigation
54
+ - do not reinterpret those buttons as alternate hotkeys like `(y)` or `(p)`
55
+
41
56
  Collaboration tools:
42
57
 
43
58
  - `list_gateway_sessions`
@@ -725,6 +740,8 @@ Input:
725
740
 
726
741
  - `session_id?`
727
742
  - `url`
743
+ - `width?`
744
+ - `height?`
728
745
  - `wait_until?`
729
746
  - `reset_context?`
730
747
 
@@ -735,12 +752,19 @@ Output:
735
752
  - `created_context`
736
753
  - `url`
737
754
  - `title?`
755
+ - `viewport_width?`
756
+ - `viewport_height?`
738
757
 
739
758
  Notes:
740
759
 
741
760
  - each session gets its own isolated browser context and page
742
761
  - call this first before reading console, DOM, styles, or screenshots
743
762
  - `url` may be an absolute URL, or a relative path when `BROWSER_ADDRESS` is configured
763
+ - in headed mode the browser window is started maximized by default
764
+ - in headed mode a fresh browser context is created without a fixed viewport unless you explicitly pass `width` and `height`
765
+ - if you want page content to grow and shrink together with the outer browser window, do not pass `width` and `height`
766
+ - if you pass viewport size, pass both `width` and `height` together
767
+ - use explicit `width` and `height` when the task depends on a specific responsive breakpoint or working area
744
768
 
745
769
  ## `browser_console`
746
770
 
@@ -1083,6 +1107,8 @@ Telegram UI summary:
1083
1107
  - default logical console identity comes from `.mcpsession.json` in the workspace or explicit `-s`
1084
1108
  - terminal runtime metadata does not change `session_id`
1085
1109
  - `Browser -> Screenshots` lists screenshots created by `browser_screenshot`
1110
+ - `Storage` and `Screenshots` are relay-aware on the gateway
1111
+ - for relay consoles they read file metadata through gateway routes, not through the gateway filesystem
1086
1112
  - `Storage` browses `.mcp-xchange` for the active console and can send stored notes/files back into Telegram
1087
1113
  - `Settings` contains `Info`, `Rename`, `Unpair`, `Back`
1088
1114
  - project/collab work is the only supported user-facing collaboration path in Telegram UI
package/VERSION.md CHANGED
@@ -100,8 +100,8 @@ For detailed engineering history, refactors, and internal development notes, see
100
100
  - `tellymcp doctor`
101
101
  - `tellymcp mcp --help`
102
102
  - Standalone and public installation guides:
103
- - [STANDALONE.md](STANDALONE.md)
104
- - [STANDALONE-ru.md](STANDALONE-ru.md)
103
+ - [STANDALONE.md](docs/STANDALONE.md)
104
+ - [STANDALONE-ru.md](docs/STANDALONE-ru.md)
105
105
  - Browser runtime helper:
106
106
  - `tellymcp browser install`
107
107
  - Public README set for GitHub and npm:
@@ -1226,6 +1226,13 @@ const TelegramMcpGatewaySocketService = {
1226
1226
  }))
1227
1227
  : [],
1228
1228
  }, { meta: { internal_call: true } });
1229
+ await runtime.telegramTransport.hydrateGatewayClientOwnerRoute({
1230
+ clientUuid: hello.client_uuid,
1231
+ ...(hello.gateway_user_uuid
1232
+ ? { gatewayUserUuid: hello.gateway_user_uuid }
1233
+ : {}),
1234
+ });
1235
+ runtime.telegramTransport.ensurePromptScanRunning();
1229
1236
  }
1230
1237
  const localVersionInfo = this.getLocalVersionInfo?.() ?? {
1231
1238
  packageVersion: "0.0.0-unknown",
@@ -2002,6 +2009,9 @@ const TelegramMcpGatewaySocketService = {
2002
2009
  }
2003
2010
  }
2004
2011
  }
2012
+ if ((this.connectedClientsByUuid?.size ?? 0) === 0) {
2013
+ runtime.telegramTransport.pausePromptScan();
2014
+ }
2005
2015
  this.connectedClientToolsAlerts?.delete(socket);
2006
2016
  this.connectedClients?.delete(socket);
2007
2017
  runtime.logger.warn("Gateway WS client disconnected", {
@@ -194,6 +194,8 @@ exports.clearSessionContextOutputSchema = z.object({
194
194
  exports.browserOpenInputSchema = z.object({
195
195
  session_id: z.string().trim().min(1).optional(),
196
196
  url: z.string().trim().min(1),
197
+ width: z.number().int().positive().max(10000).optional(),
198
+ height: z.number().int().positive().max(10000).optional(),
197
199
  wait_until: z
198
200
  .enum(["load", "domcontentloaded", "networkidle", "commit"])
199
201
  .optional(),
@@ -205,6 +207,8 @@ exports.browserOpenOutputSchema = z.object({
205
207
  created_context: z.boolean(),
206
208
  url: z.string().url(),
207
209
  title: z.string().optional(),
210
+ viewport_width: z.number().int().positive().optional(),
211
+ viewport_height: z.number().int().positive().optional(),
208
212
  });
209
213
  exports.browserReloadInputSchema = z.object({
210
214
  session_id: z.string().trim().min(1).optional(),
@@ -18,7 +18,7 @@ class BrowserOpenTool {
18
18
  register(server) {
19
19
  server.registerTool("browser_open", {
20
20
  title: "Browser Open",
21
- description: "Open a URL in the isolated Playwright browser context for the current session. Reuse the existing tab unless reset_context=true.",
21
+ description: "Open a URL in the isolated Playwright browser context for the current session. Reuse the existing tab unless reset_context=true. Optionally pass width and height to control the viewport.",
22
22
  inputSchema: schema_1.browserOpenInputSchema,
23
23
  outputSchema: schema_1.browserOpenOutputSchema,
24
24
  }, async (args) => {
@@ -47,6 +47,8 @@ function formatConsoleLocation(message) {
47
47
  function escapeCssAttributeValue(value) {
48
48
  return value.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"');
49
49
  }
50
+ const DEFAULT_BROWSER_VIEWPORT_WIDTH = 1720;
51
+ const DEFAULT_BROWSER_VIEWPORT_HEIGHT = 980;
50
52
  class BrowserService {
51
53
  config;
52
54
  sessionStore;
@@ -85,12 +87,26 @@ class BrowserService {
85
87
  }
86
88
  this.ensureEnabled();
87
89
  const existingState = this.sessionStates.get(normalizedSessionId);
88
- const shouldReset = input.reset_context === true;
90
+ const requestedViewport = this.resolveRequestedViewport(input);
91
+ const shouldUpgradeLegacyViewport = this.config.browser.headless === false &&
92
+ input.reset_context !== true &&
93
+ !requestedViewport &&
94
+ Boolean(existingState?.page.viewportSize());
95
+ const shouldReset = input.reset_context === true || shouldUpgradeLegacyViewport;
89
96
  const targetUrl = this.resolveBrowserUrl(input.url);
90
97
  if (shouldReset && existingState) {
91
98
  await this.closeState(normalizedSessionId, existingState);
92
99
  }
93
- const { state, createdContext } = await this.ensureSessionState(normalizedSessionId, shouldReset);
100
+ const { state, createdContext } = await this.ensureSessionState(normalizedSessionId, shouldReset, input);
101
+ if (requestedViewport) {
102
+ await state.page.setViewportSize(requestedViewport);
103
+ }
104
+ else if (this.config.browser.headless !== false) {
105
+ await state.page.setViewportSize({
106
+ width: DEFAULT_BROWSER_VIEWPORT_WIDTH,
107
+ height: DEFAULT_BROWSER_VIEWPORT_HEIGHT,
108
+ });
109
+ }
94
110
  const waitUntil = (input.wait_until ??
95
111
  this.config.browser.waitUntil);
96
112
  await state.page.goto(targetUrl, {
@@ -108,13 +124,18 @@ class BrowserService {
108
124
  createdContext,
109
125
  waitUntil,
110
126
  headless: this.config.browser.headless,
127
+ viewportWidth: state.page.viewportSize()?.width ?? requestedViewport?.width,
128
+ viewportHeight: state.page.viewportSize()?.height ?? requestedViewport?.height,
111
129
  });
130
+ const viewport = state.page.viewportSize();
112
131
  return {
113
132
  session_id: normalizedSessionId,
114
133
  opened: true,
115
134
  created_context: createdContext,
116
135
  url: state.currentUrl,
117
136
  ...(state.title ? { title: state.title } : {}),
137
+ ...(viewport?.width ? { viewport_width: viewport.width } : {}),
138
+ ...(viewport?.height ? { viewport_height: viewport.height } : {}),
118
139
  };
119
140
  }
120
141
  async getConsole(input) {
@@ -690,10 +711,13 @@ class BrowserService {
690
711
  async ensureBrowser() {
691
712
  this.browserPromise ??= (async () => {
692
713
  const playwright = await this.ensurePlaywright();
693
- const launchArgs = this.config.browser.headless === false &&
694
- this.config.browser.devtools === true
695
- ? ["--auto-open-devtools-for-tabs"]
696
- : [];
714
+ const launchArgs = [];
715
+ if (this.config.browser.headless === false) {
716
+ launchArgs.push("--start-maximized");
717
+ if (this.config.browser.devtools === true) {
718
+ launchArgs.push("--auto-open-devtools-for-tabs");
719
+ }
720
+ }
697
721
  const browser = await playwright.chromium.launch({
698
722
  headless: this.config.browser.headless,
699
723
  slowMo: this.config.browser.slowMoMs,
@@ -717,13 +741,13 @@ class BrowserService {
717
741
  })();
718
742
  return this.browserPromise;
719
743
  }
720
- async ensureSessionState(sessionId, forceNewContext) {
744
+ async ensureSessionState(sessionId, forceNewContext, openInput) {
721
745
  const existing = this.sessionStates.get(sessionId);
722
746
  if (existing && !forceNewContext) {
723
747
  return { state: existing, createdContext: false };
724
748
  }
725
749
  const browser = await this.ensureBrowser();
726
- const context = await browser.newContext();
750
+ const context = await browser.newContext(this.buildContextOptions(openInput));
727
751
  const page = await context.newPage();
728
752
  const createdAt = new Date().toISOString();
729
753
  const state = {
@@ -763,6 +787,32 @@ class BrowserService {
763
787
  this.sessionStates.set(sessionId, state);
764
788
  return { state, createdContext: true };
765
789
  }
790
+ resolveRequestedViewport(input) {
791
+ const width = input?.width;
792
+ const height = input?.height;
793
+ if (!width && !height) {
794
+ return null;
795
+ }
796
+ if (!width || !height) {
797
+ throw new Error("Browser viewport requires both width and height together.");
798
+ }
799
+ return { width, height };
800
+ }
801
+ buildContextOptions(input) {
802
+ const requestedViewport = this.resolveRequestedViewport(input);
803
+ if (requestedViewport) {
804
+ return {
805
+ viewport: requestedViewport,
806
+ screen: requestedViewport,
807
+ };
808
+ }
809
+ if (this.config.browser.headless === false) {
810
+ return {
811
+ viewport: null,
812
+ };
813
+ }
814
+ return {};
815
+ }
766
816
  recordNetworkFailure(state, request, response) {
767
817
  const failure = request.failure();
768
818
  pushBounded(state.networkFailures, {
@@ -676,6 +676,54 @@ class GatewayHttpService {
676
676
  return true;
677
677
  }
678
678
  }
679
+ if (pathname === "/gateway/live/action") {
680
+ if (req.method !== "POST") {
681
+ writeText(res, 405, "Method not allowed");
682
+ return true;
683
+ }
684
+ try {
685
+ const body = (await this.readJsonBody(req));
686
+ const sessionId = typeof body.session_id === "string" ? body.session_id.trim() : "";
687
+ const action = typeof body.action === "string" ? body.action.trim() : "";
688
+ const text = typeof body.text === "string" ? body.text : "";
689
+ const relayTarget = (0, relay_1.parseLiveRelaySessionId)(sessionId);
690
+ if (!relayTarget) {
691
+ writeJson(res, 400, {
692
+ error: "relay session_id is required",
693
+ });
694
+ return true;
695
+ }
696
+ if (!["enter", "escape", "text"].includes(action)) {
697
+ writeJson(res, 400, {
698
+ error: "action must be one of: enter, escape, text",
699
+ });
700
+ return true;
701
+ }
702
+ if (action === "text" && (!text || text.length > 16)) {
703
+ writeJson(res, 400, {
704
+ error: "text payload is required and must be <= 16 characters",
705
+ });
706
+ return true;
707
+ }
708
+ await this.requestLiveRelayAction({
709
+ clientUuid: relayTarget.clientUuid,
710
+ localSessionId: relayTarget.localSessionId,
711
+ action: action,
712
+ ...(action === "text" ? { text } : {}),
713
+ });
714
+ writeJson(res, 200, { ok: true });
715
+ return true;
716
+ }
717
+ catch (error) {
718
+ console.error("Gateway live action request failed", {
719
+ error: error instanceof Error ? (error.stack ?? error.message) : String(error),
720
+ });
721
+ writeJson(res, 400, {
722
+ error: error instanceof Error ? error.message : String(error),
723
+ });
724
+ return true;
725
+ }
726
+ }
679
727
  if (pathname === "/gateway/client/register") {
680
728
  if (req.method !== "POST") {
681
729
  writeText(res, 405, "Method not allowed");
@@ -396,11 +396,15 @@ exports.enMenu = {
396
396
  unavailable_target: "terminal target: {{terminalTarget}}",
397
397
  unavailable_reason: "This usually means the terminal runtime is not running or is unreachable for the current target.",
398
398
  unavailable_action: "Restart the terminal runtime for this console, or update/remove the terminal target for this session.",
399
- prompt_detected_title: "🛎 The agent in session {{sessionName}} may be waiting for your input.",
399
+ prompt_detected_title: "🛎 Possible blocker in {{sessionName}}",
400
400
  prompt_detected_score: "Detection score: {{score}}",
401
401
  prompt_detected_target: "terminal target: {{terminalTarget}}",
402
402
  prompt_detected_hint: "Open Live or answer in the terminal if this prompt really needs you.",
403
403
  prompt_detected_excerpt: "Recent prompt lines:",
404
+ prompt_action_sent: "Sent {{action}} to terminal.",
405
+ prompt_action_stale: "This blocker action is no longer available.",
406
+ prompt_action_failed: "Failed to send the action to terminal.",
407
+ prompt_action_invalid: "Invalid blocker action.",
404
408
  },
405
409
  },
406
410
  admin: {
@@ -396,11 +396,15 @@ exports.ruMenu = {
396
396
  unavailable_target: "terminal target: {{terminalTarget}}",
397
397
  unavailable_reason: "Обычно это значит, что терминальный runtime не запущен или недоступен для текущего target.",
398
398
  unavailable_action: "Перезапусти терминальный runtime для этой консоли или обнови/сними terminal target для этой сессии.",
399
- prompt_detected_title: "🛎 Похоже, агент в сессии {{sessionName}} ждёт твой ввод.",
399
+ prompt_detected_title: "🛎 Возможный блокер в {{sessionName}}",
400
400
  prompt_detected_score: "Сила срабатывания: {{score}}",
401
401
  prompt_detected_target: "terminal target: {{terminalTarget}}",
402
402
  prompt_detected_hint: "Открой Live или ответь в терминале, если этот prompt действительно требует тебя.",
403
403
  prompt_detected_excerpt: "Последние строки prompt:",
404
+ prompt_action_sent: "Отправил {{action}} в терминал.",
405
+ prompt_action_stale: "Это действие для блокера уже недоступно.",
406
+ prompt_action_failed: "Не удалось отправить действие в терминал.",
407
+ prompt_action_invalid: "Некорректное действие блокера.",
404
408
  },
405
409
  },
406
410
  admin: {
@@ -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
  }
@@ -204,6 +207,31 @@ class RedisStateStore {
204
207
  async listBoundSessionIdsForPrincipal(principal) {
205
208
  return this.redis.smembers(principalSessionsKey(principal));
206
209
  }
210
+ async listBoundPrincipals() {
211
+ const principals = new Map();
212
+ let cursor = "0";
213
+ do {
214
+ const [nextCursor, keys] = await this.redis.scan(cursor, "MATCH", bindingMatchPattern(), "COUNT", 100);
215
+ cursor = nextCursor;
216
+ for (const key of keys) {
217
+ const raw = await this.redis.get(key);
218
+ const binding = parseJson(raw);
219
+ if (!binding) {
220
+ continue;
221
+ }
222
+ const telegramChatId = Number(binding.telegramChatId);
223
+ const telegramUserId = Number(binding.telegramUserId);
224
+ if (!Number.isFinite(telegramChatId) || !Number.isFinite(telegramUserId)) {
225
+ continue;
226
+ }
227
+ principals.set(`${telegramChatId}:${telegramUserId}`, {
228
+ telegramChatId,
229
+ telegramUserId,
230
+ });
231
+ }
232
+ } while (cursor !== "0");
233
+ return Array.from(principals.values());
234
+ }
207
235
  async resetRuntimeState() {
208
236
  await this.redis.del(activeRequestKey());
209
237
  await this.redis.del(queueKey());
@@ -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
- ctx: input.ctx,
58
- principal: input.principal,
59
- relaySessionId,
60
- consoleSession,
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.from?.username
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),
@@ -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",