@deadragdoll/tellymcp 0.0.1

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 (124) hide show
  1. package/.env.example.client +93 -0
  2. package/.env.example.gateway +120 -0
  3. package/CHANGELOG.md +155 -0
  4. package/LICENSE +21 -0
  5. package/README-ru.md +338 -0
  6. package/README.md +1262 -0
  7. package/STANDALONE-ru.md +266 -0
  8. package/STANDALONE.md +266 -0
  9. package/TOOLS.md +1296 -0
  10. package/config/templates/env.both.template +83 -0
  11. package/config/templates/env.client.template +60 -0
  12. package/config/templates/env.gateway.template +82 -0
  13. package/dist/cli.js +636 -0
  14. package/dist/index.js +17 -0
  15. package/dist/lib/logfeed/store.js +52 -0
  16. package/dist/lib/middlewares/tracer.js +172 -0
  17. package/dist/lib/mixins/db.js +267 -0
  18. package/dist/lib/mixins/logfeed.js +34 -0
  19. package/dist/lib/mixins/session.errors.js +142 -0
  20. package/dist/lib/moleculer.js +2 -0
  21. package/dist/lib/trace.js +147 -0
  22. package/dist/lib/traceContext.js +116 -0
  23. package/dist/moleculer.config.js +274 -0
  24. package/dist/services/features/telegram-mcp/approval.service.js +33 -0
  25. package/dist/services/features/telegram-mcp/browser.service.js +42 -0
  26. package/dist/services/features/telegram-mcp/collaboration.service.js +53 -0
  27. package/dist/services/features/telegram-mcp/ensuredb.service.js +337 -0
  28. package/dist/services/features/telegram-mcp/gateway-delivery.service.js +378 -0
  29. package/dist/services/features/telegram-mcp/gateway-loopback.js +10 -0
  30. package/dist/services/features/telegram-mcp/gateway-rmq.service.js +294 -0
  31. package/dist/services/features/telegram-mcp/gateway-socket.service.js +1463 -0
  32. package/dist/services/features/telegram-mcp/gateway.service.js +1141 -0
  33. package/dist/services/features/telegram-mcp/inbox.service.js +33 -0
  34. package/dist/services/features/telegram-mcp/mcp-http.service.js +76 -0
  35. package/dist/services/features/telegram-mcp/mcp-server.service.js +127 -0
  36. package/dist/services/features/telegram-mcp/notify.service.js +33 -0
  37. package/dist/services/features/telegram-mcp/pair.service.js +33 -0
  38. package/dist/services/features/telegram-mcp/runtime.service.js +36 -0
  39. package/dist/services/features/telegram-mcp/session-context.service.js +33 -0
  40. package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +103 -0
  41. package/dist/services/features/telegram-mcp/src/app/config/env.js +317 -0
  42. package/dist/services/features/telegram-mcp/src/app/http.js +774 -0
  43. package/dist/services/features/telegram-mcp/src/app/index.js +2 -0
  44. package/dist/services/features/telegram-mcp/src/app/providers/mcp/server.js +13 -0
  45. package/dist/services/features/telegram-mcp/src/app/providers/redis/client.js +18 -0
  46. package/dist/services/features/telegram-mcp/src/app/webapp/assets.js +740 -0
  47. package/dist/services/features/telegram-mcp/src/app/webapp/auth.js +267 -0
  48. package/dist/services/features/telegram-mcp/src/app/webapp/relay.js +69 -0
  49. package/dist/services/features/telegram-mcp/src/app/webapp/tmux.js +9 -0
  50. package/dist/services/features/telegram-mcp/src/entities/auth/model/types.js +2 -0
  51. package/dist/services/features/telegram-mcp/src/entities/browser/model/types.js +2 -0
  52. package/dist/services/features/telegram-mcp/src/entities/collaboration/model/types.js +2 -0
  53. package/dist/services/features/telegram-mcp/src/entities/inbox/model/types.js +2 -0
  54. package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +545 -0
  55. package/dist/services/features/telegram-mcp/src/entities/request/model/types.js +2 -0
  56. package/dist/services/features/telegram-mcp/src/entities/session/model/types.js +2 -0
  57. package/dist/services/features/telegram-mcp/src/features/ask-user/model/askUserTelegram.js +33 -0
  58. package/dist/services/features/telegram-mcp/src/features/browser/model/browserClearLogsTool.js +28 -0
  59. package/dist/services/features/telegram-mcp/src/features/browser/model/browserClickTool.js +28 -0
  60. package/dist/services/features/telegram-mcp/src/features/browser/model/browserCloseTool.js +28 -0
  61. package/dist/services/features/telegram-mcp/src/features/browser/model/browserComputedStyleTool.js +28 -0
  62. package/dist/services/features/telegram-mcp/src/features/browser/model/browserConsoleTool.js +28 -0
  63. package/dist/services/features/telegram-mcp/src/features/browser/model/browserDomTool.js +28 -0
  64. package/dist/services/features/telegram-mcp/src/features/browser/model/browserErrorsTool.js +28 -0
  65. package/dist/services/features/telegram-mcp/src/features/browser/model/browserFillTool.js +28 -0
  66. package/dist/services/features/telegram-mcp/src/features/browser/model/browserNetworkFailuresTool.js +28 -0
  67. package/dist/services/features/telegram-mcp/src/features/browser/model/browserOpenTool.js +33 -0
  68. package/dist/services/features/telegram-mcp/src/features/browser/model/browserPressTool.js +28 -0
  69. package/dist/services/features/telegram-mcp/src/features/browser/model/browserReloadTool.js +28 -0
  70. package/dist/services/features/telegram-mcp/src/features/browser/model/browserScreenshotTool.js +28 -0
  71. package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +689 -0
  72. package/dist/services/features/telegram-mcp/src/features/browser/model/browserWaitForTool.js +28 -0
  73. package/dist/services/features/telegram-mcp/src/features/browser/model/browserWaitForUrlTool.js +28 -0
  74. package/dist/services/features/telegram-mcp/src/features/collaboration/model/backend.js +2 -0
  75. package/dist/services/features/telegram-mcp/src/features/collaboration/model/collaborationService.js +26 -0
  76. package/dist/services/features/telegram-mcp/src/features/collaboration/model/localCollaborationBackend.js +390 -0
  77. package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerFileService.js +102 -0
  78. package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerFileTool.js +33 -0
  79. package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerNoteTool.js +33 -0
  80. package/dist/services/features/telegram-mcp/src/features/distributed-client/model/gatewayCollaborationBackend.js +69 -0
  81. package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +657 -0
  82. package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayReplyResolution.js +17 -0
  83. package/dist/services/features/telegram-mcp/src/features/inbox/model/deleteTelegramInboxMessageTool.js +33 -0
  84. package/dist/services/features/telegram-mcp/src/features/inbox/model/getTelegramInboxCountTool.js +33 -0
  85. package/dist/services/features/telegram-mcp/src/features/inbox/model/getTelegramInboxTool.js +33 -0
  86. package/dist/services/features/telegram-mcp/src/features/inbox/model/inboxService.js +77 -0
  87. package/dist/services/features/telegram-mcp/src/features/notify/model/notifyService.js +93 -0
  88. package/dist/services/features/telegram-mcp/src/features/notify/model/notifyTelegramTool.js +33 -0
  89. package/dist/services/features/telegram-mcp/src/features/pair-session/model/clearSessionPairingTool.js +33 -0
  90. package/dist/services/features/telegram-mcp/src/features/pair-session/model/createSessionPairCodeTool.js +33 -0
  91. package/dist/services/features/telegram-mcp/src/features/pair-session/model/generatePairCode.js +202 -0
  92. package/dist/services/features/telegram-mcp/src/features/session-context/model/clearSessionContextTool.js +33 -0
  93. package/dist/services/features/telegram-mcp/src/features/session-context/model/getSessionContextTool.js +33 -0
  94. package/dist/services/features/telegram-mcp/src/features/session-context/model/getTmuxTargetTool.js +33 -0
  95. package/dist/services/features/telegram-mcp/src/features/session-context/model/renameSessionTool.js +33 -0
  96. package/dist/services/features/telegram-mcp/src/features/session-context/model/sessionContextService.js +409 -0
  97. package/dist/services/features/telegram-mcp/src/features/session-context/model/setSessionContextTool.js +33 -0
  98. package/dist/services/features/telegram-mcp/src/features/session-context/model/setTmuxTargetTool.js +33 -0
  99. package/dist/services/features/telegram-mcp/src/features/tools-sync/model/refreshToolsMarkdownService.js +123 -0
  100. package/dist/services/features/telegram-mcp/src/features/tools-sync/model/refreshToolsMarkdownTool.js +33 -0
  101. package/dist/services/features/telegram-mcp/src/processes/human-approval/model/orchestrator.js +243 -0
  102. package/dist/services/features/telegram-mcp/src/shared/api/storage/contract.js +2 -0
  103. package/dist/services/features/telegram-mcp/src/shared/api/tool-registry/registry.js +8 -0
  104. package/dist/services/features/telegram-mcp/src/shared/api/tool-registry/types.js +2 -0
  105. package/dist/services/features/telegram-mcp/src/shared/api/transport/contract.js +2 -0
  106. package/dist/services/features/telegram-mcp/src/shared/integrations/object-storage/minioExchangeStore.js +86 -0
  107. package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +436 -0
  108. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/collabSemantics.js +21 -0
  109. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/collabUi.js +87 -0
  110. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/messageFormat.js +60 -0
  111. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/proxyFetch.js +46 -0
  112. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +6534 -0
  113. package/dist/services/features/telegram-mcp/src/shared/integrations/tmux/client.js +280 -0
  114. package/dist/services/features/telegram-mcp/src/shared/lib/ids/ids.js +34 -0
  115. package/dist/services/features/telegram-mcp/src/shared/lib/logger/logger.js +68 -0
  116. package/dist/services/features/telegram-mcp/src/shared/lib/project-identity/projectIdentity.js +223 -0
  117. package/dist/services/features/telegram-mcp/src/shared/lib/redact-secrets/redactSecrets.js +22 -0
  118. package/dist/services/features/telegram-mcp/src/shared/lib/truncate/truncate.js +12 -0
  119. package/dist/services/features/telegram-mcp/src/shared/lib/version/versionHandshake.js +124 -0
  120. package/dist/services/features/telegram-mcp/src/shared/types/common.js +2 -0
  121. package/dist/services/features/telegram-mcp/standalone-http.service.js +113 -0
  122. package/dist/services/features/telegram-mcp/tools-sync.service.js +33 -0
  123. package/package.json +110 -0
  124. package/scripts/postinstall.js +60 -0
@@ -0,0 +1,689 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BrowserService = void 0;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ function pushBounded(list, entry, max) {
9
+ list.push(entry);
10
+ if (list.length > max) {
11
+ list.splice(0, list.length - max);
12
+ }
13
+ }
14
+ function trimList(list, limit) {
15
+ if (!limit || limit <= 0 || limit >= list.length) {
16
+ return [...list];
17
+ }
18
+ return list.slice(-limit);
19
+ }
20
+ function sanitizeScreenshotName(fileName) {
21
+ const trimmed = fileName?.trim();
22
+ if (!trimmed) {
23
+ const timestamp = new Date().toISOString().replace(/[:.]/gu, "-");
24
+ return `browser-screenshot-${timestamp}.png`;
25
+ }
26
+ const parsed = node_path_1.default.parse(trimmed);
27
+ const base = parsed.name.trim() || "browser-screenshot";
28
+ const extension = parsed.ext.toLowerCase() === ".png" ? ".png" : ".png";
29
+ return `${base}${extension}`;
30
+ }
31
+ function buildDatedRelativePath(fileName, date = new Date()) {
32
+ const dateSegment = date.toISOString().slice(0, 10);
33
+ const timeSegment = date.toTimeString().slice(0, 8).replace(/:/gu, "-");
34
+ return `${dateSegment}/${timeSegment}/${fileName}`;
35
+ }
36
+ function isAbsoluteBrowserUrl(value) {
37
+ return /^https?:\/\//iu.test(value) || value.startsWith("data:");
38
+ }
39
+ function formatConsoleLocation(message) {
40
+ const location = message.location();
41
+ if (!location.url && !location.lineNumber && !location.columnNumber) {
42
+ return undefined;
43
+ }
44
+ return `${location.url || "unknown"}:${location.lineNumber ?? 0}:${location.columnNumber ?? 0}`;
45
+ }
46
+ function escapeCssAttributeValue(value) {
47
+ return value.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"');
48
+ }
49
+ class BrowserService {
50
+ config;
51
+ sessionStore;
52
+ bindingStore;
53
+ xchangeFileMetaStore;
54
+ objectStore;
55
+ telegramTransport;
56
+ logger;
57
+ projectIdentityResolver;
58
+ playwrightModulePromise;
59
+ browserPromise;
60
+ sessionStates = new Map();
61
+ constructor(config, sessionStore, bindingStore, xchangeFileMetaStore, objectStore, telegramTransport, logger, projectIdentityResolver) {
62
+ this.config = config;
63
+ this.sessionStore = sessionStore;
64
+ this.bindingStore = bindingStore;
65
+ this.xchangeFileMetaStore = xchangeFileMetaStore;
66
+ this.objectStore = objectStore;
67
+ this.telegramTransport = telegramTransport;
68
+ this.logger = logger;
69
+ this.projectIdentityResolver = projectIdentityResolver;
70
+ }
71
+ async open(input) {
72
+ this.ensureEnabled();
73
+ const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
74
+ const existingState = this.sessionStates.get(resolved.sessionId);
75
+ const shouldReset = input.reset_context === true;
76
+ const targetUrl = this.resolveBrowserUrl(input.url);
77
+ if (shouldReset && existingState) {
78
+ await this.closeState(resolved.sessionId, existingState);
79
+ }
80
+ const { state, createdContext } = await this.ensureSessionState(resolved.sessionId, shouldReset);
81
+ const waitUntil = (input.wait_until ??
82
+ this.config.browser.waitUntil);
83
+ await state.page.goto(targetUrl, {
84
+ waitUntil,
85
+ timeout: this.config.browser.timeoutMs,
86
+ });
87
+ state.currentUrl = state.page.url();
88
+ state.title = await state.page.title();
89
+ state.lastUsedAt = new Date().toISOString();
90
+ this.logger.info("Browser page opened", {
91
+ sessionId: resolved.sessionId,
92
+ url: state.currentUrl,
93
+ title: state.title,
94
+ createdContext,
95
+ waitUntil,
96
+ headless: this.config.browser.headless,
97
+ });
98
+ return {
99
+ session_id: resolved.sessionId,
100
+ opened: true,
101
+ created_context: createdContext,
102
+ url: state.currentUrl,
103
+ ...(state.title ? { title: state.title } : {}),
104
+ };
105
+ }
106
+ async getConsole(input) {
107
+ this.ensureEnabled();
108
+ const { sessionId, state } = await this.requireSessionState(input);
109
+ state.lastUsedAt = new Date().toISOString();
110
+ return {
111
+ session_id: sessionId,
112
+ total: state.consoleMessages.length,
113
+ messages: trimList(state.consoleMessages, input.limit).map((message) => ({
114
+ type: message.type,
115
+ text: message.text,
116
+ ...(message.location ? { location: message.location } : {}),
117
+ timestamp: message.timestamp,
118
+ })),
119
+ };
120
+ }
121
+ async click(input) {
122
+ this.ensureEnabled();
123
+ const { sessionId, state } = await this.requireSessionState(input);
124
+ const locator = this.resolveLocator(state.page, input);
125
+ await locator.click({
126
+ timeout: this.resolveTimeoutMs(input.timeout_ms),
127
+ });
128
+ state.currentUrl = state.page.url();
129
+ state.title = await state.page.title().catch(() => state.title);
130
+ state.lastUsedAt = new Date().toISOString();
131
+ return {
132
+ session_id: sessionId,
133
+ clicked: true,
134
+ ...(input.ai_tag ? { ai_tag: input.ai_tag } : {}),
135
+ ...(input.selector ? { selector: input.selector } : {}),
136
+ ...(input.text ? { text: input.text } : {}),
137
+ url: state.currentUrl,
138
+ ...(state.title ? { title: state.title } : {}),
139
+ };
140
+ }
141
+ async fill(input) {
142
+ this.ensureEnabled();
143
+ const { sessionId, state } = await this.requireSessionState(input);
144
+ const locator = this.resolveLocator(state.page, input);
145
+ await locator.fill(input.value, {
146
+ timeout: this.resolveTimeoutMs(input.timeout_ms),
147
+ });
148
+ state.currentUrl = state.page.url();
149
+ state.title = await state.page.title().catch(() => state.title);
150
+ state.lastUsedAt = new Date().toISOString();
151
+ return {
152
+ session_id: sessionId,
153
+ filled: true,
154
+ ...(input.ai_tag ? { ai_tag: input.ai_tag } : {}),
155
+ ...(input.selector ? { selector: input.selector } : {}),
156
+ ...(input.text ? { text: input.text } : {}),
157
+ value_length: input.value.length,
158
+ url: state.currentUrl,
159
+ ...(state.title ? { title: state.title } : {}),
160
+ };
161
+ }
162
+ async press(input) {
163
+ this.ensureEnabled();
164
+ const { sessionId, state } = await this.requireSessionState(input);
165
+ if (input.selector || input.text) {
166
+ const locator = this.resolveLocator(state.page, input);
167
+ await locator.press(input.key, {
168
+ timeout: this.resolveTimeoutMs(input.timeout_ms),
169
+ });
170
+ }
171
+ else {
172
+ await state.page.keyboard.press(input.key);
173
+ }
174
+ state.currentUrl = state.page.url();
175
+ state.title = await state.page.title().catch(() => state.title);
176
+ state.lastUsedAt = new Date().toISOString();
177
+ return {
178
+ session_id: sessionId,
179
+ pressed: true,
180
+ key: input.key,
181
+ ...(input.ai_tag ? { ai_tag: input.ai_tag } : {}),
182
+ ...(input.selector ? { selector: input.selector } : {}),
183
+ ...(input.text ? { text: input.text } : {}),
184
+ url: state.currentUrl,
185
+ ...(state.title ? { title: state.title } : {}),
186
+ };
187
+ }
188
+ async reload(input) {
189
+ this.ensureEnabled();
190
+ const { sessionId, state } = await this.requireSessionState(input);
191
+ const waitUntil = (input.wait_until ??
192
+ this.config.browser.waitUntil);
193
+ await state.page.reload({
194
+ waitUntil,
195
+ timeout: this.config.browser.timeoutMs,
196
+ });
197
+ state.currentUrl = state.page.url();
198
+ state.title = await state.page.title().catch(() => state.title);
199
+ state.lastUsedAt = new Date().toISOString();
200
+ this.logger.info("Browser page reloaded", {
201
+ sessionId,
202
+ url: state.currentUrl,
203
+ title: state.title,
204
+ waitUntil,
205
+ });
206
+ return {
207
+ session_id: sessionId,
208
+ reloaded: true,
209
+ url: state.currentUrl,
210
+ ...(state.title ? { title: state.title } : {}),
211
+ };
212
+ }
213
+ async waitFor(input) {
214
+ this.ensureEnabled();
215
+ const { sessionId, state } = await this.requireSessionState(input);
216
+ const locator = this.resolveLocator(state.page, input);
217
+ const waitState = input.state ?? "visible";
218
+ await locator.waitFor({
219
+ state: waitState,
220
+ timeout: this.resolveTimeoutMs(input.timeout_ms),
221
+ });
222
+ state.currentUrl = state.page.url();
223
+ state.title = await state.page.title().catch(() => state.title);
224
+ state.lastUsedAt = new Date().toISOString();
225
+ return {
226
+ session_id: sessionId,
227
+ waited: true,
228
+ state: waitState,
229
+ ...(input.ai_tag ? { ai_tag: input.ai_tag } : {}),
230
+ ...(input.selector ? { selector: input.selector } : {}),
231
+ ...(input.text ? { text: input.text } : {}),
232
+ url: state.currentUrl,
233
+ ...(state.title ? { title: state.title } : {}),
234
+ };
235
+ }
236
+ async waitForUrl(input) {
237
+ this.ensureEnabled();
238
+ const { sessionId, state } = await this.requireSessionState(input);
239
+ const timeout = this.resolveTimeoutMs(input.timeout_ms);
240
+ if (input.url?.trim()) {
241
+ await state.page.waitForURL(input.url.trim(), {
242
+ timeout,
243
+ });
244
+ }
245
+ else if (input.url_contains?.trim()) {
246
+ const expected = input.url_contains.trim();
247
+ await state.page.waitForURL((value) => value.toString().includes(expected), {
248
+ timeout,
249
+ });
250
+ }
251
+ else {
252
+ throw new Error("Browser URL target is missing. Provide url or url_contains.");
253
+ }
254
+ state.currentUrl = state.page.url();
255
+ state.title = await state.page.title().catch(() => state.title);
256
+ state.lastUsedAt = new Date().toISOString();
257
+ return {
258
+ session_id: sessionId,
259
+ waited: true,
260
+ matched: input.url?.trim() ? "url" : "url_contains",
261
+ ...(input.url?.trim() ? { url: input.url.trim() } : {}),
262
+ ...(input.url_contains?.trim()
263
+ ? { url_contains: input.url_contains.trim() }
264
+ : {}),
265
+ current_url: state.currentUrl,
266
+ ...(state.title ? { title: state.title } : {}),
267
+ };
268
+ }
269
+ async getErrors(input) {
270
+ this.ensureEnabled();
271
+ const { sessionId, state } = await this.requireSessionState(input);
272
+ state.lastUsedAt = new Date().toISOString();
273
+ return {
274
+ session_id: sessionId,
275
+ total: state.pageErrors.length,
276
+ errors: trimList(state.pageErrors, input.limit).map((error) => ({
277
+ message: error.message,
278
+ ...(error.stack ? { stack: error.stack } : {}),
279
+ timestamp: error.timestamp,
280
+ })),
281
+ };
282
+ }
283
+ async getNetworkFailures(input) {
284
+ this.ensureEnabled();
285
+ const { sessionId, state } = await this.requireSessionState(input);
286
+ state.lastUsedAt = new Date().toISOString();
287
+ return {
288
+ session_id: sessionId,
289
+ total: state.networkFailures.length,
290
+ failures: trimList(state.networkFailures, input.limit).map((failure) => ({
291
+ url: failure.url,
292
+ method: failure.method,
293
+ ...(typeof failure.status === "number" ? { status: failure.status } : {}),
294
+ ...(failure.errorText ? { error_text: failure.errorText } : {}),
295
+ ...(failure.resourceType
296
+ ? { resource_type: failure.resourceType }
297
+ : {}),
298
+ timestamp: failure.timestamp,
299
+ })),
300
+ };
301
+ }
302
+ async clearLogs(input) {
303
+ this.ensureEnabled();
304
+ const { sessionId, state } = await this.requireSessionState(input);
305
+ const consoleMessagesCleared = state.consoleMessages.length;
306
+ const pageErrorsCleared = state.pageErrors.length;
307
+ const networkFailuresCleared = state.networkFailures.length;
308
+ state.consoleMessages = [];
309
+ state.pageErrors = [];
310
+ state.networkFailures = [];
311
+ state.lastUsedAt = new Date().toISOString();
312
+ return {
313
+ session_id: sessionId,
314
+ cleared: true,
315
+ console_messages_cleared: consoleMessagesCleared,
316
+ page_errors_cleared: pageErrorsCleared,
317
+ network_failures_cleared: networkFailuresCleared,
318
+ };
319
+ }
320
+ async getDom(input) {
321
+ this.ensureEnabled();
322
+ const { sessionId, state } = await this.requireSessionState(input);
323
+ const selector = input.selector?.trim() || "body";
324
+ const snapshot = await state.page
325
+ .locator(selector)
326
+ .first()
327
+ .evaluate((element, payload) => {
328
+ const htmlRequested = payload.includeHtml;
329
+ const textRequested = payload.includeText;
330
+ const computed = globalThis.getComputedStyle(element);
331
+ const attributes = Object.fromEntries(Array.from(element.attributes).map((attribute) => [
332
+ attribute.name,
333
+ attribute.value,
334
+ ]));
335
+ return {
336
+ found: true,
337
+ ...(htmlRequested
338
+ ? { outerHtml: element.outerHTML }
339
+ : {}),
340
+ ...(textRequested
341
+ ? {
342
+ textContent: element.textContent?.trim() ??
343
+ "",
344
+ }
345
+ : {}),
346
+ visible: computed.display !== "none" &&
347
+ computed.visibility !== "hidden" &&
348
+ computed.opacity !== "0",
349
+ attributes,
350
+ };
351
+ }, {
352
+ includeHtml: input.include_html !== false,
353
+ includeText: input.include_text !== false,
354
+ })
355
+ .catch(() => ({ found: false }));
356
+ state.lastUsedAt = new Date().toISOString();
357
+ state.currentUrl = state.page.url();
358
+ state.title = await state.page.title().catch(() => state.title);
359
+ return {
360
+ session_id: sessionId,
361
+ selector,
362
+ found: snapshot.found,
363
+ ...(state.currentUrl ? { url: state.currentUrl } : {}),
364
+ ...(state.title ? { title: state.title } : {}),
365
+ ...(snapshot.outerHtml ? { outer_html: snapshot.outerHtml } : {}),
366
+ ...(typeof snapshot.textContent === "string"
367
+ ? { text_content: snapshot.textContent }
368
+ : {}),
369
+ ...(typeof snapshot.visible === "boolean"
370
+ ? { visible: snapshot.visible }
371
+ : {}),
372
+ ...(snapshot.attributes ? { attributes: snapshot.attributes } : {}),
373
+ };
374
+ }
375
+ async getComputedStyle(input) {
376
+ this.ensureEnabled();
377
+ const { sessionId, state } = await this.requireSessionState(input);
378
+ const properties = input.properties?.length
379
+ ? input.properties
380
+ : [
381
+ "display",
382
+ "position",
383
+ "visibility",
384
+ "opacity",
385
+ "color",
386
+ "background-color",
387
+ "font-size",
388
+ "z-index",
389
+ "overflow",
390
+ ];
391
+ const snapshot = await state.page
392
+ .locator(input.selector)
393
+ .first()
394
+ .evaluate((element, requestedProperties) => {
395
+ const computed = globalThis.getComputedStyle(element);
396
+ const rect = element.getBoundingClientRect();
397
+ const styles = Object.fromEntries(requestedProperties.map((property) => [
398
+ property,
399
+ computed.getPropertyValue(property),
400
+ ]));
401
+ return {
402
+ found: true,
403
+ visible: computed.display !== "none" &&
404
+ computed.visibility !== "hidden" &&
405
+ computed.opacity !== "0",
406
+ styles,
407
+ box: {
408
+ x: rect.x,
409
+ y: rect.y,
410
+ width: rect.width,
411
+ height: rect.height,
412
+ },
413
+ };
414
+ }, properties)
415
+ .catch(() => ({ found: false }));
416
+ state.lastUsedAt = new Date().toISOString();
417
+ state.currentUrl = state.page.url();
418
+ state.title = await state.page.title().catch(() => state.title);
419
+ return {
420
+ session_id: sessionId,
421
+ selector: input.selector,
422
+ found: snapshot.found,
423
+ ...(state.currentUrl ? { url: state.currentUrl } : {}),
424
+ ...(state.title ? { title: state.title } : {}),
425
+ ...(typeof snapshot.visible === "boolean"
426
+ ? { visible: snapshot.visible }
427
+ : {}),
428
+ ...(snapshot.styles ? { styles: snapshot.styles } : {}),
429
+ ...(snapshot.box ? { box: snapshot.box } : {}),
430
+ };
431
+ }
432
+ async screenshot(input) {
433
+ this.ensureEnabled();
434
+ const { sessionId, state, session } = await this.requireSessionState(input);
435
+ const fileName = sanitizeScreenshotName(input.file_name);
436
+ const pngBuffer = input.selector?.trim()
437
+ ? await state.page
438
+ .locator(input.selector)
439
+ .first()
440
+ .screenshot({
441
+ type: "png",
442
+ timeout: this.config.browser.timeoutMs,
443
+ })
444
+ : await state.page.screenshot({
445
+ type: "png",
446
+ fullPage: input.full_page === true,
447
+ timeout: this.config.browser.timeoutMs,
448
+ });
449
+ const workspaceDir = this.objectStore.resolveWorkspaceDir(session);
450
+ const exchangeDir = node_path_1.default.resolve(workspaceDir, this.config.exchange.dir);
451
+ const storedFile = await this.objectStore.storeFile({
452
+ session,
453
+ sessionId,
454
+ source: "browser-screenshot",
455
+ relativePath: buildDatedRelativePath(fileName),
456
+ content: pngBuffer,
457
+ mimeType: "image/png",
458
+ });
459
+ const filePath = storedFile.filePath;
460
+ state.lastUsedAt = new Date().toISOString();
461
+ state.currentUrl = state.page.url();
462
+ state.title = await state.page.title().catch(() => state.title);
463
+ this.logger.info("Browser screenshot captured", {
464
+ sessionId,
465
+ filePath,
466
+ selector: input.selector,
467
+ fullPage: input.full_page === true,
468
+ });
469
+ await this.xchangeFileMetaStore.setXchangeFileMeta({
470
+ sessionId,
471
+ filePath,
472
+ relativePath: storedFile.relativePath,
473
+ source: "browser-screenshot",
474
+ uploadedAt: new Date().toISOString(),
475
+ mimeType: "image/png",
476
+ sizeBytes: storedFile.sizeBytes,
477
+ ...(input.caption ? { caption: input.caption } : {}),
478
+ });
479
+ let telegramMessageId;
480
+ if (input.send_to_telegram === true) {
481
+ const binding = await this.bindingStore.getBinding(sessionId);
482
+ if (!binding) {
483
+ throw new Error("Session is not linked to Telegram, so screenshot cannot be sent there.");
484
+ }
485
+ const sent = await this.telegramTransport.sendDocumentToChat(binding.telegramChatId, filePath, input.caption);
486
+ telegramMessageId = sent.messageId;
487
+ }
488
+ return {
489
+ session_id: sessionId,
490
+ file_path: filePath,
491
+ workspace_dir: workspaceDir,
492
+ exchange_dir: exchangeDir,
493
+ ...(typeof telegramMessageId === "number"
494
+ ? { telegram_message_id: telegramMessageId }
495
+ : {}),
496
+ ...(state.currentUrl ? { url: state.currentUrl } : {}),
497
+ ...(state.title ? { title: state.title } : {}),
498
+ };
499
+ }
500
+ async close(input) {
501
+ this.ensureEnabled();
502
+ const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
503
+ const state = this.sessionStates.get(resolved.sessionId);
504
+ if (state) {
505
+ await this.closeState(resolved.sessionId, state);
506
+ }
507
+ return {
508
+ session_id: resolved.sessionId,
509
+ closed: Boolean(state),
510
+ };
511
+ }
512
+ async shutdown() {
513
+ for (const [sessionId, state] of this.sessionStates.entries()) {
514
+ await this.closeState(sessionId, state);
515
+ }
516
+ if (this.browserPromise) {
517
+ try {
518
+ const browser = await this.browserPromise;
519
+ await browser.close();
520
+ }
521
+ catch (error) {
522
+ this.logger.warn("Browser shutdown failed", {
523
+ error: error instanceof Error ? error.message : String(error),
524
+ });
525
+ }
526
+ finally {
527
+ this.browserPromise = undefined;
528
+ }
529
+ }
530
+ }
531
+ async ensurePlaywright() {
532
+ this.playwrightModulePromise ??= import("playwright");
533
+ return this.playwrightModulePromise;
534
+ }
535
+ async ensureBrowser() {
536
+ this.browserPromise ??= (async () => {
537
+ const playwright = await this.ensurePlaywright();
538
+ const launchArgs = this.config.browser.headless === false &&
539
+ this.config.browser.devtools === true
540
+ ? ["--auto-open-devtools-for-tabs"]
541
+ : [];
542
+ const browser = await playwright.chromium.launch({
543
+ headless: this.config.browser.headless,
544
+ slowMo: this.config.browser.slowMoMs,
545
+ ...(launchArgs.length ? { args: launchArgs } : {}),
546
+ ...(this.config.browser.executablePath
547
+ ? { executablePath: this.config.browser.executablePath }
548
+ : {}),
549
+ ...(this.config.browser.channel
550
+ ? { channel: this.config.browser.channel }
551
+ : {}),
552
+ });
553
+ this.logger.info("Browser runtime launched", {
554
+ headless: this.config.browser.headless,
555
+ devtools: this.config.browser.headless === false &&
556
+ this.config.browser.devtools === true,
557
+ slowMoMs: this.config.browser.slowMoMs,
558
+ channel: this.config.browser.channel,
559
+ executablePath: this.config.browser.executablePath,
560
+ });
561
+ return browser;
562
+ })();
563
+ return this.browserPromise;
564
+ }
565
+ async ensureSessionState(sessionId, forceNewContext) {
566
+ const existing = this.sessionStates.get(sessionId);
567
+ if (existing && !forceNewContext) {
568
+ return { state: existing, createdContext: false };
569
+ }
570
+ const browser = await this.ensureBrowser();
571
+ const context = await browser.newContext();
572
+ const page = await context.newPage();
573
+ const createdAt = new Date().toISOString();
574
+ const state = {
575
+ context,
576
+ page,
577
+ createdAt,
578
+ lastUsedAt: createdAt,
579
+ consoleMessages: [],
580
+ pageErrors: [],
581
+ networkFailures: [],
582
+ };
583
+ page.on("console", (message) => {
584
+ pushBounded(state.consoleMessages, {
585
+ type: message.type(),
586
+ text: message.text(),
587
+ ...(formatConsoleLocation(message)
588
+ ? { location: formatConsoleLocation(message) }
589
+ : {}),
590
+ timestamp: new Date().toISOString(),
591
+ }, this.config.browser.maxEvents);
592
+ });
593
+ page.on("pageerror", (error) => {
594
+ pushBounded(state.pageErrors, {
595
+ message: error.message,
596
+ ...(error.stack ? { stack: error.stack } : {}),
597
+ timestamp: new Date().toISOString(),
598
+ }, this.config.browser.maxEvents);
599
+ });
600
+ page.on("requestfailed", (request) => {
601
+ this.recordNetworkFailure(state, request, undefined);
602
+ });
603
+ page.on("response", (response) => {
604
+ if (response.status() >= 400) {
605
+ this.recordNetworkFailure(state, response.request(), response);
606
+ }
607
+ });
608
+ this.sessionStates.set(sessionId, state);
609
+ return { state, createdContext: true };
610
+ }
611
+ recordNetworkFailure(state, request, response) {
612
+ const failure = request.failure();
613
+ pushBounded(state.networkFailures, {
614
+ url: request.url(),
615
+ method: request.method(),
616
+ ...(typeof response?.status() === "number"
617
+ ? { status: response.status() }
618
+ : {}),
619
+ ...(failure?.errorText ? { errorText: failure.errorText } : {}),
620
+ resourceType: request.resourceType(),
621
+ timestamp: new Date().toISOString(),
622
+ }, this.config.browser.maxEvents);
623
+ }
624
+ async requireSessionState(input) {
625
+ const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
626
+ const state = this.sessionStates.get(resolved.sessionId);
627
+ if (!state) {
628
+ throw new Error("Browser session is not open. Call browser_open first for this session.");
629
+ }
630
+ const session = await this.sessionStore.getSession(resolved.sessionId);
631
+ return {
632
+ sessionId: resolved.sessionId,
633
+ session,
634
+ state,
635
+ };
636
+ }
637
+ resolveWorkspaceDir(session) {
638
+ return session?.cwd?.trim() || process.cwd();
639
+ }
640
+ ensureEnabled() {
641
+ if (!this.config.browser.enabled) {
642
+ throw new Error("Browser tools are disabled. Enable them with BROWSER_ENABLED=true.");
643
+ }
644
+ }
645
+ resolveLocator(page, input) {
646
+ if (input.ai_tag?.trim()) {
647
+ const aiTag = escapeCssAttributeValue(input.ai_tag.trim());
648
+ return page
649
+ .locator(`[data-drive-tag="${aiTag}"], [ai-tag="${aiTag}"]`)
650
+ .first();
651
+ }
652
+ if (input.selector?.trim()) {
653
+ return page.locator(input.selector.trim()).first();
654
+ }
655
+ if (input.text?.trim()) {
656
+ return page.getByText(input.text.trim(), {
657
+ exact: input.exact === true,
658
+ }).first();
659
+ }
660
+ throw new Error("Browser target is missing. Provide ai_tag, selector, or text.");
661
+ }
662
+ resolveTimeoutMs(timeoutMs) {
663
+ return timeoutMs && timeoutMs > 0
664
+ ? timeoutMs
665
+ : this.config.browser.timeoutMs;
666
+ }
667
+ resolveBrowserUrl(inputUrl) {
668
+ const trimmed = inputUrl.trim();
669
+ if (isAbsoluteBrowserUrl(trimmed)) {
670
+ return trimmed;
671
+ }
672
+ if (!this.config.browser.address) {
673
+ throw new Error("BROWSER_ADDRESS is not configured, so browser_open requires an absolute URL.");
674
+ }
675
+ return new URL(trimmed, this.config.browser.address).toString();
676
+ }
677
+ async closeState(sessionId, state) {
678
+ this.sessionStates.delete(sessionId);
679
+ await state.context.close();
680
+ this.logger.info("Browser session context closed", {
681
+ sessionId,
682
+ currentUrl: state.currentUrl,
683
+ title: state.title,
684
+ createdAt: state.createdAt,
685
+ lastUsedAt: state.lastUsedAt,
686
+ });
687
+ }
688
+ }
689
+ exports.BrowserService = BrowserService;