@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
@@ -0,0 +1,536 @@
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.BrowserRecordingBundleWriter = void 0;
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const client_1 = require("../../../shared/integrations/terminal/client");
10
+ function sanitizeSegment(value) {
11
+ const normalized = value
12
+ .normalize("NFKD")
13
+ .replace(/[\u0300-\u036f]/gu, "")
14
+ .toLowerCase()
15
+ .replace(/[^a-z0-9]+/gu, "-")
16
+ .replace(/^-+/u, "")
17
+ .replace(/-+$/u, "");
18
+ return normalized || "recording";
19
+ }
20
+ function padNumber(value, length = 2) {
21
+ return String(value).padStart(length, "0");
22
+ }
23
+ function formatLocalTimestamp(date) {
24
+ return `${date.getFullYear()}-${padNumber(date.getMonth() + 1)}-${padNumber(date.getDate())}T${padNumber(date.getHours())}:${padNumber(date.getMinutes())}:${padNumber(date.getSeconds())}.${padNumber(date.getMilliseconds(), 3)}`;
25
+ }
26
+ function formatTimestampForDir(date) {
27
+ return formatLocalTimestamp(date).replace(/[:.]/gu, "-");
28
+ }
29
+ function normalizeRecordedTimestamp(rawValue) {
30
+ const trimmed = rawValue?.trim();
31
+ if (!trimmed) {
32
+ return formatLocalTimestamp(new Date());
33
+ }
34
+ const parsed = new Date(trimmed);
35
+ if (Number.isNaN(parsed.getTime())) {
36
+ return formatLocalTimestamp(new Date());
37
+ }
38
+ return formatLocalTimestamp(parsed);
39
+ }
40
+ function formatJsonLine(value) {
41
+ return Buffer.from(`${JSON.stringify(value)}\n`, "utf8");
42
+ }
43
+ function sanitizeRequestId(value) {
44
+ return value
45
+ .trim()
46
+ .replace(/[^a-zA-Z0-9._-]+/gu, "-")
47
+ .replace(/^-+/u, "")
48
+ .replace(/-+$/u, "") || "request";
49
+ }
50
+ function detectMimeTypeFromHeaders(headers) {
51
+ const match = headers?.find((header) => header.name.trim().toLowerCase() === "content-type");
52
+ const value = match?.value?.trim();
53
+ return value || undefined;
54
+ }
55
+ async function allocateBundleDirName(config, session, tabTitle) {
56
+ const exchangeRoot = await (0, client_1.ensureXchangeDir)(config.terminal, session.cwd || "", config.exchange.dir);
57
+ const webRoot = node_path_1.default.resolve(exchangeRoot, "web");
58
+ await (0, promises_1.mkdir)(webRoot, { recursive: true });
59
+ const baseName = `${sanitizeSegment(tabTitle)}-${formatTimestampForDir(new Date())}`;
60
+ let attempt = 1;
61
+ while (attempt < 1000) {
62
+ const candidate = attempt === 1 ? baseName : `${baseName}-${attempt}`;
63
+ const candidatePath = node_path_1.default.resolve(webRoot, candidate);
64
+ try {
65
+ await (0, promises_1.mkdir)(candidatePath);
66
+ return candidate;
67
+ }
68
+ catch (error) {
69
+ if (error &&
70
+ typeof error === "object" &&
71
+ "code" in error &&
72
+ error.code === "EEXIST") {
73
+ attempt += 1;
74
+ continue;
75
+ }
76
+ throw error;
77
+ }
78
+ }
79
+ throw new Error("Could not allocate a unique browser recording bundle directory.");
80
+ }
81
+ class BrowserRecordingBundleWriter {
82
+ config;
83
+ logger;
84
+ constructor(config, logger) {
85
+ this.config = config;
86
+ this.logger = logger;
87
+ }
88
+ async create(input) {
89
+ if (!input.session.cwd?.trim()) {
90
+ throw new Error("Workspace cwd is not registered for this console.");
91
+ }
92
+ const bundleDirName = await allocateBundleDirName(this.config, input.session, input.tabTitle);
93
+ const bundleRelativePath = `web/${bundleDirName}`;
94
+ const exchangeRoot = await (0, client_1.ensureXchangeDir)(this.config.terminal, input.session.cwd, this.config.exchange.dir);
95
+ const bundlePath = node_path_1.default.resolve(exchangeRoot, bundleRelativePath);
96
+ for (const segment of [
97
+ bundleRelativePath,
98
+ `${bundleRelativePath}/pages`,
99
+ `${bundleRelativePath}/network`,
100
+ `${bundleRelativePath}/network/requests`,
101
+ `${bundleRelativePath}/console`,
102
+ ]) {
103
+ await (0, client_1.writeXchangeRelativeFile)(this.config.terminal, input.session.cwd, this.config.exchange.dir, `${segment}/.keep`, Buffer.from("", "utf8"));
104
+ }
105
+ const startedAt = formatLocalTimestamp(new Date());
106
+ const record = {
107
+ sessionId: input.sessionId,
108
+ backend: "firefox-attached",
109
+ recordingId: input.recordingId,
110
+ instanceId: input.instanceId,
111
+ tabId: input.tabId,
112
+ ...(input.tabTitle ? { tabTitle: input.tabTitle } : {}),
113
+ ...(input.tabUrl ? { tabUrl: input.tabUrl } : {}),
114
+ bundleDirName,
115
+ bundleRelativePath,
116
+ bundlePath,
117
+ startedAt,
118
+ status: "recording",
119
+ eventCount: 0,
120
+ };
121
+ const sessionJson = {
122
+ recording_id: record.recordingId,
123
+ backend: record.backend,
124
+ session_id: record.sessionId,
125
+ session_label: input.session.label || input.session.sessionId,
126
+ instance_id: record.instanceId,
127
+ tab_id: record.tabId,
128
+ ...(record.tabTitle ? { tab_title: record.tabTitle } : {}),
129
+ ...(record.tabUrl ? { tab_url: record.tabUrl } : {}),
130
+ started_at: record.startedAt,
131
+ status: record.status,
132
+ bundle_dir_name: record.bundleDirName,
133
+ bundle_relative_path: record.bundleRelativePath,
134
+ bundle_path: record.bundlePath,
135
+ workspace_dir: input.session.cwd,
136
+ exchange_dir: node_path_1.default.resolve(input.session.cwd, this.config.exchange.dir),
137
+ files: {
138
+ timeline: "timeline.ndjson",
139
+ session: "session.json",
140
+ pages: "pages",
141
+ network: "network",
142
+ network_index: "network/index.ndjson",
143
+ network_requests: "network/requests",
144
+ console: "console",
145
+ },
146
+ };
147
+ await (0, client_1.writeXchangeRelativeFile)(this.config.terminal, input.session.cwd, this.config.exchange.dir, `${bundleRelativePath}/session.json`, Buffer.from(`${JSON.stringify(sessionJson, null, 2)}\n`, "utf8"));
148
+ const state = {
149
+ record,
150
+ session: input.session,
151
+ pageCounter: 0,
152
+ snapshotCounter: 0,
153
+ requestCounter: 0,
154
+ requestArtifacts: new Map(),
155
+ };
156
+ await this.appendEvent(state, {
157
+ kind: "session_started",
158
+ status: "recording",
159
+ });
160
+ this.logger.info("Browser recording bundle created", {
161
+ sessionId: input.sessionId,
162
+ recordingId: input.recordingId,
163
+ bundlePath,
164
+ tabId: input.tabId,
165
+ tabTitle: input.tabTitle,
166
+ });
167
+ return state;
168
+ }
169
+ async appendEvent(state, event) {
170
+ const at = normalizeRecordedTimestamp(event.at);
171
+ state.record.eventCount += 1;
172
+ state.record.lastEventAt = at;
173
+ let relPath;
174
+ let pageId = state.currentPageId;
175
+ if (event.kind === "page_snapshot") {
176
+ const targetUrl = event.url?.trim() || state.currentPageUrl || "";
177
+ if (!pageId || (targetUrl && targetUrl !== state.currentPageUrl)) {
178
+ state.pageCounter += 1;
179
+ pageId = `page-${String(state.pageCounter).padStart(3, "0")}`;
180
+ state.currentPageId = pageId;
181
+ state.currentPageUrl = targetUrl || state.currentPageUrl;
182
+ }
183
+ state.snapshotCounter += 1;
184
+ relPath = `${state.record.bundleRelativePath}/pages/${pageId}/snapshot-${String(state.snapshotCounter).padStart(4, "0")}.html`;
185
+ await (0, client_1.writeXchangeRelativeFile)(this.config.terminal, state.session.cwd || "", this.config.exchange.dir, relPath, Buffer.from(String(event.html || ""), "utf8"));
186
+ await this.appendJsonLine(state, `${state.record.bundleRelativePath}/pages/events.ndjson`, {
187
+ at,
188
+ page_id: pageId,
189
+ reason: event.reason || "snapshot",
190
+ url: event.url || state.currentPageUrl || "",
191
+ title: event.title || state.record.tabTitle || "",
192
+ ready_state: event.ready_state || "",
193
+ html_rel_path: relPath.replace(`${state.record.bundleRelativePath}/`, ""),
194
+ });
195
+ }
196
+ else if (event.kind === "console_event") {
197
+ await this.appendJsonLine(state, `${state.record.bundleRelativePath}/console/events.ndjson`, {
198
+ at,
199
+ level: event.level || "log",
200
+ text: event.text || "",
201
+ args: event.args || [],
202
+ url: event.url || state.currentPageUrl || "",
203
+ title: event.title || state.record.tabTitle || "",
204
+ page_id: state.currentPageId,
205
+ });
206
+ }
207
+ else if (event.kind.startsWith("network_") ||
208
+ event.kind === "cookies_snapshot") {
209
+ const networkEvent = await this.materializeNetworkBodyArtifact(state, event);
210
+ const requestArtifact = await this.upsertNetworkRequestArtifact(state, networkEvent);
211
+ await this.appendJsonLine(state, `${state.record.bundleRelativePath}/network/index.ndjson`, {
212
+ at,
213
+ page_id: state.currentPageId,
214
+ request_id: networkEvent.request_id,
215
+ kind: networkEvent.kind,
216
+ method: networkEvent.method,
217
+ url: networkEvent.url,
218
+ resource_type: networkEvent.resource_type,
219
+ status_code: networkEvent.status_code,
220
+ summary: this.buildSummary(networkEvent),
221
+ ...(requestArtifact
222
+ ? {
223
+ seq: requestArtifact.seq,
224
+ request_bundle_rel_path: requestArtifact.requestDirRelPath.replace(`${state.record.bundleRelativePath}/`, ""),
225
+ meta_rel_path: requestArtifact.metaFileRelPath.replace(`${state.record.bundleRelativePath}/`, ""),
226
+ request_rel_path: requestArtifact.requestFileRelPath.replace(`${state.record.bundleRelativePath}/`, ""),
227
+ response_rel_path: requestArtifact.responseFileRelPath.replace(`${state.record.bundleRelativePath}/`, ""),
228
+ ...(this.extractBodyRelPath(requestArtifact)
229
+ ? {
230
+ body_rel_path: this.extractBodyRelPath(requestArtifact),
231
+ }
232
+ : {}),
233
+ }
234
+ : {}),
235
+ });
236
+ }
237
+ if (event.kind === "navigation") {
238
+ const nextUrl = event.url?.trim() || "";
239
+ if (nextUrl && nextUrl !== state.currentPageUrl) {
240
+ state.currentPageUrl = nextUrl;
241
+ }
242
+ }
243
+ if (event.kind === "session_stopped") {
244
+ state.record.status = "stopped";
245
+ state.record.stoppedAt = at;
246
+ }
247
+ const eventRecord = event;
248
+ const requestId = typeof eventRecord.request_id === "string" ? eventRecord.request_id : "";
249
+ const eventUrl = typeof eventRecord.url === "string" ? eventRecord.url : "";
250
+ const eventTitle = typeof eventRecord.title === "string" ? eventRecord.title : "";
251
+ const timelineEntry = {
252
+ at,
253
+ kind: event.kind,
254
+ ...(state.currentPageId ? { page_id: state.currentPageId } : {}),
255
+ ...(relPath
256
+ ? {
257
+ rel_path: relPath.replace(`${state.record.bundleRelativePath}/`, ""),
258
+ }
259
+ : {}),
260
+ ...(requestId.trim()
261
+ ? { request_id: requestId.trim() }
262
+ : {}),
263
+ ...(eventUrl.trim()
264
+ ? { url: eventUrl.trim() }
265
+ : {}),
266
+ ...(eventTitle.trim()
267
+ ? { title: eventTitle.trim() }
268
+ : {}),
269
+ summary: this.buildSummary(event),
270
+ };
271
+ await this.appendJsonLine(state, `${state.record.bundleRelativePath}/timeline.ndjson`, timelineEntry);
272
+ await this.writeSessionJson(state);
273
+ return state;
274
+ }
275
+ async finalizeExisting(input) {
276
+ const state = {
277
+ record: { ...input.record },
278
+ session: input.session,
279
+ currentPageId: undefined,
280
+ currentPageUrl: input.record.tabUrl,
281
+ pageCounter: 0,
282
+ snapshotCounter: 0,
283
+ requestCounter: 0,
284
+ requestArtifacts: new Map(),
285
+ };
286
+ await this.appendEvent(state, {
287
+ kind: "session_stopped",
288
+ status: "stopped",
289
+ });
290
+ return state.record;
291
+ }
292
+ async appendJsonLine(state, relativePath, value) {
293
+ await (0, client_1.writeXchangeRelativeFile)(this.config.terminal, state.session.cwd || "", this.config.exchange.dir, relativePath, formatJsonLine(value), { append: true });
294
+ }
295
+ async materializeNetworkBodyArtifact(state, event) {
296
+ const mimeType = detectMimeTypeFromHeaders(event.headers);
297
+ return {
298
+ ...event,
299
+ ...(mimeType ? { body_mime_type: mimeType } : {}),
300
+ ...(typeof event.body_text === "string" ? { body_encoding: "utf8" } : {}),
301
+ };
302
+ }
303
+ async upsertNetworkRequestArtifact(state, event) {
304
+ const requestId = typeof event.request_id === "string" && event.request_id.trim()
305
+ ? event.request_id.trim()
306
+ : "";
307
+ if (!requestId) {
308
+ return null;
309
+ }
310
+ let artifact = state.requestArtifacts.get(requestId);
311
+ if (!artifact) {
312
+ state.requestCounter += 1;
313
+ const requestDirRelPath = `${state.record.bundleRelativePath}/network/requests/${String(state.requestCounter).padStart(6, "0")}-${sanitizeRequestId(requestId)}`;
314
+ artifact = {
315
+ seq: state.requestCounter,
316
+ requestId,
317
+ requestDirRelPath,
318
+ metaFileRelPath: `${requestDirRelPath}/meta.json`,
319
+ requestFileRelPath: `${requestDirRelPath}/request.json`,
320
+ responseFileRelPath: `${requestDirRelPath}/response.json`,
321
+ request: {
322
+ request_id: requestId,
323
+ },
324
+ response: {
325
+ request_id: requestId,
326
+ },
327
+ };
328
+ state.requestArtifacts.set(requestId, artifact);
329
+ await (0, client_1.writeXchangeRelativeFile)(this.config.terminal, state.session.cwd || "", this.config.exchange.dir, `${requestDirRelPath}/.keep`, Buffer.from("", "utf8"));
330
+ }
331
+ const kind = typeof event.kind === "string" ? event.kind : "";
332
+ const artifactEvent = { ...event };
333
+ delete artifactEvent.body_base64;
334
+ delete artifactEvent.body_text;
335
+ delete artifactEvent.body_file_rel_path;
336
+ delete artifactEvent.body_mime_type;
337
+ delete artifactEvent.body_encoding;
338
+ if (kind === "network_request" || kind === "network_request_headers") {
339
+ artifact.request = {
340
+ ...artifact.request,
341
+ ...artifactEvent,
342
+ seq: artifact.seq,
343
+ };
344
+ }
345
+ else {
346
+ artifact.response = {
347
+ ...artifact.response,
348
+ ...artifactEvent,
349
+ seq: artifact.seq,
350
+ };
351
+ }
352
+ await (0, client_1.writeXchangeRelativeFile)(this.config.terminal, state.session.cwd || "", this.config.exchange.dir, artifact.requestFileRelPath, Buffer.from(`${JSON.stringify(artifact.request, null, 2)}\n`, "utf8"));
353
+ await (0, client_1.writeXchangeRelativeFile)(this.config.terminal, state.session.cwd || "", this.config.exchange.dir, artifact.responseFileRelPath, Buffer.from(`${JSON.stringify(artifact.response, null, 2)}\n`, "utf8"));
354
+ await this.writeRequestArtifactMeta(state, artifact);
355
+ await this.materializeRequestBodyArtifact(state, artifact, event, kind);
356
+ return artifact;
357
+ }
358
+ async materializeRequestBodyArtifact(state, artifact, event, kind) {
359
+ const bodyBase64 = typeof event.body_base64 === "string" && event.body_base64.trim()
360
+ ? event.body_base64.trim()
361
+ : "";
362
+ const bodyText = typeof event.body_text === "string" ? event.body_text : "";
363
+ const bodyMimeType = typeof event.body_mime_type === "string" && event.body_mime_type.trim()
364
+ ? event.body_mime_type.trim()
365
+ : undefined;
366
+ let bodyFileRelPath;
367
+ let bodyEncoding;
368
+ if (bodyBase64) {
369
+ bodyFileRelPath = `${artifact.requestDirRelPath}/${kind}-body.bin`;
370
+ bodyEncoding = "base64";
371
+ await (0, client_1.writeXchangeRelativeFile)(this.config.terminal, state.session.cwd || "", this.config.exchange.dir, bodyFileRelPath, Buffer.from(bodyBase64, "base64"));
372
+ }
373
+ else if (bodyText.length > 16384) {
374
+ bodyFileRelPath = `${artifact.requestDirRelPath}/${kind}-body.txt`;
375
+ bodyEncoding = "utf8";
376
+ await (0, client_1.writeXchangeRelativeFile)(this.config.terminal, state.session.cwd || "", this.config.exchange.dir, bodyFileRelPath, Buffer.from(bodyText, "utf8"));
377
+ }
378
+ if (kind === "network_request" || kind === "network_request_headers") {
379
+ artifact.request = {
380
+ ...artifact.request,
381
+ ...(bodyMimeType ? { body_mime_type: bodyMimeType } : {}),
382
+ ...(bodyEncoding ? { body_encoding: bodyEncoding } : {}),
383
+ ...(bodyFileRelPath
384
+ ? {
385
+ body_file_rel_path: bodyFileRelPath.replace(`${state.record.bundleRelativePath}/`, ""),
386
+ }
387
+ : {}),
388
+ ...(!bodyFileRelPath && bodyText ? { body_text: bodyText } : {}),
389
+ ...(bodyFileRelPath && bodyText
390
+ ? {
391
+ body_text_preview: bodyText.slice(0, 4096),
392
+ }
393
+ : {}),
394
+ };
395
+ }
396
+ else {
397
+ artifact.response = {
398
+ ...artifact.response,
399
+ ...(bodyMimeType ? { body_mime_type: bodyMimeType } : {}),
400
+ ...(bodyEncoding ? { body_encoding: bodyEncoding } : {}),
401
+ ...(bodyFileRelPath
402
+ ? {
403
+ body_file_rel_path: bodyFileRelPath.replace(`${state.record.bundleRelativePath}/`, ""),
404
+ }
405
+ : {}),
406
+ ...(!bodyFileRelPath && bodyText ? { body_text: bodyText } : {}),
407
+ ...(bodyFileRelPath && bodyText
408
+ ? {
409
+ body_text_preview: bodyText.slice(0, 4096),
410
+ }
411
+ : {}),
412
+ };
413
+ }
414
+ await (0, client_1.writeXchangeRelativeFile)(this.config.terminal, state.session.cwd || "", this.config.exchange.dir, artifact.requestFileRelPath, Buffer.from(`${JSON.stringify(artifact.request, null, 2)}\n`, "utf8"));
415
+ await (0, client_1.writeXchangeRelativeFile)(this.config.terminal, state.session.cwd || "", this.config.exchange.dir, artifact.responseFileRelPath, Buffer.from(`${JSON.stringify(artifact.response, null, 2)}\n`, "utf8"));
416
+ await this.writeRequestArtifactMeta(state, artifact);
417
+ }
418
+ extractBodyRelPath(artifact) {
419
+ const responseBody = typeof artifact.response.body_file_rel_path === "string"
420
+ ? artifact.response.body_file_rel_path
421
+ : "";
422
+ if (responseBody.trim()) {
423
+ return responseBody.trim();
424
+ }
425
+ const requestBody = typeof artifact.request.body_file_rel_path === "string"
426
+ ? artifact.request.body_file_rel_path
427
+ : "";
428
+ return requestBody.trim() || undefined;
429
+ }
430
+ async writeRequestArtifactMeta(state, artifact) {
431
+ const requestAt = typeof artifact.request.at === "string" && artifact.request.at.trim()
432
+ ? artifact.request.at.trim()
433
+ : undefined;
434
+ const responseAt = typeof artifact.response.at === "string" && artifact.response.at.trim()
435
+ ? artifact.response.at.trim()
436
+ : undefined;
437
+ const method = typeof artifact.request.method === "string" && artifact.request.method.trim()
438
+ ? artifact.request.method.trim()
439
+ : undefined;
440
+ const url = typeof artifact.request.url === "string" && artifact.request.url.trim()
441
+ ? artifact.request.url.trim()
442
+ : typeof artifact.response.url === "string" && artifact.response.url.trim()
443
+ ? artifact.response.url.trim()
444
+ : undefined;
445
+ const resourceType = typeof artifact.request.resource_type === "string" &&
446
+ artifact.request.resource_type.trim()
447
+ ? artifact.request.resource_type.trim()
448
+ : typeof artifact.response.resource_type === "string" &&
449
+ artifact.response.resource_type.trim()
450
+ ? artifact.response.resource_type.trim()
451
+ : undefined;
452
+ const statusCode = typeof artifact.response.status_code === "number"
453
+ ? artifact.response.status_code
454
+ : undefined;
455
+ const requestBodyRelPath = typeof artifact.request.body_file_rel_path === "string" &&
456
+ artifact.request.body_file_rel_path.trim()
457
+ ? artifact.request.body_file_rel_path.trim()
458
+ : undefined;
459
+ const responseBodyRelPath = typeof artifact.response.body_file_rel_path === "string" &&
460
+ artifact.response.body_file_rel_path.trim()
461
+ ? artifact.response.body_file_rel_path.trim()
462
+ : undefined;
463
+ const meta = {
464
+ seq: artifact.seq,
465
+ request_id: artifact.requestId,
466
+ ...(requestAt ? { request_at: requestAt } : {}),
467
+ ...(responseAt ? { response_at: responseAt } : {}),
468
+ ...(method ? { method } : {}),
469
+ ...(url ? { url } : {}),
470
+ ...(resourceType ? { resource_type: resourceType } : {}),
471
+ ...(typeof statusCode === "number" ? { status_code: statusCode } : {}),
472
+ request_rel_path: artifact.requestFileRelPath.replace(`${state.record.bundleRelativePath}/`, ""),
473
+ response_rel_path: artifact.responseFileRelPath.replace(`${state.record.bundleRelativePath}/`, ""),
474
+ ...(requestBodyRelPath ? { request_body_rel_path: requestBodyRelPath } : {}),
475
+ ...(responseBodyRelPath ? { response_body_rel_path: responseBodyRelPath } : {}),
476
+ };
477
+ await (0, client_1.writeXchangeRelativeFile)(this.config.terminal, state.session.cwd || "", this.config.exchange.dir, artifact.metaFileRelPath, Buffer.from(`${JSON.stringify(meta, null, 2)}\n`, "utf8"));
478
+ }
479
+ async writeSessionJson(state) {
480
+ await (0, client_1.writeXchangeRelativeFile)(this.config.terminal, state.session.cwd || "", this.config.exchange.dir, `${state.record.bundleRelativePath}/session.json`, Buffer.from(`${JSON.stringify({
481
+ recording_id: state.record.recordingId,
482
+ backend: state.record.backend,
483
+ session_id: state.record.sessionId,
484
+ session_label: state.session.label || state.session.sessionId,
485
+ instance_id: state.record.instanceId,
486
+ tab_id: state.record.tabId,
487
+ ...(state.record.tabTitle ? { tab_title: state.record.tabTitle } : {}),
488
+ ...(state.record.tabUrl ? { tab_url: state.record.tabUrl } : {}),
489
+ started_at: state.record.startedAt,
490
+ ...(state.record.stoppedAt
491
+ ? { stopped_at: state.record.stoppedAt }
492
+ : {}),
493
+ status: state.record.status,
494
+ event_count: state.record.eventCount,
495
+ ...(state.record.lastEventAt
496
+ ? { last_event_at: state.record.lastEventAt }
497
+ : {}),
498
+ bundle_dir_name: state.record.bundleDirName,
499
+ bundle_relative_path: state.record.bundleRelativePath,
500
+ bundle_path: state.record.bundlePath,
501
+ current_page_id: state.currentPageId,
502
+ current_page_url: state.currentPageUrl,
503
+ files: {
504
+ timeline: "timeline.ndjson",
505
+ session: "session.json",
506
+ pages: "pages",
507
+ network: "network",
508
+ network_index: "network/index.ndjson",
509
+ network_requests: "network/requests",
510
+ console: "console",
511
+ },
512
+ }, null, 2)}\n`, "utf8"));
513
+ }
514
+ buildSummary(event) {
515
+ switch (event.kind) {
516
+ case "console_event":
517
+ return `${event.level || "log"} ${event.text || ""}`.trim();
518
+ case "page_snapshot":
519
+ return `${event.reason || "snapshot"} ${event.title || event.url || ""}`.trim();
520
+ case "network_request":
521
+ return `${event.method || "GET"} ${event.url || ""}`.trim();
522
+ case "network_response_complete":
523
+ return `${event.status_code || 0} ${event.url || ""}`.trim();
524
+ case "network_error":
525
+ return `${event.error || "error"} ${event.url || ""}`.trim();
526
+ case "navigation":
527
+ return `${event.status || "navigation"} ${event.url || ""}`.trim();
528
+ case "session_started":
529
+ case "session_stopped":
530
+ return event.kind;
531
+ default:
532
+ return event.kind;
533
+ }
534
+ }
535
+ }
536
+ exports.BrowserRecordingBundleWriter = BrowserRecordingBundleWriter;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FirefoxAttachRegistry = void 0;
4
+ const localTimestamp_1 = require("../../../shared/lib/time/localTimestamp");
5
+ class FirefoxAttachRegistry {
6
+ instances = new Map();
7
+ setConnected(input) {
8
+ const now = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
9
+ const existing = this.instances.get(input.instanceId);
10
+ const next = {
11
+ instanceId: input.instanceId,
12
+ browser: input.browser,
13
+ extensionVersion: input.extensionVersion,
14
+ ...(input.profileName ? { profileName: input.profileName } : {}),
15
+ connectedAt: existing?.connectedAt ?? now,
16
+ lastSeenAt: now,
17
+ capabilities: [...input.capabilities],
18
+ tabs: existing?.tabs ?? [],
19
+ activeTab: existing?.activeTab ?? null,
20
+ };
21
+ this.instances.set(input.instanceId, next);
22
+ return next;
23
+ }
24
+ touch(instanceId) {
25
+ const existing = this.instances.get(instanceId);
26
+ if (!existing) {
27
+ return;
28
+ }
29
+ existing.lastSeenAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
30
+ }
31
+ setTabs(instanceId, tabs) {
32
+ const existing = this.instances.get(instanceId);
33
+ if (!existing) {
34
+ return;
35
+ }
36
+ existing.tabs = tabs.map((tab) => ({ ...tab }));
37
+ existing.activeTab =
38
+ tabs.find((tab) => tab.active) ?? existing.activeTab ?? null;
39
+ existing.lastSeenAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
40
+ }
41
+ setActiveTab(instanceId, tab) {
42
+ const existing = this.instances.get(instanceId);
43
+ if (!existing) {
44
+ return;
45
+ }
46
+ existing.activeTab = tab ? { ...tab } : null;
47
+ existing.lastSeenAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
48
+ }
49
+ updateTab(instanceId, tab) {
50
+ const existing = this.instances.get(instanceId);
51
+ if (!existing) {
52
+ return;
53
+ }
54
+ const nextTabs = [...existing.tabs];
55
+ const index = nextTabs.findIndex((item) => item.tab_id === tab.tab_id);
56
+ if (index >= 0) {
57
+ nextTabs[index] = { ...nextTabs[index], ...tab };
58
+ }
59
+ else {
60
+ nextTabs.push({ ...tab });
61
+ }
62
+ existing.tabs = nextTabs;
63
+ if (tab.active) {
64
+ existing.activeTab = { ...tab };
65
+ }
66
+ existing.lastSeenAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
67
+ }
68
+ remove(instanceId) {
69
+ this.instances.delete(instanceId);
70
+ }
71
+ listInstances() {
72
+ return Array.from(this.instances.values()).map((item) => ({
73
+ ...item,
74
+ tabs: item.tabs.map((tab) => ({ ...tab })),
75
+ activeTab: item.activeTab ? { ...item.activeTab } : null,
76
+ capabilities: [...item.capabilities],
77
+ }));
78
+ }
79
+ }
80
+ exports.FirefoxAttachRegistry = FirefoxAttachRegistry;