@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
@@ -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) {
@@ -47,6 +48,8 @@ function formatConsoleLocation(message) {
47
48
  function escapeCssAttributeValue(value) {
48
49
  return value.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"');
49
50
  }
51
+ const DEFAULT_BROWSER_VIEWPORT_WIDTH = 1720;
52
+ const DEFAULT_BROWSER_VIEWPORT_HEIGHT = 980;
50
53
  class BrowserService {
51
54
  config;
52
55
  sessionStore;
@@ -58,10 +61,11 @@ class BrowserService {
58
61
  logger;
59
62
  projectIdentityResolver;
60
63
  remoteConsoleInvoker;
64
+ firefoxAttachRegistry;
61
65
  playwrightModulePromise;
62
66
  browserPromise;
63
67
  sessionStates = new Map();
64
- constructor(config, sessionStore, maintenanceStore, bindingStore, xchangeFileMetaStore, objectStore, telegramTransport, logger, projectIdentityResolver, remoteConsoleInvoker) {
68
+ constructor(config, sessionStore, maintenanceStore, bindingStore, xchangeFileMetaStore, objectStore, telegramTransport, logger, projectIdentityResolver, remoteConsoleInvoker, firefoxAttachRegistry) {
65
69
  this.config = config;
66
70
  this.sessionStore = sessionStore;
67
71
  this.maintenanceStore = maintenanceStore;
@@ -72,6 +76,173 @@ class BrowserService {
72
76
  this.logger = logger;
73
77
  this.projectIdentityResolver = projectIdentityResolver;
74
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
+ };
75
246
  }
76
247
  async open(input) {
77
248
  const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
@@ -85,12 +256,26 @@ class BrowserService {
85
256
  }
86
257
  this.ensureEnabled();
87
258
  const existingState = this.sessionStates.get(normalizedSessionId);
88
- const shouldReset = input.reset_context === true;
259
+ const requestedViewport = this.resolveRequestedViewport(input);
260
+ const shouldUpgradeLegacyViewport = this.config.browser.headless === false &&
261
+ input.reset_context !== true &&
262
+ !requestedViewport &&
263
+ Boolean(existingState?.page.viewportSize());
264
+ const shouldReset = input.reset_context === true || shouldUpgradeLegacyViewport;
89
265
  const targetUrl = this.resolveBrowserUrl(input.url);
90
266
  if (shouldReset && existingState) {
91
267
  await this.closeState(normalizedSessionId, existingState);
92
268
  }
93
- const { state, createdContext } = await this.ensureSessionState(normalizedSessionId, shouldReset);
269
+ const { state, createdContext } = await this.ensureSessionState(normalizedSessionId, shouldReset, input);
270
+ if (requestedViewport) {
271
+ await state.page.setViewportSize(requestedViewport);
272
+ }
273
+ else if (this.config.browser.headless !== false) {
274
+ await state.page.setViewportSize({
275
+ width: DEFAULT_BROWSER_VIEWPORT_WIDTH,
276
+ height: DEFAULT_BROWSER_VIEWPORT_HEIGHT,
277
+ });
278
+ }
94
279
  const waitUntil = (input.wait_until ??
95
280
  this.config.browser.waitUntil);
96
281
  await state.page.goto(targetUrl, {
@@ -108,13 +293,18 @@ class BrowserService {
108
293
  createdContext,
109
294
  waitUntil,
110
295
  headless: this.config.browser.headless,
296
+ viewportWidth: state.page.viewportSize()?.width ?? requestedViewport?.width,
297
+ viewportHeight: state.page.viewportSize()?.height ?? requestedViewport?.height,
111
298
  });
299
+ const viewport = state.page.viewportSize();
112
300
  return {
113
301
  session_id: normalizedSessionId,
114
302
  opened: true,
115
303
  created_context: createdContext,
116
304
  url: state.currentUrl,
117
305
  ...(state.title ? { title: state.title } : {}),
306
+ ...(viewport?.width ? { viewport_width: viewport.width } : {}),
307
+ ...(viewport?.height ? { viewport_height: viewport.height } : {}),
118
308
  };
119
309
  }
120
310
  async getConsole(input) {
@@ -152,6 +342,22 @@ class BrowserService {
152
342
  return remote;
153
343
  }
154
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
+ }
155
361
  const { sessionId, state } = await this.requireSessionState(input);
156
362
  const locator = this.resolveLocator(state.page, input);
157
363
  await locator.click({
@@ -181,6 +387,26 @@ class BrowserService {
181
387
  return remote;
182
388
  }
183
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
+ }
184
410
  const { sessionId, state } = await this.requireSessionState(input);
185
411
  const locator = this.resolveLocator(state.page, input);
186
412
  await locator.fill(input.value, {
@@ -211,6 +437,26 @@ class BrowserService {
211
437
  return remote;
212
438
  }
213
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
+ }
214
460
  const { sessionId, state } = await this.requireSessionState(input);
215
461
  if (input.selector || input.text) {
216
462
  const locator = this.resolveLocator(state.page, input);
@@ -235,6 +481,64 @@ class BrowserService {
235
481
  ...(state.title ? { title: state.title } : {}),
236
482
  };
237
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
+ }
238
542
  async reload(input) {
239
543
  const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
240
544
  const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
@@ -432,6 +736,37 @@ class BrowserService {
432
736
  return remote;
433
737
  }
434
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
+ }
435
770
  const { sessionId, state } = await this.requireSessionState(input);
436
771
  const selector = input.selector?.trim() || "body";
437
772
  const snapshot = await state.page
@@ -562,6 +897,75 @@ class BrowserService {
562
897
  return remote;
563
898
  }
564
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
+ }
565
969
  const { sessionId, state, session } = await this.requireSessionState(input);
566
970
  const fileName = sanitizeScreenshotName(input.file_name);
567
971
  const pngBuffer = input.selector?.trim()
@@ -690,10 +1094,13 @@ class BrowserService {
690
1094
  async ensureBrowser() {
691
1095
  this.browserPromise ??= (async () => {
692
1096
  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
- : [];
1097
+ const launchArgs = [];
1098
+ if (this.config.browser.headless === false) {
1099
+ launchArgs.push("--start-maximized");
1100
+ if (this.config.browser.devtools === true) {
1101
+ launchArgs.push("--auto-open-devtools-for-tabs");
1102
+ }
1103
+ }
697
1104
  const browser = await playwright.chromium.launch({
698
1105
  headless: this.config.browser.headless,
699
1106
  slowMo: this.config.browser.slowMoMs,
@@ -717,13 +1124,13 @@ class BrowserService {
717
1124
  })();
718
1125
  return this.browserPromise;
719
1126
  }
720
- async ensureSessionState(sessionId, forceNewContext) {
1127
+ async ensureSessionState(sessionId, forceNewContext, openInput) {
721
1128
  const existing = this.sessionStates.get(sessionId);
722
1129
  if (existing && !forceNewContext) {
723
1130
  return { state: existing, createdContext: false };
724
1131
  }
725
1132
  const browser = await this.ensureBrowser();
726
- const context = await browser.newContext();
1133
+ const context = await browser.newContext(this.buildContextOptions(openInput));
727
1134
  const page = await context.newPage();
728
1135
  const createdAt = new Date().toISOString();
729
1136
  const state = {
@@ -763,6 +1170,32 @@ class BrowserService {
763
1170
  this.sessionStates.set(sessionId, state);
764
1171
  return { state, createdContext: true };
765
1172
  }
1173
+ resolveRequestedViewport(input) {
1174
+ const width = input?.width;
1175
+ const height = input?.height;
1176
+ if (!width && !height) {
1177
+ return null;
1178
+ }
1179
+ if (!width || !height) {
1180
+ throw new Error("Browser viewport requires both width and height together.");
1181
+ }
1182
+ return { width, height };
1183
+ }
1184
+ buildContextOptions(input) {
1185
+ const requestedViewport = this.resolveRequestedViewport(input);
1186
+ if (requestedViewport) {
1187
+ return {
1188
+ viewport: requestedViewport,
1189
+ screen: requestedViewport,
1190
+ };
1191
+ }
1192
+ if (this.config.browser.headless === false) {
1193
+ return {
1194
+ viewport: null,
1195
+ };
1196
+ }
1197
+ return {};
1198
+ }
766
1199
  recordNetworkFailure(state, request, response) {
767
1200
  const failure = request.failure();
768
1201
  pushBounded(state.networkFailures, {
@@ -825,6 +1258,107 @@ class BrowserService {
825
1258
  const localSession = await this.sessionStore.getSession(localSessionId);
826
1259
  return localSession ? localSessionId : trimmed;
827
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
+ }
828
1362
  async sendScreenshotToGatewayTelegramRoute(input) {
829
1363
  if (!this.config.distributed.gatewayPublicUrl) {
830
1364
  throw new Error("send_to_telegram on client nodes requires GATEWAY_PUBLIC_URL.");