@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.
Files changed (62) hide show
  1. package/README-ru.md +48 -0
  2. package/README.md +47 -0
  3. package/TOOLS.md +232 -3
  4. package/VERSION.md +2 -2
  5. package/dist/cli.js +109 -1
  6. package/dist/services/features/telegram-mcp/browser.service.js +38 -1
  7. package/dist/services/features/telegram-mcp/gateway-socket.service.js +10 -0
  8. package/dist/services/features/telegram-mcp/mcp-server.service.js +12 -0
  9. package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +14 -0
  10. package/dist/services/features/telegram-mcp/src/app/config/env.js +13 -0
  11. package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +151 -2
  12. package/dist/services/features/telegram-mcp/src/features/browser/model/browserClickTool.js +1 -1
  13. package/dist/services/features/telegram-mcp/src/features/browser/model/browserDomTool.js +1 -1
  14. package/dist/services/features/telegram-mcp/src/features/browser/model/browserFillTool.js +1 -1
  15. package/dist/services/features/telegram-mcp/src/features/browser/model/browserInjectScriptTool.js +28 -0
  16. package/dist/services/features/telegram-mcp/src/features/browser/model/browserListAttachedInstancesTool.js +33 -0
  17. package/dist/services/features/telegram-mcp/src/features/browser/model/browserListTabsTool.js +33 -0
  18. package/dist/services/features/telegram-mcp/src/features/browser/model/browserOpenTool.js +1 -1
  19. package/dist/services/features/telegram-mcp/src/features/browser/model/browserPressTool.js +1 -1
  20. package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStartTool.js +28 -0
  21. package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStatusTool.js +28 -0
  22. package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStopTool.js +28 -0
  23. package/dist/services/features/telegram-mcp/src/features/browser/model/browserScreenshotTool.js +1 -1
  24. package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +543 -9
  25. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/browserRecordingBundle.js +502 -0
  26. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachRegistry.js +79 -0
  27. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js +559 -0
  28. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/types.js +2 -0
  29. package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +48 -0
  30. package/dist/services/features/telegram-mcp/src/shared/i18n/resources/en.js +5 -1
  31. package/dist/services/features/telegram-mcp/src/shared/i18n/resources/ru.js +5 -1
  32. package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +56 -0
  33. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +37 -0
  34. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportConsoleRegistry.js +13 -7
  35. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportConstructorWiring.js +9 -0
  36. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportMenuShell.js +3 -0
  37. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportPayloadState.js +7 -0
  38. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportTerminalActions.js +231 -34
  39. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportTerminalRuntime.js +61 -6
  40. package/dist/services/features/telegram-mcp/src/shared/lib/terminalPromptDetection.js +200 -28
  41. package/docs/STANDALONE-ru.md +42 -6
  42. package/docs/STANDALONE.md +42 -6
  43. package/package.json +6 -3
  44. package/packages/chrome-attach-extension/dist/background.js +1326 -0
  45. package/packages/chrome-attach-extension/dist/icon.svg +6 -0
  46. package/packages/chrome-attach-extension/dist/manifest.json +36 -0
  47. package/packages/chrome-attach-extension/dist/options.html +312 -0
  48. package/packages/chrome-attach-extension/dist/options.js +593 -0
  49. package/packages/chrome-attach-extension/dist/popup.html +93 -0
  50. package/packages/chrome-attach-extension/dist/popup.js +79 -0
  51. package/packages/chrome-attach-extension/dist/recorder-content.js +83 -0
  52. package/packages/chrome-attach-extension/dist/recorder-page.js +266 -0
  53. package/packages/firefox-attach-extension/README.md +13 -0
  54. package/packages/firefox-attach-extension/dist/background.js +1242 -0
  55. package/packages/firefox-attach-extension/dist/icon.svg +6 -0
  56. package/packages/firefox-attach-extension/dist/manifest.json +56 -0
  57. package/packages/firefox-attach-extension/dist/options.html +312 -0
  58. package/packages/firefox-attach-extension/dist/options.js +527 -0
  59. package/packages/firefox-attach-extension/dist/popup.html +93 -0
  60. package/packages/firefox-attach-extension/dist/popup.js +64 -0
  61. package/packages/firefox-attach-extension/dist/recorder-content.js +77 -0
  62. package/packages/firefox-attach-extension/dist/recorder-page.js +302 -0
@@ -0,0 +1,559 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.FirefoxAttachServer = void 0;
7
+ const node_crypto_1 = require("node:crypto");
8
+ const ws_1 = __importDefault(require("ws"));
9
+ const browserRecordingBundle_1 = require("./browserRecordingBundle");
10
+ const firefoxAttachRegistry_1 = require("./firefoxAttachRegistry");
11
+ const wsLib = ws_1.default;
12
+ const WebSocketServer = wsLib.WebSocketServer;
13
+ const WS_OPEN = wsLib.OPEN;
14
+ class FirefoxAttachServer {
15
+ config;
16
+ logger;
17
+ sessionStore;
18
+ maintenanceStore;
19
+ wsServer = null;
20
+ registry = new firefoxAttachRegistry_1.FirefoxAttachRegistry();
21
+ recordingWriter;
22
+ sockets = new Set();
23
+ socketsByInstanceId = new Map();
24
+ pendingTabActions = new Map();
25
+ pendingRecordingControls = new Map();
26
+ activeRecordingsById = new Map();
27
+ constructor(config, logger, sessionStore, maintenanceStore) {
28
+ this.config = config;
29
+ this.logger = logger;
30
+ this.sessionStore = sessionStore;
31
+ this.maintenanceStore = maintenanceStore;
32
+ this.recordingWriter = new browserRecordingBundle_1.BrowserRecordingBundleWriter(config, logger);
33
+ }
34
+ async start() {
35
+ if (!this.config.browser.attach.enabled) {
36
+ return;
37
+ }
38
+ if (this.wsServer) {
39
+ return;
40
+ }
41
+ this.wsServer = new WebSocketServer({
42
+ host: this.config.browser.attach.host,
43
+ port: this.config.browser.attach.port,
44
+ path: this.config.browser.attach.path,
45
+ });
46
+ this.wsServer.on("connection", (socket) => {
47
+ const state = { socket };
48
+ this.sockets.add(state);
49
+ socket.on("message", (payload) => {
50
+ void this.handleMessage(state, payload);
51
+ });
52
+ socket.on("close", () => {
53
+ if (state.instanceId) {
54
+ void this.handleInstanceDisconnect(state.instanceId);
55
+ this.registry.remove(state.instanceId);
56
+ this.socketsByInstanceId.delete(state.instanceId);
57
+ this.logger.info("Firefox attach instance disconnected", {
58
+ instanceId: state.instanceId,
59
+ });
60
+ }
61
+ this.sockets.delete(state);
62
+ });
63
+ socket.on("error", (error) => {
64
+ this.logger.debug("Firefox attach socket error", {
65
+ error: error instanceof Error ? (error.stack ?? error.message) : String(error),
66
+ });
67
+ });
68
+ });
69
+ this.logger.info("Firefox attach WebSocket server started", {
70
+ host: this.config.browser.attach.host,
71
+ port: this.config.browser.attach.port,
72
+ path: this.config.browser.attach.path,
73
+ });
74
+ }
75
+ async stop() {
76
+ if (!this.wsServer) {
77
+ return;
78
+ }
79
+ for (const state of this.sockets) {
80
+ try {
81
+ state.socket.close(1001, "server shutdown");
82
+ }
83
+ catch {
84
+ // ignore
85
+ }
86
+ }
87
+ this.sockets.clear();
88
+ const wsServer = this.wsServer;
89
+ this.wsServer = null;
90
+ await new Promise((resolve, reject) => {
91
+ wsServer.close((error) => {
92
+ if (error) {
93
+ reject(error);
94
+ return;
95
+ }
96
+ resolve();
97
+ });
98
+ });
99
+ this.logger.info("Firefox attach WebSocket server stopped");
100
+ }
101
+ listInstances() {
102
+ return this.registry.listInstances();
103
+ }
104
+ async getRecordingStatus(sessionId) {
105
+ return this.maintenanceStore.getBrowserRecording(sessionId);
106
+ }
107
+ async startRecording(input) {
108
+ const existing = await this.maintenanceStore.getBrowserRecording(input.sessionId);
109
+ if (existing?.status === "recording") {
110
+ if (existing.instanceId !== input.instanceId) {
111
+ throw new Error("Recording is already active in another browser instance.");
112
+ }
113
+ return existing;
114
+ }
115
+ const session = await this.sessionStore.getSession(input.sessionId);
116
+ const resolvedSession = session ||
117
+ {
118
+ sessionId: input.sessionId,
119
+ label: this.config.project.sessionLabel?.trim() || input.sessionId,
120
+ cwd: process.cwd(),
121
+ updatedAt: new Date().toISOString(),
122
+ };
123
+ if (!resolvedSession.cwd?.trim()) {
124
+ throw new Error("Workspace cwd is not registered for this console.");
125
+ }
126
+ if (!session) {
127
+ await this.sessionStore.setSession({
128
+ sessionId: resolvedSession.sessionId,
129
+ ...(resolvedSession.label ? { label: resolvedSession.label } : {}),
130
+ cwd: resolvedSession.cwd,
131
+ updatedAt: resolvedSession.updatedAt,
132
+ });
133
+ this.logger.info("Firefox attach recording synthesized missing session context", {
134
+ sessionId: input.sessionId,
135
+ cwd: resolvedSession.cwd,
136
+ label: resolvedSession.label,
137
+ });
138
+ }
139
+ const recordingId = (0, node_crypto_1.randomUUID)();
140
+ const state = await this.recordingWriter.create({
141
+ session: resolvedSession,
142
+ sessionId: input.sessionId,
143
+ instanceId: input.instanceId,
144
+ tabId: input.tabId,
145
+ tabTitle: input.tabTitle,
146
+ ...(input.tabUrl ? { tabUrl: input.tabUrl } : {}),
147
+ recordingId,
148
+ });
149
+ this.activeRecordingsById.set(recordingId, state);
150
+ await this.maintenanceStore.setBrowserRecording(state.record);
151
+ await this.broadcastRecordingState(state.record);
152
+ try {
153
+ await this.invokeRecordingControl({
154
+ instanceId: input.instanceId,
155
+ tabId: input.tabId,
156
+ recordingId,
157
+ mode: "start",
158
+ });
159
+ }
160
+ catch (error) {
161
+ this.activeRecordingsById.delete(recordingId);
162
+ await this.maintenanceStore.clearBrowserRecording(input.sessionId);
163
+ await this.broadcastRecordingState(null);
164
+ throw error;
165
+ }
166
+ return state.record;
167
+ }
168
+ async stopRecording(input) {
169
+ const existing = await this.maintenanceStore.getBrowserRecording(input.sessionId);
170
+ if (!existing) {
171
+ return null;
172
+ }
173
+ const activeState = this.activeRecordingsById.get(existing.recordingId);
174
+ if (activeState) {
175
+ try {
176
+ await this.invokeRecordingControl({
177
+ instanceId: activeState.record.instanceId,
178
+ tabId: activeState.record.tabId,
179
+ recordingId: activeState.record.recordingId,
180
+ mode: "stop",
181
+ });
182
+ }
183
+ catch {
184
+ // Local writer still finalizes the bundle.
185
+ }
186
+ await this.recordingWriter.appendEvent(activeState, {
187
+ kind: "session_stopped",
188
+ status: "stopped",
189
+ });
190
+ this.activeRecordingsById.delete(existing.recordingId);
191
+ await this.maintenanceStore.setBrowserRecording(activeState.record);
192
+ await this.broadcastRecordingState(activeState.record);
193
+ return activeState.record;
194
+ }
195
+ if (existing.status !== "stopped") {
196
+ const stopped = {
197
+ ...existing,
198
+ status: "stopped",
199
+ stoppedAt: new Date().toISOString(),
200
+ };
201
+ await this.maintenanceStore.setBrowserRecording(stopped);
202
+ await this.broadcastRecordingState(stopped);
203
+ return stopped;
204
+ }
205
+ return existing;
206
+ }
207
+ async invokeTabAction(input) {
208
+ const socketState = this.socketsByInstanceId.get(input.instanceId);
209
+ if (!socketState) {
210
+ throw new Error(`Attached browser instance '${input.instanceId}' is not connected.`);
211
+ }
212
+ const requestId = `tab-action-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
213
+ const result = await new Promise((resolve, reject) => {
214
+ const timer = setTimeout(() => {
215
+ this.pendingTabActions.delete(requestId);
216
+ reject(new Error(`Attached browser action '${input.action}' timed out for instance '${input.instanceId}'.`));
217
+ }, this.config.browser.timeoutMs);
218
+ this.pendingTabActions.set(requestId, {
219
+ resolve,
220
+ reject,
221
+ timer,
222
+ });
223
+ this.sendJson(socketState.socket, {
224
+ type: "tab_action",
225
+ request_id: requestId,
226
+ tab_id: input.tabId,
227
+ action: input.action,
228
+ ...(input.payload ? { payload: input.payload } : {}),
229
+ });
230
+ });
231
+ if (!result.ok) {
232
+ throw new Error(result.error ||
233
+ `Attached browser action '${input.action}' failed for instance '${input.instanceId}'.`);
234
+ }
235
+ return result.result;
236
+ }
237
+ sendJson(socket, payload) {
238
+ if (socket.readyState !== WS_OPEN) {
239
+ return;
240
+ }
241
+ socket.send(JSON.stringify(payload));
242
+ }
243
+ async handleMessage(state, payload) {
244
+ const raw = typeof payload === "string"
245
+ ? payload
246
+ : Buffer.isBuffer(payload)
247
+ ? payload.toString("utf8")
248
+ : String(payload ?? "");
249
+ let message;
250
+ try {
251
+ message = JSON.parse(raw);
252
+ }
253
+ catch {
254
+ this.logger.debug("Firefox attach message ignored because it is not valid JSON");
255
+ return;
256
+ }
257
+ switch (message.type) {
258
+ case "hello":
259
+ await this.handleHello(state, message);
260
+ return;
261
+ case "heartbeat":
262
+ if (state.instanceId) {
263
+ this.registry.touch(state.instanceId);
264
+ }
265
+ return;
266
+ case "list_tabs_result":
267
+ if (state.instanceId) {
268
+ this.registry.setTabs(state.instanceId, message.tabs);
269
+ }
270
+ return;
271
+ case "get_active_tab_result":
272
+ if (state.instanceId) {
273
+ this.registry.setActiveTab(state.instanceId, message.tab);
274
+ }
275
+ return;
276
+ case "active_tab_changed":
277
+ if (state.instanceId) {
278
+ this.registry.setActiveTab(state.instanceId, message.tab);
279
+ }
280
+ return;
281
+ case "tab_updated":
282
+ if (state.instanceId) {
283
+ this.registry.updateTab(state.instanceId, message.tab);
284
+ }
285
+ return;
286
+ case "attach_tab_selected":
287
+ if (state.instanceId) {
288
+ this.registry.updateTab(state.instanceId, {
289
+ ...message.tab,
290
+ active: true,
291
+ });
292
+ await this.persistSelectedTab(state.instanceId, message.tab);
293
+ }
294
+ return;
295
+ case "tab_action_result":
296
+ this.resolvePendingTabAction(message);
297
+ return;
298
+ case "recording_control_result":
299
+ this.resolvePendingRecordingControl(message);
300
+ return;
301
+ case "recording_event":
302
+ if (state.instanceId) {
303
+ await this.handleRecordingEvent(state.instanceId, message.recording_id, message.tab_id, message.event);
304
+ }
305
+ return;
306
+ case "recording_manual_start":
307
+ case "recording_manual_stop":
308
+ case "recording_manual_status":
309
+ if (state.instanceId) {
310
+ await this.handleManualRecordingRequest(state.socket, state.instanceId, message);
311
+ }
312
+ return;
313
+ default:
314
+ return;
315
+ }
316
+ }
317
+ async handleHello(state, message) {
318
+ state.instanceId = message.instance_id;
319
+ this.socketsByInstanceId.set(message.instance_id, state);
320
+ const instance = this.registry.setConnected({
321
+ instanceId: message.instance_id,
322
+ extensionVersion: message.extension_version,
323
+ ...(message.profile_name ? { profileName: message.profile_name } : {}),
324
+ capabilities: ["tabs", "active_tab", "recording"],
325
+ });
326
+ this.sendJson(state.socket, {
327
+ type: "hello_ack",
328
+ ok: true,
329
+ instance_id: message.instance_id,
330
+ capabilities: instance.capabilities,
331
+ ...(this.config.project.sessionId
332
+ ? { session_id: this.config.project.sessionId }
333
+ : {}),
334
+ ...(this.config.project.sessionLabel
335
+ ? { session_label: this.config.project.sessionLabel }
336
+ : {}),
337
+ });
338
+ this.logger.info("Firefox attach instance connected", {
339
+ instanceId: message.instance_id,
340
+ extensionVersion: message.extension_version,
341
+ profileName: message.profile_name,
342
+ });
343
+ this.sendJson(state.socket, {
344
+ type: "list_tabs",
345
+ request_id: "bootstrap-list-tabs",
346
+ });
347
+ this.sendJson(state.socket, {
348
+ type: "get_active_tab",
349
+ request_id: "bootstrap-active-tab",
350
+ });
351
+ await this.sendCurrentRecordingState(state.socket);
352
+ }
353
+ async persistSelectedTab(instanceId, tab) {
354
+ const sessionId = this.config.project.sessionId?.trim();
355
+ if (!sessionId) {
356
+ return;
357
+ }
358
+ await this.maintenanceStore.setBrowserAttachment({
359
+ sessionId,
360
+ backend: "firefox-attached",
361
+ instanceId,
362
+ tabId: tab.tab_id,
363
+ attachedAt: new Date().toISOString(),
364
+ ...(tab.title ? { title: tab.title } : {}),
365
+ ...(tab.url ? { url: tab.url } : {}),
366
+ });
367
+ this.logger.info("Firefox attached tab selected", {
368
+ sessionId,
369
+ instanceId,
370
+ tabId: tab.tab_id,
371
+ title: tab.title,
372
+ url: tab.url,
373
+ });
374
+ }
375
+ resolvePendingTabAction(message) {
376
+ const pending = this.pendingTabActions.get(message.request_id);
377
+ if (!pending) {
378
+ return;
379
+ }
380
+ clearTimeout(pending.timer);
381
+ this.pendingTabActions.delete(message.request_id);
382
+ pending.resolve(message);
383
+ }
384
+ resolvePendingRecordingControl(message) {
385
+ const pending = this.pendingRecordingControls.get(message.request_id);
386
+ if (!pending) {
387
+ return;
388
+ }
389
+ clearTimeout(pending.timer);
390
+ this.pendingRecordingControls.delete(message.request_id);
391
+ pending.resolve(message);
392
+ }
393
+ async invokeRecordingControl(input) {
394
+ const socketState = this.socketsByInstanceId.get(input.instanceId);
395
+ if (!socketState) {
396
+ throw new Error(`Attached browser instance '${input.instanceId}' is not connected.`);
397
+ }
398
+ const requestId = `recording-${input.mode}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
399
+ const result = await new Promise((resolve, reject) => {
400
+ const timer = setTimeout(() => {
401
+ this.pendingRecordingControls.delete(requestId);
402
+ reject(new Error(`Attached browser recording '${input.mode}' timed out for instance '${input.instanceId}'.`));
403
+ }, this.config.browser.timeoutMs);
404
+ this.pendingRecordingControls.set(requestId, {
405
+ resolve,
406
+ reject,
407
+ timer,
408
+ });
409
+ this.sendJson(socketState.socket, input.mode === "start"
410
+ ? {
411
+ type: "recording_start",
412
+ request_id: requestId,
413
+ recording_id: input.recordingId,
414
+ tab_id: input.tabId,
415
+ }
416
+ : {
417
+ type: "recording_stop",
418
+ request_id: requestId,
419
+ recording_id: input.recordingId,
420
+ tab_id: input.tabId,
421
+ });
422
+ });
423
+ if (!result.ok) {
424
+ throw new Error(result.error ||
425
+ `Attached browser recording '${input.mode}' failed for instance '${input.instanceId}'.`);
426
+ }
427
+ }
428
+ async handleRecordingEvent(instanceId, recordingId, tabId, event) {
429
+ const state = this.activeRecordingsById.get(recordingId);
430
+ if (!state) {
431
+ return;
432
+ }
433
+ if (state.record.instanceId !== instanceId || state.record.tabId !== tabId) {
434
+ return;
435
+ }
436
+ await this.recordingWriter.appendEvent(state, event);
437
+ await this.maintenanceStore.setBrowserRecording(state.record);
438
+ }
439
+ async handleInstanceDisconnect(instanceId) {
440
+ const activeStates = Array.from(this.activeRecordingsById.values()).filter((state) => state.record.instanceId === instanceId);
441
+ if (activeStates.length === 0) {
442
+ return;
443
+ }
444
+ for (const state of activeStates) {
445
+ await this.recordingWriter.appendEvent(state, {
446
+ kind: "session_stopped",
447
+ status: "instance_disconnected",
448
+ });
449
+ this.activeRecordingsById.delete(state.record.recordingId);
450
+ await this.maintenanceStore.setBrowserRecording(state.record);
451
+ await this.broadcastRecordingState(state.record);
452
+ }
453
+ }
454
+ buildRecordingStateMessage(record) {
455
+ return {
456
+ type: "recording_state",
457
+ active: record?.status === "recording",
458
+ ...(record ? { recording: this.mapManualRecordingResult(record) } : {}),
459
+ };
460
+ }
461
+ async sendCurrentRecordingState(socket) {
462
+ const sessionId = this.config.project.sessionId?.trim();
463
+ const record = sessionId
464
+ ? await this.maintenanceStore.getBrowserRecording(sessionId)
465
+ : null;
466
+ this.sendJson(socket, this.buildRecordingStateMessage(record));
467
+ }
468
+ async broadcastRecordingState(record) {
469
+ const payload = this.buildRecordingStateMessage(record);
470
+ for (const state of this.sockets) {
471
+ this.sendJson(state.socket, payload);
472
+ }
473
+ }
474
+ mapManualRecordingResult(record) {
475
+ if (!record) {
476
+ return undefined;
477
+ }
478
+ return {
479
+ recording_id: record.recordingId,
480
+ instance_id: record.instanceId,
481
+ tab_id: record.tabId,
482
+ ...(record.tabTitle ? { tab_title: record.tabTitle } : {}),
483
+ ...(record.tabUrl ? { tab_url: record.tabUrl } : {}),
484
+ bundle_dir_name: record.bundleDirName,
485
+ bundle_relative_path: record.bundleRelativePath,
486
+ bundle_path: record.bundlePath,
487
+ started_at: record.startedAt,
488
+ ...(record.stoppedAt ? { stopped_at: record.stoppedAt } : {}),
489
+ status: record.status,
490
+ event_count: record.eventCount,
491
+ ...(record.lastEventAt ? { last_event_at: record.lastEventAt } : {}),
492
+ };
493
+ }
494
+ async handleManualRecordingRequest(socket, instanceId, message) {
495
+ const sessionId = this.config.project.sessionId?.trim();
496
+ if (!sessionId) {
497
+ this.sendJson(socket, {
498
+ type: "recording_manual_result",
499
+ request_id: message.request_id,
500
+ ok: false,
501
+ active: false,
502
+ error: "Session id is not configured for this attach server.",
503
+ });
504
+ return;
505
+ }
506
+ try {
507
+ if (message.type === "recording_manual_status") {
508
+ const record = await this.getRecordingStatus(sessionId);
509
+ this.sendJson(socket, {
510
+ type: "recording_manual_result",
511
+ request_id: message.request_id,
512
+ ok: true,
513
+ active: record?.status === "recording",
514
+ ...(record ? { recording: this.mapManualRecordingResult(record) } : {}),
515
+ });
516
+ return;
517
+ }
518
+ if (message.type === "recording_manual_stop") {
519
+ const record = await this.stopRecording({ sessionId });
520
+ this.sendJson(socket, {
521
+ type: "recording_manual_result",
522
+ request_id: message.request_id,
523
+ ok: true,
524
+ active: record?.status === "recording",
525
+ ...(record ? { recording: this.mapManualRecordingResult(record) } : {}),
526
+ });
527
+ return;
528
+ }
529
+ const tab = message.tab;
530
+ if (!tab) {
531
+ throw new Error("Selected tab metadata is required to start recording.");
532
+ }
533
+ const record = await this.startRecording({
534
+ sessionId,
535
+ instanceId,
536
+ tabId: tab.tab_id,
537
+ tabTitle: tab.title || "attached-tab",
538
+ ...(tab.url ? { tabUrl: tab.url } : {}),
539
+ });
540
+ this.sendJson(socket, {
541
+ type: "recording_manual_result",
542
+ request_id: message.request_id,
543
+ ok: true,
544
+ active: true,
545
+ recording: this.mapManualRecordingResult(record),
546
+ });
547
+ }
548
+ catch (error) {
549
+ this.sendJson(socket, {
550
+ type: "recording_manual_result",
551
+ request_id: message.request_id,
552
+ ok: false,
553
+ active: false,
554
+ error: error instanceof Error ? error.message : String(error),
555
+ });
556
+ }
557
+ }
558
+ }
559
+ exports.FirefoxAttachServer = FirefoxAttachServer;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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: {