@deadragdoll/tellymcp 0.0.12 → 0.0.14

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 (83) hide show
  1. package/.env.example.client +5 -1
  2. package/.env.example.gateway +16 -1
  3. package/CHANGELOG.md +30 -0
  4. package/README-ru.md +33 -0
  5. package/README.md +33 -0
  6. package/TOOLS.md +384 -9
  7. package/config/templates/env.both.template +15 -2
  8. package/config/templates/env.client.template +5 -2
  9. package/config/templates/env.gateway.template +15 -2
  10. package/dist/cli.js +137 -2
  11. package/dist/services/features/telegram-mcp/browser.service.js +56 -1
  12. package/dist/services/features/telegram-mcp/file-content.service.js +94 -0
  13. package/dist/services/features/telegram-mcp/gateway-delivery.service.js +5 -1
  14. package/dist/services/features/telegram-mcp/gateway-socket.service.js +43 -11
  15. package/dist/services/features/telegram-mcp/mcp-http.service.js +1 -0
  16. package/dist/services/features/telegram-mcp/mcp-server.service.js +30 -0
  17. package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +23 -1
  18. package/dist/services/features/telegram-mcp/src/app/config/env.js +132 -2
  19. package/dist/services/features/telegram-mcp/src/app/http.js +139 -99
  20. package/dist/services/features/telegram-mcp/src/app/oauthFacade.js +642 -0
  21. package/dist/services/features/telegram-mcp/src/app/webapp/assets.js +151 -170
  22. package/dist/services/features/telegram-mcp/src/app/webapp/auth.js +96 -99
  23. package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +205 -17
  24. package/dist/services/features/telegram-mcp/src/features/browser/model/browserAttachActiveTabTool.js +28 -0
  25. package/dist/services/features/telegram-mcp/src/features/browser/model/browserAttachTabTool.js +28 -0
  26. package/dist/services/features/telegram-mcp/src/features/browser/model/browserClickTool.js +1 -1
  27. package/dist/services/features/telegram-mcp/src/features/browser/model/browserDetachTabTool.js +28 -0
  28. package/dist/services/features/telegram-mcp/src/features/browser/model/browserDomTool.js +1 -1
  29. package/dist/services/features/telegram-mcp/src/features/browser/model/browserFillTool.js +1 -1
  30. package/dist/services/features/telegram-mcp/src/features/browser/model/browserInjectScriptTool.js +28 -0
  31. package/dist/services/features/telegram-mcp/src/features/browser/model/browserListAttachedInstancesTool.js +33 -0
  32. package/dist/services/features/telegram-mcp/src/features/browser/model/browserListTabsTool.js +33 -0
  33. package/dist/services/features/telegram-mcp/src/features/browser/model/browserPressTool.js +1 -1
  34. package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStartTool.js +28 -0
  35. package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStatusTool.js +28 -0
  36. package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStopTool.js +28 -0
  37. package/dist/services/features/telegram-mcp/src/features/browser/model/browserScreenshotTool.js +1 -1
  38. package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +890 -23
  39. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/browserRecordingBundle.js +536 -0
  40. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachRegistry.js +80 -0
  41. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js +736 -0
  42. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/types.js +188 -0
  43. package/dist/services/features/telegram-mcp/src/features/collaboration/model/collaborationService.js +2 -0
  44. package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerFileService.js +6 -3
  45. package/dist/services/features/telegram-mcp/src/features/distributed-client/model/gatewayClientAccess.js +4 -1
  46. package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +33 -35
  47. package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileListTool.js +33 -0
  48. package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileService.js +327 -0
  49. package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileTool.js +81 -0
  50. package/dist/services/features/telegram-mcp/src/features/file-content/model/temporaryFileLinkStore.js +307 -0
  51. package/dist/services/features/telegram-mcp/src/features/file-content/model/workspaceFilePolicy.js +115 -0
  52. package/dist/services/features/telegram-mcp/src/features/notify/model/notifyService.js +5 -1
  53. package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +28 -0
  54. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +3 -0
  55. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportFileHandoffActions.js +6 -3
  56. package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/client.js +29 -6
  57. package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/ptyRegistry.js +100 -2
  58. package/dist/services/features/telegram-mcp/src/shared/lib/bodyLimits.js +63 -0
  59. package/dist/services/features/telegram-mcp/src/shared/lib/gatewayAuth.js +13 -0
  60. package/dist/services/features/telegram-mcp/src/shared/lib/time/localTimestamp.js +21 -0
  61. package/docs/CHAT_CONNECTOR.md +134 -0
  62. package/docs/STANDALONE-ru.md +55 -6
  63. package/docs/STANDALONE.md +55 -6
  64. package/package.json +10 -5
  65. package/packages/chrome-attach-extension/dist/background.js +1735 -0
  66. package/packages/chrome-attach-extension/dist/icon.svg +6 -0
  67. package/packages/chrome-attach-extension/dist/manifest.json +37 -0
  68. package/packages/chrome-attach-extension/dist/options.html +312 -0
  69. package/packages/chrome-attach-extension/dist/options.js +606 -0
  70. package/packages/chrome-attach-extension/dist/popup.html +93 -0
  71. package/packages/chrome-attach-extension/dist/popup.js +79 -0
  72. package/packages/chrome-attach-extension/dist/recorder-content.js +96 -0
  73. package/packages/chrome-attach-extension/dist/recorder-page.js +282 -0
  74. package/packages/firefox-attach-extension/README.md +13 -0
  75. package/packages/firefox-attach-extension/dist/background.js +1622 -0
  76. package/packages/firefox-attach-extension/dist/icon.svg +6 -0
  77. package/packages/firefox-attach-extension/dist/manifest.json +44 -0
  78. package/packages/firefox-attach-extension/dist/options.html +312 -0
  79. package/packages/firefox-attach-extension/dist/options.js +540 -0
  80. package/packages/firefox-attach-extension/dist/popup.html +93 -0
  81. package/packages/firefox-attach-extension/dist/popup.js +64 -0
  82. package/packages/firefox-attach-extension/dist/recorder-content.js +90 -0
  83. package/packages/firefox-attach-extension/dist/recorder-page.js +318 -0
@@ -4,7 +4,10 @@ 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"));
9
+ const bodyLimits_1 = require("../../../shared/lib/bodyLimits");
10
+ const localTimestamp_1 = require("../../../shared/lib/time/localTimestamp");
8
11
  const gatewayClientAccess_1 = require("../../distributed-client/model/gatewayClientAccess");
9
12
  function pushBounded(list, entry, max) {
10
13
  list.push(entry);
@@ -21,7 +24,7 @@ function trimList(list, limit) {
21
24
  function sanitizeScreenshotName(fileName) {
22
25
  const trimmed = fileName?.trim();
23
26
  if (!trimmed) {
24
- const timestamp = new Date().toISOString().replace(/[:.]/gu, "-");
27
+ const timestamp = (0, localTimestamp_1.formatLocalTimestampForFileName)(new Date());
25
28
  return `browser-screenshot-${timestamp}.png`;
26
29
  }
27
30
  const parsed = node_path_1.default.parse(trimmed);
@@ -30,8 +33,8 @@ function sanitizeScreenshotName(fileName) {
30
33
  return `${base}${extension}`;
31
34
  }
32
35
  function buildDatedRelativePath(fileName, date = new Date()) {
33
- const dateSegment = date.toISOString().slice(0, 10);
34
- const timeSegment = date.toTimeString().slice(0, 8).replace(/:/gu, "-");
36
+ const dateSegment = (0, localTimestamp_1.formatLocalDateSegment)(date);
37
+ const timeSegment = (0, localTimestamp_1.formatLocalTimeSegment)(date);
35
38
  return `${dateSegment}/${timeSegment}/${fileName}`;
36
39
  }
37
40
  function isAbsoluteBrowserUrl(value) {
@@ -60,10 +63,11 @@ class BrowserService {
60
63
  logger;
61
64
  projectIdentityResolver;
62
65
  remoteConsoleInvoker;
66
+ firefoxAttachRegistry;
63
67
  playwrightModulePromise;
64
68
  browserPromise;
65
69
  sessionStates = new Map();
66
- constructor(config, sessionStore, maintenanceStore, bindingStore, xchangeFileMetaStore, objectStore, telegramTransport, logger, projectIdentityResolver, remoteConsoleInvoker) {
70
+ constructor(config, sessionStore, maintenanceStore, bindingStore, xchangeFileMetaStore, objectStore, telegramTransport, logger, projectIdentityResolver, remoteConsoleInvoker, firefoxAttachRegistry) {
67
71
  this.config = config;
68
72
  this.sessionStore = sessionStore;
69
73
  this.maintenanceStore = maintenanceStore;
@@ -74,6 +78,304 @@ class BrowserService {
74
78
  this.logger = logger;
75
79
  this.projectIdentityResolver = projectIdentityResolver;
76
80
  this.remoteConsoleInvoker = remoteConsoleInvoker;
81
+ this.firefoxAttachRegistry = firefoxAttachRegistry;
82
+ }
83
+ async listAttachedInstances(input) {
84
+ const resolvedSessionId = await this.resolveOptionalSessionIdForRemote(input);
85
+ if (resolvedSessionId) {
86
+ const remote = await this.invokeRemote(resolvedSessionId, "telegramMcp.browser.listAttachedInstancesRemote", {
87
+ ...input,
88
+ session_id: resolvedSessionId,
89
+ });
90
+ if (remote) {
91
+ return remote;
92
+ }
93
+ }
94
+ const instances = this.listFirefoxAttachInstances();
95
+ return {
96
+ ...(resolvedSessionId ? { session_id: resolvedSessionId } : {}),
97
+ total: instances.length,
98
+ instances: instances.map((instance) => ({
99
+ instance_id: instance.instanceId,
100
+ browser: instance.browser,
101
+ extension_version: instance.extensionVersion,
102
+ ...(instance.profileName ? { profile_name: instance.profileName } : {}),
103
+ connected_at: instance.connectedAt,
104
+ last_seen_at: instance.lastSeenAt,
105
+ capabilities: [...instance.capabilities],
106
+ tab_count: instance.tabs.length,
107
+ ...(instance.activeTab
108
+ ? {
109
+ active_tab: {
110
+ tab_id: instance.activeTab.tab_id,
111
+ ...(typeof instance.activeTab.window_id === "number"
112
+ ? { window_id: instance.activeTab.window_id }
113
+ : {}),
114
+ active: instance.activeTab.active,
115
+ title: instance.activeTab.title,
116
+ url: instance.activeTab.url,
117
+ ...(instance.activeTab.status
118
+ ? { status: instance.activeTab.status }
119
+ : {}),
120
+ },
121
+ }
122
+ : {}),
123
+ })),
124
+ };
125
+ }
126
+ async listTabs(input) {
127
+ const resolvedSessionId = await this.resolveOptionalSessionIdForRemote(input);
128
+ if (resolvedSessionId) {
129
+ const remote = await this.invokeRemote(resolvedSessionId, "telegramMcp.browser.listTabsRemote", {
130
+ ...input,
131
+ session_id: resolvedSessionId,
132
+ });
133
+ if (remote) {
134
+ return remote;
135
+ }
136
+ }
137
+ const instances = this.listFirefoxAttachInstances();
138
+ const instance = input.instance_id?.trim()
139
+ ? instances.find((item) => item.instanceId === input.instance_id?.trim())
140
+ : instances.length === 1
141
+ ? instances[0]
142
+ : undefined;
143
+ if (!instance) {
144
+ if (input.instance_id?.trim()) {
145
+ throw new Error(`Attached browser instance '${input.instance_id.trim()}' was not found.`);
146
+ }
147
+ throw new Error("Attached browser instance is ambiguous. Pass instance_id or keep exactly one connected browser instance.");
148
+ }
149
+ const selectedAttachment = resolvedSessionId
150
+ ? await this.getAttachedBrowserAttachment(resolvedSessionId)
151
+ : null;
152
+ return {
153
+ ...(resolvedSessionId ? { session_id: resolvedSessionId } : {}),
154
+ instance_id: instance.instanceId,
155
+ total: instance.tabs.length,
156
+ tabs: instance.tabs.map((tab) => ({
157
+ tab_id: tab.tab_id,
158
+ ...(typeof tab.window_id === "number" ? { window_id: tab.window_id } : {}),
159
+ active: tab.active,
160
+ ...(selectedAttachment &&
161
+ selectedAttachment.instanceId === instance.instanceId &&
162
+ selectedAttachment.tabId === tab.tab_id
163
+ ? { selected: true }
164
+ : {}),
165
+ title: tab.title,
166
+ url: tab.url,
167
+ ...(tab.status ? { status: tab.status } : {}),
168
+ })),
169
+ };
170
+ }
171
+ async attachActiveTab(input) {
172
+ const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
173
+ const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
174
+ const remote = await this.invokeRemote(normalizedSessionId, "telegramMcp.browser.attachActiveTabRemote", {
175
+ ...input,
176
+ session_id: normalizedSessionId,
177
+ });
178
+ if (remote) {
179
+ return remote;
180
+ }
181
+ const instances = this.listFirefoxAttachInstances();
182
+ const instance = input.instance_id?.trim()
183
+ ? instances.find((item) => item.instanceId === input.instance_id?.trim())
184
+ : instances.length === 1
185
+ ? instances[0]
186
+ : undefined;
187
+ if (!instance) {
188
+ if (input.instance_id?.trim()) {
189
+ throw new Error(`Attached browser instance '${input.instance_id.trim()}' was not found.`);
190
+ }
191
+ throw new Error("Attached browser instance is ambiguous. Pass instance_id or keep exactly one connected browser instance.");
192
+ }
193
+ const activeTab = instance.activeTab;
194
+ if (!activeTab) {
195
+ throw new Error(`Attached browser instance '${instance.instanceId}' does not expose an active tab.`);
196
+ }
197
+ return await this.attachTab({
198
+ session_id: normalizedSessionId,
199
+ instance_id: instance.instanceId,
200
+ tab_id: activeTab.tab_id,
201
+ });
202
+ }
203
+ async attachTab(input) {
204
+ const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
205
+ const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
206
+ const remote = await this.invokeRemote(normalizedSessionId, "telegramMcp.browser.attachTabRemote", {
207
+ ...input,
208
+ session_id: normalizedSessionId,
209
+ });
210
+ if (remote) {
211
+ return remote;
212
+ }
213
+ if (!this.firefoxAttachRegistry) {
214
+ throw new Error("Attached browser backend is not available.");
215
+ }
216
+ const instances = this.listFirefoxAttachInstances();
217
+ const instance = input.instance_id?.trim()
218
+ ? instances.find((item) => item.instanceId === input.instance_id?.trim())
219
+ : instances.length === 1
220
+ ? instances[0]
221
+ : undefined;
222
+ if (!instance) {
223
+ if (input.instance_id?.trim()) {
224
+ throw new Error(`Attached browser instance '${input.instance_id.trim()}' was not found.`);
225
+ }
226
+ throw new Error("Attached browser instance is ambiguous. Pass instance_id or keep exactly one connected browser instance.");
227
+ }
228
+ const tab = instance.tabs.find((item) => item.tab_id === input.tab_id);
229
+ if (!tab) {
230
+ throw new Error(`Tab '${input.tab_id}' was not found in attached browser instance '${instance.instanceId}'.`);
231
+ }
232
+ const result = await this.firefoxAttachRegistry.invokeTabAction({
233
+ instanceId: instance.instanceId,
234
+ tabId: tab.tab_id,
235
+ action: "attach",
236
+ });
237
+ const attachedAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
238
+ await this.maintenanceStore.setBrowserAttachment({
239
+ sessionId: normalizedSessionId,
240
+ backend: "firefox-attached",
241
+ instanceId: instance.instanceId,
242
+ tabId: tab.tab_id,
243
+ attachedAt,
244
+ ...(result?.title
245
+ ? { title: String(result.title) }
246
+ : tab.title
247
+ ? { title: tab.title }
248
+ : {}),
249
+ ...(result?.url
250
+ ? { url: String(result.url) }
251
+ : tab.url
252
+ ? { url: tab.url }
253
+ : {}),
254
+ });
255
+ return {
256
+ session_id: normalizedSessionId,
257
+ backend: "firefox-attached",
258
+ instance_id: instance.instanceId,
259
+ tab_id: tab.tab_id,
260
+ attached_at: attachedAt,
261
+ ...(result?.title
262
+ ? { title: String(result.title) }
263
+ : tab.title
264
+ ? { title: tab.title }
265
+ : {}),
266
+ ...(result?.url
267
+ ? { url: String(result.url) }
268
+ : tab.url
269
+ ? { url: tab.url }
270
+ : {}),
271
+ };
272
+ }
273
+ async detachTab(input) {
274
+ const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
275
+ const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
276
+ const remote = await this.invokeRemote(normalizedSessionId, "telegramMcp.browser.detachTabRemote", {
277
+ ...input,
278
+ session_id: normalizedSessionId,
279
+ });
280
+ if (remote) {
281
+ return remote;
282
+ }
283
+ const attachment = await this.getAttachedBrowserAttachment(normalizedSessionId);
284
+ if (attachment && this.firefoxAttachRegistry) {
285
+ try {
286
+ await this.firefoxAttachRegistry.invokeTabAction({
287
+ instanceId: attachment.instanceId,
288
+ tabId: attachment.tabId,
289
+ action: "detach",
290
+ });
291
+ }
292
+ catch {
293
+ // Clearing local state is still the source of truth.
294
+ }
295
+ }
296
+ await this.maintenanceStore.clearBrowserAttachment(normalizedSessionId);
297
+ return {
298
+ session_id: normalizedSessionId,
299
+ detached: true,
300
+ };
301
+ }
302
+ async startRecording(input) {
303
+ const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
304
+ const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
305
+ const remote = await this.invokeRemote(normalizedSessionId, "telegramMcp.browser.startRecordingRemote", {
306
+ ...input,
307
+ session_id: normalizedSessionId,
308
+ });
309
+ if (remote) {
310
+ return remote;
311
+ }
312
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
313
+ if (!attached) {
314
+ throw new Error("No attached browser tab is selected for this session. Select a tab in the browser attach panel first.");
315
+ }
316
+ if (!this.firefoxAttachRegistry) {
317
+ throw new Error("Firefox attach backend is not available.");
318
+ }
319
+ const record = await this.firefoxAttachRegistry.startRecording({
320
+ sessionId: normalizedSessionId,
321
+ instanceId: input.instance_id?.trim() || attached.instanceId,
322
+ tabId: attached.tabId,
323
+ tabTitle: attached.title?.trim() || "attached-tab",
324
+ ...(attached.url ? { tabUrl: attached.url } : {}),
325
+ });
326
+ return this.mapRecordingRecord(normalizedSessionId, record);
327
+ }
328
+ async stopRecording(input) {
329
+ const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
330
+ const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
331
+ const remote = await this.invokeRemote(normalizedSessionId, "telegramMcp.browser.stopRecordingRemote", {
332
+ ...input,
333
+ session_id: normalizedSessionId,
334
+ });
335
+ if (remote) {
336
+ return remote;
337
+ }
338
+ if (!this.firefoxAttachRegistry) {
339
+ throw new Error("Firefox attach backend is not available.");
340
+ }
341
+ const record = await this.firefoxAttachRegistry.stopRecording({
342
+ sessionId: normalizedSessionId,
343
+ });
344
+ if (!record) {
345
+ return {
346
+ session_id: normalizedSessionId,
347
+ stopped: false,
348
+ };
349
+ }
350
+ return {
351
+ session_id: normalizedSessionId,
352
+ stopped: true,
353
+ recording_id: record.recordingId,
354
+ bundle_dir_name: record.bundleDirName,
355
+ bundle_relative_path: record.bundleRelativePath,
356
+ bundle_path: record.bundlePath,
357
+ ...(record.stoppedAt ? { stopped_at: record.stoppedAt } : {}),
358
+ };
359
+ }
360
+ async getRecordingStatus(input) {
361
+ const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
362
+ const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
363
+ const remote = await this.invokeRemote(normalizedSessionId, "telegramMcp.browser.getRecordingStatusRemote", {
364
+ ...input,
365
+ session_id: normalizedSessionId,
366
+ });
367
+ if (remote) {
368
+ return remote;
369
+ }
370
+ if (!this.firefoxAttachRegistry) {
371
+ throw new Error("Firefox attach backend is not available.");
372
+ }
373
+ const record = await this.firefoxAttachRegistry.getRecordingStatus(normalizedSessionId);
374
+ return {
375
+ session_id: normalizedSessionId,
376
+ active: record?.status === "recording",
377
+ ...(record ? { recording: this.mapRecordingStatusRecord(record) } : {}),
378
+ };
77
379
  }
78
380
  async open(input) {
79
381
  const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
@@ -115,7 +417,7 @@ class BrowserService {
115
417
  });
116
418
  state.currentUrl = state.page.url();
117
419
  state.title = await state.page.title();
118
- state.lastUsedAt = new Date().toISOString();
420
+ state.lastUsedAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
119
421
  this.logger.info("Browser page opened", {
120
422
  sessionId: resolved.sessionId,
121
423
  normalizedSessionId,
@@ -149,8 +451,40 @@ class BrowserService {
149
451
  return remote;
150
452
  }
151
453
  this.ensureEnabled();
454
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
455
+ if (attached) {
456
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
457
+ action: "get_logs",
458
+ payload: {
459
+ limit: input.limit,
460
+ },
461
+ });
462
+ const consoleMessages = Array.isArray(result?.console_messages)
463
+ ? result.console_messages
464
+ : [];
465
+ return {
466
+ session_id: normalizedSessionId,
467
+ total: typeof result?.console_total === "number"
468
+ ? result.console_total
469
+ : consoleMessages.length,
470
+ messages: consoleMessages.map((message) => ({
471
+ type: typeof message?.type === "string"
472
+ ? message.type
473
+ : "log",
474
+ text: typeof message?.text === "string"
475
+ ? message.text
476
+ : "",
477
+ ...(typeof message?.location === "string"
478
+ ? { location: message.location }
479
+ : {}),
480
+ timestamp: typeof message?.timestamp === "string"
481
+ ? message.timestamp
482
+ : (0, localTimestamp_1.formatLocalTimestamp)(new Date()),
483
+ })),
484
+ };
485
+ }
152
486
  const { sessionId, state } = await this.requireSessionState(input);
153
- state.lastUsedAt = new Date().toISOString();
487
+ state.lastUsedAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
154
488
  return {
155
489
  session_id: sessionId,
156
490
  total: state.consoleMessages.length,
@@ -173,6 +507,22 @@ class BrowserService {
173
507
  return remote;
174
508
  }
175
509
  this.ensureEnabled();
510
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
511
+ if (attached) {
512
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
513
+ action: "click",
514
+ payload: this.buildAttachedLocatorPayload(input),
515
+ });
516
+ return {
517
+ session_id: normalizedSessionId,
518
+ clicked: true,
519
+ ...(input.ai_tag ? { ai_tag: input.ai_tag } : {}),
520
+ ...(input.selector ? { selector: input.selector } : {}),
521
+ ...(input.text ? { text: input.text } : {}),
522
+ url: String(result?.url || attached.url || ""),
523
+ ...(result?.title ? { title: String(result.title) } : {}),
524
+ };
525
+ }
176
526
  const { sessionId, state } = await this.requireSessionState(input);
177
527
  const locator = this.resolveLocator(state.page, input);
178
528
  await locator.click({
@@ -180,7 +530,7 @@ class BrowserService {
180
530
  });
181
531
  state.currentUrl = state.page.url();
182
532
  state.title = await state.page.title().catch(() => state.title);
183
- state.lastUsedAt = new Date().toISOString();
533
+ state.lastUsedAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
184
534
  return {
185
535
  session_id: sessionId,
186
536
  clicked: true,
@@ -202,6 +552,26 @@ class BrowserService {
202
552
  return remote;
203
553
  }
204
554
  this.ensureEnabled();
555
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
556
+ if (attached) {
557
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
558
+ action: "fill",
559
+ payload: {
560
+ ...this.buildAttachedLocatorPayload(input),
561
+ value: input.value,
562
+ },
563
+ });
564
+ return {
565
+ session_id: normalizedSessionId,
566
+ filled: true,
567
+ ...(input.ai_tag ? { ai_tag: input.ai_tag } : {}),
568
+ ...(input.selector ? { selector: input.selector } : {}),
569
+ ...(input.text ? { text: input.text } : {}),
570
+ value_length: input.value.length,
571
+ url: String(result?.url || attached.url || ""),
572
+ ...(result?.title ? { title: String(result.title) } : {}),
573
+ };
574
+ }
205
575
  const { sessionId, state } = await this.requireSessionState(input);
206
576
  const locator = this.resolveLocator(state.page, input);
207
577
  await locator.fill(input.value, {
@@ -209,7 +579,7 @@ class BrowserService {
209
579
  });
210
580
  state.currentUrl = state.page.url();
211
581
  state.title = await state.page.title().catch(() => state.title);
212
- state.lastUsedAt = new Date().toISOString();
582
+ state.lastUsedAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
213
583
  return {
214
584
  session_id: sessionId,
215
585
  filled: true,
@@ -232,6 +602,26 @@ class BrowserService {
232
602
  return remote;
233
603
  }
234
604
  this.ensureEnabled();
605
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
606
+ if (attached) {
607
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
608
+ action: "press",
609
+ payload: {
610
+ ...this.buildAttachedLocatorPayload(input),
611
+ key: input.key,
612
+ },
613
+ });
614
+ return {
615
+ session_id: normalizedSessionId,
616
+ pressed: true,
617
+ key: input.key,
618
+ ...(input.ai_tag ? { ai_tag: input.ai_tag } : {}),
619
+ ...(input.selector ? { selector: input.selector } : {}),
620
+ ...(input.text ? { text: input.text } : {}),
621
+ url: String(result?.url || attached.url || ""),
622
+ ...(result?.title ? { title: String(result.title) } : {}),
623
+ };
624
+ }
235
625
  const { sessionId, state } = await this.requireSessionState(input);
236
626
  if (input.selector || input.text) {
237
627
  const locator = this.resolveLocator(state.page, input);
@@ -244,7 +634,7 @@ class BrowserService {
244
634
  }
245
635
  state.currentUrl = state.page.url();
246
636
  state.title = await state.page.title().catch(() => state.title);
247
- state.lastUsedAt = new Date().toISOString();
637
+ state.lastUsedAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
248
638
  return {
249
639
  session_id: sessionId,
250
640
  pressed: true,
@@ -256,6 +646,72 @@ class BrowserService {
256
646
  ...(state.title ? { title: state.title } : {}),
257
647
  };
258
648
  }
649
+ async injectScript(input) {
650
+ const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
651
+ const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
652
+ const remote = await this.invokeRemote(normalizedSessionId, "telegramMcp.browser.injectScriptRemote", {
653
+ ...input,
654
+ session_id: normalizedSessionId,
655
+ });
656
+ if (remote) {
657
+ return remote;
658
+ }
659
+ this.ensureEnabled();
660
+ const namespace = input.namespace?.trim() || "TELLY";
661
+ const sourcePath = input.file_path?.trim()
662
+ ? node_path_1.default.resolve(input.file_path.trim())
663
+ : null;
664
+ if (!input.source?.trim().length && sourcePath) {
665
+ const sourceStats = await (0, promises_1.stat)(sourcePath);
666
+ (0, bodyLimits_1.assertBodySize)(sourceStats.size);
667
+ }
668
+ const source = input.source?.trim().length
669
+ ? input.source
670
+ : sourcePath
671
+ ? await (0, promises_1.readFile)(sourcePath, "utf8")
672
+ : null;
673
+ if (!source) {
674
+ throw new Error("Provide source or file_path.");
675
+ }
676
+ (0, bodyLimits_1.assertStringBodySize)(source);
677
+ const sourceType = input.source?.trim().length ? "inline" : "file";
678
+ const wrappedSource = this.wrapInjectedScript(source, namespace);
679
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
680
+ if (attached) {
681
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
682
+ action: "inject_script",
683
+ payload: {
684
+ namespace,
685
+ source,
686
+ },
687
+ });
688
+ return {
689
+ session_id: normalizedSessionId,
690
+ injected: true,
691
+ namespace,
692
+ source_type: sourceType,
693
+ bytes: Buffer.byteLength(source, "utf8"),
694
+ url: String(result?.url || attached.url || ""),
695
+ ...(result?.title ? { title: String(result.title) } : {}),
696
+ };
697
+ }
698
+ const { sessionId, state } = await this.requireSessionState(input);
699
+ await state.page.addScriptTag({
700
+ content: wrappedSource,
701
+ });
702
+ state.currentUrl = state.page.url();
703
+ state.title = await state.page.title().catch(() => state.title);
704
+ state.lastUsedAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
705
+ return {
706
+ session_id: sessionId,
707
+ injected: true,
708
+ namespace,
709
+ source_type: sourceType,
710
+ bytes: Buffer.byteLength(source, "utf8"),
711
+ url: state.currentUrl,
712
+ ...(state.title ? { title: state.title } : {}),
713
+ };
714
+ }
259
715
  async reload(input) {
260
716
  const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
261
717
  const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
@@ -267,6 +723,18 @@ class BrowserService {
267
723
  return remote;
268
724
  }
269
725
  this.ensureEnabled();
726
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
727
+ if (attached) {
728
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
729
+ action: "reload",
730
+ });
731
+ return {
732
+ session_id: normalizedSessionId,
733
+ reloaded: true,
734
+ url: String(result?.url || attached.url || ""),
735
+ ...(result?.title ? { title: String(result.title) } : {}),
736
+ };
737
+ }
270
738
  const { sessionId, state } = await this.requireSessionState(input);
271
739
  const waitUntil = (input.wait_until ??
272
740
  this.config.browser.waitUntil);
@@ -276,7 +744,7 @@ class BrowserService {
276
744
  });
277
745
  state.currentUrl = state.page.url();
278
746
  state.title = await state.page.title().catch(() => state.title);
279
- state.lastUsedAt = new Date().toISOString();
747
+ state.lastUsedAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
280
748
  this.logger.info("Browser page reloaded", {
281
749
  sessionId,
282
750
  url: state.currentUrl,
@@ -301,6 +769,30 @@ class BrowserService {
301
769
  return remote;
302
770
  }
303
771
  this.ensureEnabled();
772
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
773
+ if (attached) {
774
+ const waitState = input.state ?? "visible";
775
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
776
+ action: "wait_for",
777
+ payload: {
778
+ ...this.buildAttachedLocatorPayload(input),
779
+ state: waitState,
780
+ ...(typeof input.timeout_ms === "number"
781
+ ? { timeout_ms: input.timeout_ms }
782
+ : {}),
783
+ },
784
+ });
785
+ return {
786
+ session_id: normalizedSessionId,
787
+ waited: true,
788
+ state: waitState,
789
+ ...(input.ai_tag ? { ai_tag: input.ai_tag } : {}),
790
+ ...(input.selector ? { selector: input.selector } : {}),
791
+ ...(input.text ? { text: input.text } : {}),
792
+ url: String(result?.url || attached.url || ""),
793
+ ...(result?.title ? { title: String(result.title) } : {}),
794
+ };
795
+ }
304
796
  const { sessionId, state } = await this.requireSessionState(input);
305
797
  const locator = this.resolveLocator(state.page, input);
306
798
  const waitState = input.state ?? "visible";
@@ -310,7 +802,7 @@ class BrowserService {
310
802
  });
311
803
  state.currentUrl = state.page.url();
312
804
  state.title = await state.page.title().catch(() => state.title);
313
- state.lastUsedAt = new Date().toISOString();
805
+ state.lastUsedAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
314
806
  return {
315
807
  session_id: sessionId,
316
808
  waited: true,
@@ -333,6 +825,32 @@ class BrowserService {
333
825
  return remote;
334
826
  }
335
827
  this.ensureEnabled();
828
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
829
+ if (attached) {
830
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
831
+ action: "wait_for_url",
832
+ payload: {
833
+ ...(input.url?.trim() ? { url: input.url.trim() } : {}),
834
+ ...(input.url_contains?.trim()
835
+ ? { url_contains: input.url_contains.trim() }
836
+ : {}),
837
+ ...(typeof input.timeout_ms === "number"
838
+ ? { timeout_ms: input.timeout_ms }
839
+ : {}),
840
+ },
841
+ });
842
+ return {
843
+ session_id: normalizedSessionId,
844
+ waited: true,
845
+ matched: input.url?.trim() ? "url" : "url_contains",
846
+ ...(input.url?.trim() ? { url: input.url.trim() } : {}),
847
+ ...(input.url_contains?.trim()
848
+ ? { url_contains: input.url_contains.trim() }
849
+ : {}),
850
+ current_url: String(result?.url || attached.url || ""),
851
+ ...(result?.title ? { title: String(result.title) } : {}),
852
+ };
853
+ }
336
854
  const { sessionId, state } = await this.requireSessionState(input);
337
855
  const timeout = this.resolveTimeoutMs(input.timeout_ms);
338
856
  if (input.url?.trim()) {
@@ -351,7 +869,7 @@ class BrowserService {
351
869
  }
352
870
  state.currentUrl = state.page.url();
353
871
  state.title = await state.page.title().catch(() => state.title);
354
- state.lastUsedAt = new Date().toISOString();
872
+ state.lastUsedAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
355
873
  return {
356
874
  session_id: sessionId,
357
875
  waited: true,
@@ -375,8 +893,37 @@ class BrowserService {
375
893
  return remote;
376
894
  }
377
895
  this.ensureEnabled();
896
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
897
+ if (attached) {
898
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
899
+ action: "get_logs",
900
+ payload: {
901
+ limit: input.limit,
902
+ },
903
+ });
904
+ const pageErrors = Array.isArray(result?.page_errors)
905
+ ? result.page_errors
906
+ : [];
907
+ return {
908
+ session_id: normalizedSessionId,
909
+ total: typeof result?.page_error_total === "number"
910
+ ? result.page_error_total
911
+ : pageErrors.length,
912
+ errors: pageErrors.map((error) => ({
913
+ message: typeof error?.message === "string"
914
+ ? error.message
915
+ : "",
916
+ ...(typeof error?.stack === "string"
917
+ ? { stack: error.stack }
918
+ : {}),
919
+ timestamp: typeof error?.timestamp === "string"
920
+ ? error.timestamp
921
+ : (0, localTimestamp_1.formatLocalTimestamp)(new Date()),
922
+ })),
923
+ };
924
+ }
378
925
  const { sessionId, state } = await this.requireSessionState(input);
379
- state.lastUsedAt = new Date().toISOString();
926
+ state.lastUsedAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
380
927
  return {
381
928
  session_id: sessionId,
382
929
  total: state.pageErrors.length,
@@ -398,8 +945,46 @@ class BrowserService {
398
945
  return remote;
399
946
  }
400
947
  this.ensureEnabled();
948
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
949
+ if (attached) {
950
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
951
+ action: "get_logs",
952
+ payload: {
953
+ limit: input.limit,
954
+ },
955
+ });
956
+ const failures = Array.isArray(result?.network_failures)
957
+ ? result.network_failures
958
+ : [];
959
+ return {
960
+ session_id: normalizedSessionId,
961
+ total: typeof result?.network_failure_total === "number"
962
+ ? result.network_failure_total
963
+ : failures.length,
964
+ failures: failures.map((failure) => ({
965
+ url: typeof failure?.url === "string"
966
+ ? failure.url
967
+ : "",
968
+ method: typeof failure?.method === "string"
969
+ ? failure.method
970
+ : "GET",
971
+ ...(typeof failure?.status === "number"
972
+ ? { status: failure.status }
973
+ : {}),
974
+ ...(typeof failure?.error_text === "string"
975
+ ? { error_text: failure.error_text }
976
+ : {}),
977
+ ...(typeof failure?.resource_type === "string"
978
+ ? { resource_type: failure.resource_type }
979
+ : {}),
980
+ timestamp: typeof failure?.timestamp === "string"
981
+ ? failure.timestamp
982
+ : (0, localTimestamp_1.formatLocalTimestamp)(new Date()),
983
+ })),
984
+ };
985
+ }
401
986
  const { sessionId, state } = await this.requireSessionState(input);
402
- state.lastUsedAt = new Date().toISOString();
987
+ state.lastUsedAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
403
988
  return {
404
989
  session_id: sessionId,
405
990
  total: state.networkFailures.length,
@@ -426,6 +1011,25 @@ class BrowserService {
426
1011
  return remote;
427
1012
  }
428
1013
  this.ensureEnabled();
1014
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
1015
+ if (attached) {
1016
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
1017
+ action: "clear_logs",
1018
+ });
1019
+ return {
1020
+ session_id: normalizedSessionId,
1021
+ cleared: true,
1022
+ console_messages_cleared: typeof result?.console_messages_cleared === "number"
1023
+ ? result.console_messages_cleared
1024
+ : 0,
1025
+ page_errors_cleared: typeof result?.page_errors_cleared === "number"
1026
+ ? result.page_errors_cleared
1027
+ : 0,
1028
+ network_failures_cleared: typeof result?.network_failures_cleared === "number"
1029
+ ? result.network_failures_cleared
1030
+ : 0,
1031
+ };
1032
+ }
429
1033
  const { sessionId, state } = await this.requireSessionState(input);
430
1034
  const consoleMessagesCleared = state.consoleMessages.length;
431
1035
  const pageErrorsCleared = state.pageErrors.length;
@@ -433,7 +1037,7 @@ class BrowserService {
433
1037
  state.consoleMessages = [];
434
1038
  state.pageErrors = [];
435
1039
  state.networkFailures = [];
436
- state.lastUsedAt = new Date().toISOString();
1040
+ state.lastUsedAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
437
1041
  return {
438
1042
  session_id: sessionId,
439
1043
  cleared: true,
@@ -453,6 +1057,37 @@ class BrowserService {
453
1057
  return remote;
454
1058
  }
455
1059
  this.ensureEnabled();
1060
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
1061
+ if (attached) {
1062
+ const selector = input.selector?.trim() || "body";
1063
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
1064
+ action: "dom",
1065
+ payload: {
1066
+ selector,
1067
+ include_html: input.include_html !== false,
1068
+ include_text: input.include_text !== false,
1069
+ },
1070
+ });
1071
+ return {
1072
+ session_id: normalizedSessionId,
1073
+ selector,
1074
+ found: result?.found === true,
1075
+ ...(result?.url ? { url: String(result.url) } : {}),
1076
+ ...(result?.title ? { title: String(result.title) } : {}),
1077
+ ...(typeof result?.outer_html === "string"
1078
+ ? { outer_html: result.outer_html }
1079
+ : {}),
1080
+ ...(typeof result?.text_content === "string"
1081
+ ? { text_content: result.text_content }
1082
+ : {}),
1083
+ ...(typeof result?.visible === "boolean"
1084
+ ? { visible: result.visible }
1085
+ : {}),
1086
+ ...(result?.attributes && typeof result.attributes === "object"
1087
+ ? { attributes: result.attributes }
1088
+ : {}),
1089
+ };
1090
+ }
456
1091
  const { sessionId, state } = await this.requireSessionState(input);
457
1092
  const selector = input.selector?.trim() || "body";
458
1093
  const snapshot = await state.page
@@ -487,7 +1122,7 @@ class BrowserService {
487
1122
  includeText: input.include_text !== false,
488
1123
  })
489
1124
  .catch(() => ({ found: false }));
490
- state.lastUsedAt = new Date().toISOString();
1125
+ state.lastUsedAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
491
1126
  state.currentUrl = state.page.url();
492
1127
  state.title = await state.page.title().catch(() => state.title);
493
1128
  return {
@@ -517,6 +1152,57 @@ class BrowserService {
517
1152
  return remote;
518
1153
  }
519
1154
  this.ensureEnabled();
1155
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
1156
+ if (attached) {
1157
+ const properties = input.properties?.length
1158
+ ? input.properties
1159
+ : [
1160
+ "display",
1161
+ "position",
1162
+ "visibility",
1163
+ "opacity",
1164
+ "color",
1165
+ "background-color",
1166
+ "font-size",
1167
+ "z-index",
1168
+ "overflow",
1169
+ ];
1170
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
1171
+ action: "computed_style",
1172
+ payload: {
1173
+ selector: input.selector,
1174
+ properties,
1175
+ },
1176
+ });
1177
+ return {
1178
+ session_id: normalizedSessionId,
1179
+ selector: input.selector,
1180
+ found: result?.found === true,
1181
+ ...(result?.url ? { url: String(result.url) } : {}),
1182
+ ...(result?.title ? { title: String(result.title) } : {}),
1183
+ ...(typeof result?.visible === "boolean"
1184
+ ? { visible: result.visible }
1185
+ : {}),
1186
+ ...(result?.styles && typeof result.styles === "object"
1187
+ ? { styles: result.styles }
1188
+ : {}),
1189
+ ...(result?.box &&
1190
+ typeof result.box === "object" &&
1191
+ typeof result.box.x === "number" &&
1192
+ typeof result.box.y === "number" &&
1193
+ typeof result.box.width === "number" &&
1194
+ typeof result.box.height === "number"
1195
+ ? {
1196
+ box: {
1197
+ x: Number(result.box.x),
1198
+ y: Number(result.box.y),
1199
+ width: Number(result.box.width),
1200
+ height: Number(result.box.height),
1201
+ },
1202
+ }
1203
+ : {}),
1204
+ };
1205
+ }
520
1206
  const { sessionId, state } = await this.requireSessionState(input);
521
1207
  const properties = input.properties?.length
522
1208
  ? input.properties
@@ -556,7 +1242,7 @@ class BrowserService {
556
1242
  };
557
1243
  }, properties)
558
1244
  .catch(() => ({ found: false }));
559
- state.lastUsedAt = new Date().toISOString();
1245
+ state.lastUsedAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
560
1246
  state.currentUrl = state.page.url();
561
1247
  state.title = await state.page.title().catch(() => state.title);
562
1248
  return {
@@ -583,6 +1269,75 @@ class BrowserService {
583
1269
  return remote;
584
1270
  }
585
1271
  this.ensureEnabled();
1272
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
1273
+ if (attached) {
1274
+ const session = await this.sessionStore.getSession(normalizedSessionId);
1275
+ const fileName = sanitizeScreenshotName(input.file_name);
1276
+ const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
1277
+ action: "screenshot",
1278
+ });
1279
+ const pngBase64 = typeof result?.png_base64 === "string" ? result.png_base64 : "";
1280
+ if (!pngBase64) {
1281
+ throw new Error("Attached browser screenshot did not return PNG data.");
1282
+ }
1283
+ const pngBuffer = Buffer.from(pngBase64, "base64");
1284
+ const workspaceDir = this.objectStore.resolveWorkspaceDir(session);
1285
+ const exchangeDir = node_path_1.default.resolve(workspaceDir, this.config.exchange.dir);
1286
+ const storedFile = await this.objectStore.storeFile({
1287
+ session,
1288
+ sessionId: normalizedSessionId,
1289
+ source: "browser-screenshot",
1290
+ relativePath: buildDatedRelativePath(fileName),
1291
+ content: pngBuffer,
1292
+ mimeType: "image/png",
1293
+ });
1294
+ await this.xchangeFileMetaStore.setXchangeFileMeta({
1295
+ sessionId: normalizedSessionId,
1296
+ filePath: storedFile.filePath,
1297
+ relativePath: storedFile.relativePath,
1298
+ source: "browser-screenshot",
1299
+ uploadedAt: (0, localTimestamp_1.formatLocalTimestamp)(new Date()),
1300
+ storageRef: storedFile.storageRef,
1301
+ bucketName: storedFile.bucketName,
1302
+ objectName: storedFile.objectName,
1303
+ vfsNodeId: storedFile.vfsNodeId,
1304
+ vfsPublicUrl: storedFile.vfsPublicUrl,
1305
+ vfsParentId: storedFile.vfsParentId,
1306
+ mimeType: "image/png",
1307
+ sizeBytes: storedFile.sizeBytes,
1308
+ ...(input.caption ? { caption: input.caption } : {}),
1309
+ });
1310
+ let telegramMessageId;
1311
+ if (input.send_to_telegram === true) {
1312
+ if (this.config.distributed.mode === "client") {
1313
+ telegramMessageId = await this.sendScreenshotToGatewayTelegramRoute({
1314
+ sessionId: normalizedSessionId,
1315
+ fileName,
1316
+ pngBuffer,
1317
+ ...(input.caption ? { caption: input.caption } : {}),
1318
+ });
1319
+ }
1320
+ else {
1321
+ const binding = await this.bindingStore.getBinding(normalizedSessionId);
1322
+ if (!binding) {
1323
+ throw new Error("Session is not linked to Telegram, so screenshot cannot be sent there.");
1324
+ }
1325
+ const sent = await this.telegramTransport.sendDocumentToChat(binding.telegramChatId, storedFile.filePath, input.caption);
1326
+ telegramMessageId = sent.messageId;
1327
+ }
1328
+ }
1329
+ return {
1330
+ session_id: normalizedSessionId,
1331
+ file_path: storedFile.filePath,
1332
+ workspace_dir: workspaceDir,
1333
+ exchange_dir: exchangeDir,
1334
+ ...(typeof telegramMessageId === "number"
1335
+ ? { telegram_message_id: telegramMessageId }
1336
+ : {}),
1337
+ ...(result?.url ? { url: String(result.url) } : {}),
1338
+ ...(result?.title ? { title: String(result.title) } : {}),
1339
+ };
1340
+ }
586
1341
  const { sessionId, state, session } = await this.requireSessionState(input);
587
1342
  const fileName = sanitizeScreenshotName(input.file_name);
588
1343
  const pngBuffer = input.selector?.trim()
@@ -609,7 +1364,7 @@ class BrowserService {
609
1364
  mimeType: "image/png",
610
1365
  });
611
1366
  const filePath = storedFile.filePath;
612
- state.lastUsedAt = new Date().toISOString();
1367
+ state.lastUsedAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
613
1368
  state.currentUrl = state.page.url();
614
1369
  state.title = await state.page.title().catch(() => state.title);
615
1370
  this.logger.info("Browser screenshot captured", {
@@ -623,7 +1378,7 @@ class BrowserService {
623
1378
  filePath,
624
1379
  relativePath: storedFile.relativePath,
625
1380
  source: "browser-screenshot",
626
- uploadedAt: new Date().toISOString(),
1381
+ uploadedAt: (0, localTimestamp_1.formatLocalTimestamp)(new Date()),
627
1382
  storageRef: storedFile.storageRef,
628
1383
  bucketName: storedFile.bucketName,
629
1384
  objectName: storedFile.objectName,
@@ -676,6 +1431,17 @@ class BrowserService {
676
1431
  return remote;
677
1432
  }
678
1433
  this.ensureEnabled();
1434
+ const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
1435
+ if (attached) {
1436
+ await this.runAttachedTabAction(normalizedSessionId, attached, {
1437
+ action: "close",
1438
+ });
1439
+ await this.maintenanceStore.clearBrowserAttachment(normalizedSessionId);
1440
+ return {
1441
+ session_id: normalizedSessionId,
1442
+ closed: true,
1443
+ };
1444
+ }
679
1445
  const state = this.sessionStates.get(normalizedSessionId);
680
1446
  if (state) {
681
1447
  await this.closeState(normalizedSessionId, state);
@@ -749,7 +1515,7 @@ class BrowserService {
749
1515
  const browser = await this.ensureBrowser();
750
1516
  const context = await browser.newContext(this.buildContextOptions(openInput));
751
1517
  const page = await context.newPage();
752
- const createdAt = new Date().toISOString();
1518
+ const createdAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
753
1519
  const state = {
754
1520
  context,
755
1521
  page,
@@ -766,14 +1532,14 @@ class BrowserService {
766
1532
  ...(formatConsoleLocation(message)
767
1533
  ? { location: formatConsoleLocation(message) }
768
1534
  : {}),
769
- timestamp: new Date().toISOString(),
1535
+ timestamp: (0, localTimestamp_1.formatLocalTimestamp)(new Date()),
770
1536
  }, this.config.browser.maxEvents);
771
1537
  });
772
1538
  page.on("pageerror", (error) => {
773
1539
  pushBounded(state.pageErrors, {
774
1540
  message: error.message,
775
1541
  ...(error.stack ? { stack: error.stack } : {}),
776
- timestamp: new Date().toISOString(),
1542
+ timestamp: (0, localTimestamp_1.formatLocalTimestamp)(new Date()),
777
1543
  }, this.config.browser.maxEvents);
778
1544
  });
779
1545
  page.on("requestfailed", (request) => {
@@ -823,7 +1589,7 @@ class BrowserService {
823
1589
  : {}),
824
1590
  ...(failure?.errorText ? { errorText: failure.errorText } : {}),
825
1591
  resourceType: request.resourceType(),
826
- timestamp: new Date().toISOString(),
1592
+ timestamp: (0, localTimestamp_1.formatLocalTimestamp)(new Date()),
827
1593
  }, this.config.browser.maxEvents);
828
1594
  }
829
1595
  async requireSessionState(input) {
@@ -875,6 +1641,107 @@ class BrowserService {
875
1641
  const localSession = await this.sessionStore.getSession(localSessionId);
876
1642
  return localSession ? localSessionId : trimmed;
877
1643
  }
1644
+ async resolveOptionalSessionIdForRemote(input) {
1645
+ const trimmed = input.session_id?.trim();
1646
+ if (!trimmed) {
1647
+ return undefined;
1648
+ }
1649
+ return await this.normalizeSessionIdForAccess(trimmed);
1650
+ }
1651
+ listFirefoxAttachInstances() {
1652
+ if (!this.firefoxAttachRegistry) {
1653
+ return [];
1654
+ }
1655
+ return this.firefoxAttachRegistry.listInstances();
1656
+ }
1657
+ buildAttachedLocatorPayload(input) {
1658
+ return {
1659
+ ...(input.ai_tag?.trim() ? { ai_tag: input.ai_tag.trim() } : {}),
1660
+ ...(input.selector?.trim() ? { selector: input.selector.trim() } : {}),
1661
+ ...(input.text?.trim() ? { text: input.text.trim() } : {}),
1662
+ ...(typeof input.exact === "boolean" ? { exact: input.exact } : {}),
1663
+ };
1664
+ }
1665
+ async getAttachedBrowserAttachment(sessionId) {
1666
+ return await this.maintenanceStore.getBrowserAttachment(sessionId);
1667
+ }
1668
+ mapRecordingRecord(sessionId, record) {
1669
+ return {
1670
+ session_id: sessionId,
1671
+ started: true,
1672
+ backend: "firefox-attached",
1673
+ recording_id: record.recordingId,
1674
+ instance_id: record.instanceId,
1675
+ tab_id: record.tabId,
1676
+ ...(record.tabTitle ? { tab_title: record.tabTitle } : {}),
1677
+ ...(record.tabUrl ? { tab_url: record.tabUrl } : {}),
1678
+ bundle_dir_name: record.bundleDirName,
1679
+ bundle_relative_path: record.bundleRelativePath,
1680
+ bundle_path: record.bundlePath,
1681
+ started_at: record.startedAt,
1682
+ };
1683
+ }
1684
+ mapRecordingStatusRecord(record) {
1685
+ return {
1686
+ backend: "firefox-attached",
1687
+ recording_id: record.recordingId,
1688
+ instance_id: record.instanceId,
1689
+ tab_id: record.tabId,
1690
+ ...(record.tabTitle ? { tab_title: record.tabTitle } : {}),
1691
+ ...(record.tabUrl ? { tab_url: record.tabUrl } : {}),
1692
+ bundle_dir_name: record.bundleDirName,
1693
+ bundle_relative_path: record.bundleRelativePath,
1694
+ bundle_path: record.bundlePath,
1695
+ started_at: record.startedAt,
1696
+ ...(record.stoppedAt ? { stopped_at: record.stoppedAt } : {}),
1697
+ status: record.status,
1698
+ event_count: record.eventCount,
1699
+ ...(record.lastEventAt ? { last_event_at: record.lastEventAt } : {}),
1700
+ };
1701
+ }
1702
+ async runAttachedTabAction(sessionId, attachment, input) {
1703
+ if (!this.firefoxAttachRegistry) {
1704
+ throw new Error("Attached browser backend is not available.");
1705
+ }
1706
+ const result = await this.firefoxAttachRegistry.invokeTabAction({
1707
+ instanceId: attachment.instanceId,
1708
+ tabId: attachment.tabId,
1709
+ action: input.action,
1710
+ payload: input.payload,
1711
+ });
1712
+ await this.maintenanceStore.setBrowserAttachment({
1713
+ ...attachment,
1714
+ ...(result?.title ? { title: String(result.title) } : {}),
1715
+ ...(result?.url ? { url: String(result.url) } : {}),
1716
+ });
1717
+ this.logger.info("Attached browser tab action executed", {
1718
+ sessionId,
1719
+ instanceId: attachment.instanceId,
1720
+ tabId: attachment.tabId,
1721
+ action: input.action,
1722
+ });
1723
+ return result;
1724
+ }
1725
+ wrapInjectedScript(source, namespace) {
1726
+ return `const __tellyNamespace = ${JSON.stringify(namespace)};
1727
+ window[__tellyNamespace] = window[__tellyNamespace] || {};
1728
+ var TELLY = window[__tellyNamespace];
1729
+ const __tellyBeforeKeys = new Set(Object.getOwnPropertyNames(window));
1730
+ ${source}
1731
+ for (const __tellyKey of Object.getOwnPropertyNames(window)) {
1732
+ if (__tellyBeforeKeys.has(__tellyKey)) {
1733
+ continue;
1734
+ }
1735
+ if (__tellyKey === __tellyNamespace) {
1736
+ continue;
1737
+ }
1738
+ try {
1739
+ window[__tellyNamespace][__tellyKey] = window[__tellyKey];
1740
+ } catch {
1741
+ // ignore unassignable globals
1742
+ }
1743
+ }`;
1744
+ }
878
1745
  async sendScreenshotToGatewayTelegramRoute(input) {
879
1746
  if (!this.config.distributed.gatewayPublicUrl) {
880
1747
  throw new Error("send_to_telegram on client nodes requires GATEWAY_PUBLIC_URL.");