@deadragdoll/tellymcp 0.0.12 → 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 (48) hide show
  1. package/README-ru.md +19 -0
  2. package/README.md +19 -0
  3. package/TOOLS.md +206 -3
  4. package/dist/cli.js +109 -1
  5. package/dist/services/features/telegram-mcp/browser.service.js +38 -1
  6. package/dist/services/features/telegram-mcp/mcp-server.service.js +12 -0
  7. package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +14 -0
  8. package/dist/services/features/telegram-mcp/src/app/config/env.js +13 -0
  9. package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +147 -2
  10. package/dist/services/features/telegram-mcp/src/features/browser/model/browserClickTool.js +1 -1
  11. package/dist/services/features/telegram-mcp/src/features/browser/model/browserDomTool.js +1 -1
  12. package/dist/services/features/telegram-mcp/src/features/browser/model/browserFillTool.js +1 -1
  13. package/dist/services/features/telegram-mcp/src/features/browser/model/browserInjectScriptTool.js +28 -0
  14. package/dist/services/features/telegram-mcp/src/features/browser/model/browserListAttachedInstancesTool.js +33 -0
  15. package/dist/services/features/telegram-mcp/src/features/browser/model/browserListTabsTool.js +33 -0
  16. package/dist/services/features/telegram-mcp/src/features/browser/model/browserPressTool.js +1 -1
  17. package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStartTool.js +28 -0
  18. package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStatusTool.js +28 -0
  19. package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStopTool.js +28 -0
  20. package/dist/services/features/telegram-mcp/src/features/browser/model/browserScreenshotTool.js +1 -1
  21. package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +485 -1
  22. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/browserRecordingBundle.js +502 -0
  23. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachRegistry.js +79 -0
  24. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js +559 -0
  25. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/types.js +2 -0
  26. package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +28 -0
  27. package/docs/STANDALONE-ru.md +42 -6
  28. package/docs/STANDALONE.md +42 -6
  29. package/package.json +6 -3
  30. package/packages/chrome-attach-extension/dist/background.js +1326 -0
  31. package/packages/chrome-attach-extension/dist/icon.svg +6 -0
  32. package/packages/chrome-attach-extension/dist/manifest.json +36 -0
  33. package/packages/chrome-attach-extension/dist/options.html +312 -0
  34. package/packages/chrome-attach-extension/dist/options.js +593 -0
  35. package/packages/chrome-attach-extension/dist/popup.html +93 -0
  36. package/packages/chrome-attach-extension/dist/popup.js +79 -0
  37. package/packages/chrome-attach-extension/dist/recorder-content.js +83 -0
  38. package/packages/chrome-attach-extension/dist/recorder-page.js +266 -0
  39. package/packages/firefox-attach-extension/README.md +13 -0
  40. package/packages/firefox-attach-extension/dist/background.js +1242 -0
  41. package/packages/firefox-attach-extension/dist/icon.svg +6 -0
  42. package/packages/firefox-attach-extension/dist/manifest.json +56 -0
  43. package/packages/firefox-attach-extension/dist/options.html +312 -0
  44. package/packages/firefox-attach-extension/dist/options.js +527 -0
  45. package/packages/firefox-attach-extension/dist/popup.html +93 -0
  46. package/packages/firefox-attach-extension/dist/popup.js +64 -0
  47. package/packages/firefox-attach-extension/dist/recorder-content.js +77 -0
  48. 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 });
@@ -44,6 +44,12 @@ function adminAuthorizedMatchPattern() {
44
44
  function gatewayClientUuidKey() {
45
45
  return `${KEY_PREFIX}:gateway:client-uuid`;
46
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
+ }
47
53
  function outgoingDeliveryNoticeKey(deliveryUuid) {
48
54
  return `${KEY_PREFIX}:gateway:outgoing-delivery:${deliveryUuid}`;
49
55
  }
@@ -103,6 +109,8 @@ class RedisStateStore {
103
109
  await this.clearXchangeFileMetas(sessionId);
104
110
  await this.clearProjectMenuViewStates(sessionId);
105
111
  await this.clearOutgoingDeliveryNoticesForSession(sessionId);
112
+ await this.clearBrowserAttachment(sessionId);
113
+ await this.clearBrowserRecording(sessionId);
106
114
  await this.sessionAdapter.delete(sessionKey(sessionId));
107
115
  }
108
116
  async createPairCode(record, ttlSeconds) {
@@ -340,6 +348,26 @@ class RedisStateStore {
340
348
  async setGatewayClientUuid(clientUuid) {
341
349
  await this.redis.set(gatewayClientUuidKey(), clientUuid);
342
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
+ }
343
371
  async setProjectMenuViewState(state) {
344
372
  await this.redis.set(projectMenuViewStateKey(state.projectUuid, state.sessionId), JSON.stringify(state));
345
373
  }