@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
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.BrowserService = void 0;
7
+ const promises_1 = require("node:fs/promises");
7
8
  const node_path_1 = __importDefault(require("node:path"));
8
9
  const gatewayClientAccess_1 = require("../../distributed-client/model/gatewayClientAccess");
9
10
  function pushBounded(list, entry, max) {
@@ -60,10 +61,11 @@ class BrowserService {
60
61
  logger;
61
62
  projectIdentityResolver;
62
63
  remoteConsoleInvoker;
64
+ firefoxAttachRegistry;
63
65
  playwrightModulePromise;
64
66
  browserPromise;
65
67
  sessionStates = new Map();
66
- constructor(config, sessionStore, maintenanceStore, bindingStore, xchangeFileMetaStore, objectStore, telegramTransport, logger, projectIdentityResolver, remoteConsoleInvoker) {
68
+ constructor(config, sessionStore, maintenanceStore, bindingStore, xchangeFileMetaStore, objectStore, telegramTransport, logger, projectIdentityResolver, remoteConsoleInvoker, firefoxAttachRegistry) {
67
69
  this.config = config;
68
70
  this.sessionStore = sessionStore;
69
71
  this.maintenanceStore = maintenanceStore;
@@ -74,6 +76,173 @@ class BrowserService {
74
76
  this.logger = logger;
75
77
  this.projectIdentityResolver = projectIdentityResolver;
76
78
  this.remoteConsoleInvoker = remoteConsoleInvoker;
79
+ this.firefoxAttachRegistry = firefoxAttachRegistry;
80
+ }
81
+ async listAttachedInstances(input) {
82
+ const resolvedSessionId = await this.resolveOptionalSessionIdForRemote(input);
83
+ if (resolvedSessionId) {
84
+ const remote = await this.invokeRemote(resolvedSessionId, "telegramMcp.browser.listAttachedInstancesRemote", {
85
+ ...input,
86
+ session_id: resolvedSessionId,
87
+ });
88
+ if (remote) {
89
+ return remote;
90
+ }
91
+ }
92
+ const instances = this.listFirefoxAttachInstances();
93
+ return {
94
+ ...(resolvedSessionId ? { session_id: resolvedSessionId } : {}),
95
+ total: instances.length,
96
+ instances: instances.map((instance) => ({
97
+ instance_id: instance.instanceId,
98
+ browser: instance.browser,
99
+ extension_version: instance.extensionVersion,
100
+ ...(instance.profileName ? { profile_name: instance.profileName } : {}),
101
+ connected_at: instance.connectedAt,
102
+ last_seen_at: instance.lastSeenAt,
103
+ capabilities: [...instance.capabilities],
104
+ tab_count: instance.tabs.length,
105
+ ...(instance.activeTab
106
+ ? {
107
+ active_tab: {
108
+ tab_id: instance.activeTab.tab_id,
109
+ ...(typeof instance.activeTab.window_id === "number"
110
+ ? { window_id: instance.activeTab.window_id }
111
+ : {}),
112
+ active: instance.activeTab.active,
113
+ title: instance.activeTab.title,
114
+ url: instance.activeTab.url,
115
+ ...(instance.activeTab.status
116
+ ? { status: instance.activeTab.status }
117
+ : {}),
118
+ },
119
+ }
120
+ : {}),
121
+ })),
122
+ };
123
+ }
124
+ async listTabs(input) {
125
+ const resolvedSessionId = await this.resolveOptionalSessionIdForRemote(input);
126
+ if (resolvedSessionId) {
127
+ const remote = await this.invokeRemote(resolvedSessionId, "telegramMcp.browser.listTabsRemote", {
128
+ ...input,
129
+ session_id: resolvedSessionId,
130
+ });
131
+ if (remote) {
132
+ return remote;
133
+ }
134
+ }
135
+ const instances = this.listFirefoxAttachInstances();
136
+ const instance = input.instance_id?.trim()
137
+ ? instances.find((item) => item.instanceId === input.instance_id?.trim())
138
+ : instances.length === 1
139
+ ? instances[0]
140
+ : undefined;
141
+ if (!instance) {
142
+ if (input.instance_id?.trim()) {
143
+ throw new Error(`Attached browser instance '${input.instance_id.trim()}' was not found.`);
144
+ }
145
+ throw new Error("Attached browser instance is ambiguous. Pass instance_id or keep exactly one connected browser instance.");
146
+ }
147
+ const selectedAttachment = resolvedSessionId
148
+ ? await this.getAttachedBrowserAttachment(resolvedSessionId)
149
+ : null;
150
+ return {
151
+ ...(resolvedSessionId ? { session_id: resolvedSessionId } : {}),
152
+ instance_id: instance.instanceId,
153
+ total: instance.tabs.length,
154
+ tabs: instance.tabs.map((tab) => ({
155
+ tab_id: tab.tab_id,
156
+ ...(typeof tab.window_id === "number" ? { window_id: tab.window_id } : {}),
157
+ active: tab.active,
158
+ ...(selectedAttachment &&
159
+ selectedAttachment.instanceId === instance.instanceId &&
160
+ selectedAttachment.tabId === tab.tab_id
161
+ ? { selected: true }
162
+ : {}),
163
+ title: tab.title,
164
+ url: tab.url,
165
+ ...(tab.status ? { status: tab.status } : {}),
166
+ })),
167
+ };
168
+ }
169
+ async startRecording(input) {
170
+ const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
171
+ const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
172
+ const remote = await this.invokeRemote(normalizedSessionId, "telegramMcp.browser.startRecordingRemote", {
173
+ ...input,
174
+ session_id: normalizedSessionId,
175
+ });
176
+ if (remote) {
177
+ return remote;
178
+ }
179
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
180
+ if (!attached) {
181
+ throw new Error("No attached Firefox tab is selected for this session. Select a tab in the extension popup first.");
182
+ }
183
+ if (!this.firefoxAttachRegistry) {
184
+ throw new Error("Firefox attach backend is not available.");
185
+ }
186
+ const record = await this.firefoxAttachRegistry.startRecording({
187
+ sessionId: normalizedSessionId,
188
+ instanceId: input.instance_id?.trim() || attached.instanceId,
189
+ tabId: attached.tabId,
190
+ tabTitle: attached.title?.trim() || "attached-tab",
191
+ ...(attached.url ? { tabUrl: attached.url } : {}),
192
+ });
193
+ return this.mapRecordingRecord(normalizedSessionId, record);
194
+ }
195
+ async stopRecording(input) {
196
+ const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
197
+ const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
198
+ const remote = await this.invokeRemote(normalizedSessionId, "telegramMcp.browser.stopRecordingRemote", {
199
+ ...input,
200
+ session_id: normalizedSessionId,
201
+ });
202
+ if (remote) {
203
+ return remote;
204
+ }
205
+ if (!this.firefoxAttachRegistry) {
206
+ throw new Error("Firefox attach backend is not available.");
207
+ }
208
+ const record = await this.firefoxAttachRegistry.stopRecording({
209
+ sessionId: normalizedSessionId,
210
+ });
211
+ if (!record) {
212
+ return {
213
+ session_id: normalizedSessionId,
214
+ stopped: false,
215
+ };
216
+ }
217
+ return {
218
+ session_id: normalizedSessionId,
219
+ stopped: true,
220
+ recording_id: record.recordingId,
221
+ bundle_dir_name: record.bundleDirName,
222
+ bundle_relative_path: record.bundleRelativePath,
223
+ bundle_path: record.bundlePath,
224
+ ...(record.stoppedAt ? { stopped_at: record.stoppedAt } : {}),
225
+ };
226
+ }
227
+ async getRecordingStatus(input) {
228
+ const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
229
+ const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
230
+ const remote = await this.invokeRemote(normalizedSessionId, "telegramMcp.browser.getRecordingStatusRemote", {
231
+ ...input,
232
+ session_id: normalizedSessionId,
233
+ });
234
+ if (remote) {
235
+ return remote;
236
+ }
237
+ if (!this.firefoxAttachRegistry) {
238
+ throw new Error("Firefox attach backend is not available.");
239
+ }
240
+ const record = await this.firefoxAttachRegistry.getRecordingStatus(normalizedSessionId);
241
+ return {
242
+ session_id: normalizedSessionId,
243
+ active: record?.status === "recording",
244
+ ...(record ? { recording: this.mapRecordingStatusRecord(record) } : {}),
245
+ };
77
246
  }
78
247
  async open(input) {
79
248
  const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
@@ -173,6 +342,22 @@ class BrowserService {
173
342
  return remote;
174
343
  }
175
344
  this.ensureEnabled();
345
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
346
+ if (attached) {
347
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
348
+ action: "click",
349
+ payload: this.buildAttachedLocatorPayload(input),
350
+ });
351
+ return {
352
+ session_id: normalizedSessionId,
353
+ clicked: true,
354
+ ...(input.ai_tag ? { ai_tag: input.ai_tag } : {}),
355
+ ...(input.selector ? { selector: input.selector } : {}),
356
+ ...(input.text ? { text: input.text } : {}),
357
+ url: String(result?.url || attached.url || ""),
358
+ ...(result?.title ? { title: String(result.title) } : {}),
359
+ };
360
+ }
176
361
  const { sessionId, state } = await this.requireSessionState(input);
177
362
  const locator = this.resolveLocator(state.page, input);
178
363
  await locator.click({
@@ -202,6 +387,26 @@ class BrowserService {
202
387
  return remote;
203
388
  }
204
389
  this.ensureEnabled();
390
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
391
+ if (attached) {
392
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
393
+ action: "fill",
394
+ payload: {
395
+ ...this.buildAttachedLocatorPayload(input),
396
+ value: input.value,
397
+ },
398
+ });
399
+ return {
400
+ session_id: normalizedSessionId,
401
+ filled: true,
402
+ ...(input.ai_tag ? { ai_tag: input.ai_tag } : {}),
403
+ ...(input.selector ? { selector: input.selector } : {}),
404
+ ...(input.text ? { text: input.text } : {}),
405
+ value_length: input.value.length,
406
+ url: String(result?.url || attached.url || ""),
407
+ ...(result?.title ? { title: String(result.title) } : {}),
408
+ };
409
+ }
205
410
  const { sessionId, state } = await this.requireSessionState(input);
206
411
  const locator = this.resolveLocator(state.page, input);
207
412
  await locator.fill(input.value, {
@@ -232,6 +437,26 @@ class BrowserService {
232
437
  return remote;
233
438
  }
234
439
  this.ensureEnabled();
440
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
441
+ if (attached) {
442
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
443
+ action: "press",
444
+ payload: {
445
+ ...this.buildAttachedLocatorPayload(input),
446
+ key: input.key,
447
+ },
448
+ });
449
+ return {
450
+ session_id: normalizedSessionId,
451
+ pressed: true,
452
+ key: input.key,
453
+ ...(input.ai_tag ? { ai_tag: input.ai_tag } : {}),
454
+ ...(input.selector ? { selector: input.selector } : {}),
455
+ ...(input.text ? { text: input.text } : {}),
456
+ url: String(result?.url || attached.url || ""),
457
+ ...(result?.title ? { title: String(result.title) } : {}),
458
+ };
459
+ }
235
460
  const { sessionId, state } = await this.requireSessionState(input);
236
461
  if (input.selector || input.text) {
237
462
  const locator = this.resolveLocator(state.page, input);
@@ -256,6 +481,64 @@ class BrowserService {
256
481
  ...(state.title ? { title: state.title } : {}),
257
482
  };
258
483
  }
484
+ async injectScript(input) {
485
+ const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
486
+ const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
487
+ const remote = await this.invokeRemote(normalizedSessionId, "telegramMcp.browser.injectScriptRemote", {
488
+ ...input,
489
+ session_id: normalizedSessionId,
490
+ });
491
+ if (remote) {
492
+ return remote;
493
+ }
494
+ this.ensureEnabled();
495
+ const namespace = input.namespace?.trim() || "TELLY";
496
+ const source = input.source?.trim().length
497
+ ? input.source
498
+ : input.file_path?.trim()
499
+ ? await (0, promises_1.readFile)(node_path_1.default.resolve(input.file_path.trim()), "utf8")
500
+ : null;
501
+ if (!source) {
502
+ throw new Error("Provide source or file_path.");
503
+ }
504
+ const sourceType = input.source?.trim().length ? "inline" : "file";
505
+ const wrappedSource = this.wrapInjectedScript(source, namespace);
506
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
507
+ if (attached) {
508
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
509
+ action: "inject_script",
510
+ payload: {
511
+ namespace,
512
+ source,
513
+ },
514
+ });
515
+ return {
516
+ session_id: normalizedSessionId,
517
+ injected: true,
518
+ namespace,
519
+ source_type: sourceType,
520
+ bytes: Buffer.byteLength(source, "utf8"),
521
+ url: String(result?.url || attached.url || ""),
522
+ ...(result?.title ? { title: String(result.title) } : {}),
523
+ };
524
+ }
525
+ const { sessionId, state } = await this.requireSessionState(input);
526
+ await state.page.addScriptTag({
527
+ content: wrappedSource,
528
+ });
529
+ state.currentUrl = state.page.url();
530
+ state.title = await state.page.title().catch(() => state.title);
531
+ state.lastUsedAt = new Date().toISOString();
532
+ return {
533
+ session_id: sessionId,
534
+ injected: true,
535
+ namespace,
536
+ source_type: sourceType,
537
+ bytes: Buffer.byteLength(source, "utf8"),
538
+ url: state.currentUrl,
539
+ ...(state.title ? { title: state.title } : {}),
540
+ };
541
+ }
259
542
  async reload(input) {
260
543
  const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
261
544
  const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
@@ -453,6 +736,37 @@ class BrowserService {
453
736
  return remote;
454
737
  }
455
738
  this.ensureEnabled();
739
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
740
+ if (attached) {
741
+ const selector = input.selector?.trim() || "body";
742
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
743
+ action: "dom",
744
+ payload: {
745
+ selector,
746
+ include_html: input.include_html !== false,
747
+ include_text: input.include_text !== false,
748
+ },
749
+ });
750
+ return {
751
+ session_id: normalizedSessionId,
752
+ selector,
753
+ found: result?.found === true,
754
+ ...(result?.url ? { url: String(result.url) } : {}),
755
+ ...(result?.title ? { title: String(result.title) } : {}),
756
+ ...(typeof result?.outer_html === "string"
757
+ ? { outer_html: result.outer_html }
758
+ : {}),
759
+ ...(typeof result?.text_content === "string"
760
+ ? { text_content: result.text_content }
761
+ : {}),
762
+ ...(typeof result?.visible === "boolean"
763
+ ? { visible: result.visible }
764
+ : {}),
765
+ ...(result?.attributes && typeof result.attributes === "object"
766
+ ? { attributes: result.attributes }
767
+ : {}),
768
+ };
769
+ }
456
770
  const { sessionId, state } = await this.requireSessionState(input);
457
771
  const selector = input.selector?.trim() || "body";
458
772
  const snapshot = await state.page
@@ -583,6 +897,75 @@ class BrowserService {
583
897
  return remote;
584
898
  }
585
899
  this.ensureEnabled();
900
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
901
+ if (attached) {
902
+ const session = await this.sessionStore.getSession(normalizedSessionId);
903
+ const fileName = sanitizeScreenshotName(input.file_name);
904
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
905
+ action: "screenshot",
906
+ });
907
+ const pngBase64 = typeof result?.png_base64 === "string" ? result.png_base64 : "";
908
+ if (!pngBase64) {
909
+ throw new Error("Attached browser screenshot did not return PNG data.");
910
+ }
911
+ const pngBuffer = Buffer.from(pngBase64, "base64");
912
+ const workspaceDir = this.objectStore.resolveWorkspaceDir(session);
913
+ const exchangeDir = node_path_1.default.resolve(workspaceDir, this.config.exchange.dir);
914
+ const storedFile = await this.objectStore.storeFile({
915
+ session,
916
+ sessionId: normalizedSessionId,
917
+ source: "browser-screenshot",
918
+ relativePath: buildDatedRelativePath(fileName),
919
+ content: pngBuffer,
920
+ mimeType: "image/png",
921
+ });
922
+ await this.xchangeFileMetaStore.setXchangeFileMeta({
923
+ sessionId: normalizedSessionId,
924
+ filePath: storedFile.filePath,
925
+ relativePath: storedFile.relativePath,
926
+ source: "browser-screenshot",
927
+ uploadedAt: new Date().toISOString(),
928
+ storageRef: storedFile.storageRef,
929
+ bucketName: storedFile.bucketName,
930
+ objectName: storedFile.objectName,
931
+ vfsNodeId: storedFile.vfsNodeId,
932
+ vfsPublicUrl: storedFile.vfsPublicUrl,
933
+ vfsParentId: storedFile.vfsParentId,
934
+ mimeType: "image/png",
935
+ sizeBytes: storedFile.sizeBytes,
936
+ ...(input.caption ? { caption: input.caption } : {}),
937
+ });
938
+ let telegramMessageId;
939
+ if (input.send_to_telegram === true) {
940
+ if (this.config.distributed.mode === "client") {
941
+ telegramMessageId = await this.sendScreenshotToGatewayTelegramRoute({
942
+ sessionId: normalizedSessionId,
943
+ fileName,
944
+ pngBuffer,
945
+ ...(input.caption ? { caption: input.caption } : {}),
946
+ });
947
+ }
948
+ else {
949
+ const binding = await this.bindingStore.getBinding(normalizedSessionId);
950
+ if (!binding) {
951
+ throw new Error("Session is not linked to Telegram, so screenshot cannot be sent there.");
952
+ }
953
+ const sent = await this.telegramTransport.sendDocumentToChat(binding.telegramChatId, storedFile.filePath, input.caption);
954
+ telegramMessageId = sent.messageId;
955
+ }
956
+ }
957
+ return {
958
+ session_id: normalizedSessionId,
959
+ file_path: storedFile.filePath,
960
+ workspace_dir: workspaceDir,
961
+ exchange_dir: exchangeDir,
962
+ ...(typeof telegramMessageId === "number"
963
+ ? { telegram_message_id: telegramMessageId }
964
+ : {}),
965
+ ...(result?.url ? { url: String(result.url) } : {}),
966
+ ...(result?.title ? { title: String(result.title) } : {}),
967
+ };
968
+ }
586
969
  const { sessionId, state, session } = await this.requireSessionState(input);
587
970
  const fileName = sanitizeScreenshotName(input.file_name);
588
971
  const pngBuffer = input.selector?.trim()
@@ -875,6 +1258,107 @@ class BrowserService {
875
1258
  const localSession = await this.sessionStore.getSession(localSessionId);
876
1259
  return localSession ? localSessionId : trimmed;
877
1260
  }
1261
+ async resolveOptionalSessionIdForRemote(input) {
1262
+ const trimmed = input.session_id?.trim();
1263
+ if (!trimmed) {
1264
+ return undefined;
1265
+ }
1266
+ return await this.normalizeSessionIdForAccess(trimmed);
1267
+ }
1268
+ listFirefoxAttachInstances() {
1269
+ if (!this.firefoxAttachRegistry) {
1270
+ return [];
1271
+ }
1272
+ return this.firefoxAttachRegistry.listInstances();
1273
+ }
1274
+ buildAttachedLocatorPayload(input) {
1275
+ return {
1276
+ ...(input.ai_tag?.trim() ? { ai_tag: input.ai_tag.trim() } : {}),
1277
+ ...(input.selector?.trim() ? { selector: input.selector.trim() } : {}),
1278
+ ...(input.text?.trim() ? { text: input.text.trim() } : {}),
1279
+ ...(typeof input.exact === "boolean" ? { exact: input.exact } : {}),
1280
+ };
1281
+ }
1282
+ async getAttachedBrowserAttachment(sessionId) {
1283
+ return await this.maintenanceStore.getBrowserAttachment(sessionId);
1284
+ }
1285
+ mapRecordingRecord(sessionId, record) {
1286
+ return {
1287
+ session_id: sessionId,
1288
+ started: true,
1289
+ backend: "firefox-attached",
1290
+ recording_id: record.recordingId,
1291
+ instance_id: record.instanceId,
1292
+ tab_id: record.tabId,
1293
+ ...(record.tabTitle ? { tab_title: record.tabTitle } : {}),
1294
+ ...(record.tabUrl ? { tab_url: record.tabUrl } : {}),
1295
+ bundle_dir_name: record.bundleDirName,
1296
+ bundle_relative_path: record.bundleRelativePath,
1297
+ bundle_path: record.bundlePath,
1298
+ started_at: record.startedAt,
1299
+ };
1300
+ }
1301
+ mapRecordingStatusRecord(record) {
1302
+ return {
1303
+ backend: "firefox-attached",
1304
+ recording_id: record.recordingId,
1305
+ instance_id: record.instanceId,
1306
+ tab_id: record.tabId,
1307
+ ...(record.tabTitle ? { tab_title: record.tabTitle } : {}),
1308
+ ...(record.tabUrl ? { tab_url: record.tabUrl } : {}),
1309
+ bundle_dir_name: record.bundleDirName,
1310
+ bundle_relative_path: record.bundleRelativePath,
1311
+ bundle_path: record.bundlePath,
1312
+ started_at: record.startedAt,
1313
+ ...(record.stoppedAt ? { stopped_at: record.stoppedAt } : {}),
1314
+ status: record.status,
1315
+ event_count: record.eventCount,
1316
+ ...(record.lastEventAt ? { last_event_at: record.lastEventAt } : {}),
1317
+ };
1318
+ }
1319
+ async runAttachedTabAction(sessionId, attachment, input) {
1320
+ if (!this.firefoxAttachRegistry) {
1321
+ throw new Error("Attached browser backend is not available.");
1322
+ }
1323
+ const result = await this.firefoxAttachRegistry.invokeTabAction({
1324
+ instanceId: attachment.instanceId,
1325
+ tabId: attachment.tabId,
1326
+ action: input.action,
1327
+ payload: input.payload,
1328
+ });
1329
+ await this.maintenanceStore.setBrowserAttachment({
1330
+ ...attachment,
1331
+ ...(result?.title ? { title: String(result.title) } : {}),
1332
+ ...(result?.url ? { url: String(result.url) } : {}),
1333
+ });
1334
+ this.logger.info("Attached browser tab action executed", {
1335
+ sessionId,
1336
+ instanceId: attachment.instanceId,
1337
+ tabId: attachment.tabId,
1338
+ action: input.action,
1339
+ });
1340
+ return result;
1341
+ }
1342
+ wrapInjectedScript(source, namespace) {
1343
+ return `const __tellyNamespace = ${JSON.stringify(namespace)};
1344
+ window[__tellyNamespace] = window[__tellyNamespace] || {};
1345
+ var TELLY = window[__tellyNamespace];
1346
+ const __tellyBeforeKeys = new Set(Object.getOwnPropertyNames(window));
1347
+ ${source}
1348
+ for (const __tellyKey of Object.getOwnPropertyNames(window)) {
1349
+ if (__tellyBeforeKeys.has(__tellyKey)) {
1350
+ continue;
1351
+ }
1352
+ if (__tellyKey === __tellyNamespace) {
1353
+ continue;
1354
+ }
1355
+ try {
1356
+ window[__tellyNamespace][__tellyKey] = window[__tellyKey];
1357
+ } catch {
1358
+ // ignore unassignable globals
1359
+ }
1360
+ }`;
1361
+ }
878
1362
  async sendScreenshotToGatewayTelegramRoute(input) {
879
1363
  if (!this.config.distributed.gatewayPublicUrl) {
880
1364
  throw new Error("send_to_telegram on client nodes requires GATEWAY_PUBLIC_URL.");