@deadragdoll/tellymcp 0.0.13 → 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 (64) 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 +14 -0
  5. package/README.md +14 -0
  6. package/TOOLS.md +182 -10
  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 +28 -1
  11. package/dist/services/features/telegram-mcp/browser.service.js +18 -0
  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 +18 -0
  17. package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +9 -1
  18. package/dist/services/features/telegram-mcp/src/app/config/env.js +119 -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 +62 -19
  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/browserDetachTabTool.js +28 -0
  27. package/dist/services/features/telegram-mcp/src/features/browser/model/browserListAttachedInstancesTool.js +1 -1
  28. package/dist/services/features/telegram-mcp/src/features/browser/model/browserListTabsTool.js +1 -1
  29. package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +410 -27
  30. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/browserRecordingBundle.js +37 -3
  31. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachRegistry.js +7 -6
  32. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js +214 -37
  33. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/types.js +186 -0
  34. package/dist/services/features/telegram-mcp/src/features/collaboration/model/collaborationService.js +2 -0
  35. package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerFileService.js +6 -3
  36. package/dist/services/features/telegram-mcp/src/features/distributed-client/model/gatewayClientAccess.js +4 -1
  37. package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +33 -35
  38. package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileListTool.js +33 -0
  39. package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileService.js +327 -0
  40. package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileTool.js +81 -0
  41. package/dist/services/features/telegram-mcp/src/features/file-content/model/temporaryFileLinkStore.js +307 -0
  42. package/dist/services/features/telegram-mcp/src/features/file-content/model/workspaceFilePolicy.js +115 -0
  43. package/dist/services/features/telegram-mcp/src/features/notify/model/notifyService.js +5 -1
  44. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +3 -0
  45. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportFileHandoffActions.js +6 -3
  46. package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/client.js +29 -6
  47. package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/ptyRegistry.js +100 -2
  48. package/dist/services/features/telegram-mcp/src/shared/lib/bodyLimits.js +63 -0
  49. package/dist/services/features/telegram-mcp/src/shared/lib/gatewayAuth.js +13 -0
  50. package/dist/services/features/telegram-mcp/src/shared/lib/time/localTimestamp.js +21 -0
  51. package/docs/CHAT_CONNECTOR.md +134 -0
  52. package/docs/STANDALONE-ru.md +13 -0
  53. package/docs/STANDALONE.md +13 -0
  54. package/package.json +5 -3
  55. package/packages/chrome-attach-extension/dist/background.js +572 -163
  56. package/packages/chrome-attach-extension/dist/manifest.json +2 -1
  57. package/packages/chrome-attach-extension/dist/options.js +15 -2
  58. package/packages/chrome-attach-extension/dist/recorder-content.js +14 -1
  59. package/packages/chrome-attach-extension/dist/recorder-page.js +34 -18
  60. package/packages/firefox-attach-extension/dist/background.js +413 -33
  61. package/packages/firefox-attach-extension/dist/manifest.json +0 -12
  62. package/packages/firefox-attach-extension/dist/options.js +14 -1
  63. package/packages/firefox-attach-extension/dist/recorder-content.js +14 -1
  64. package/packages/firefox-attach-extension/dist/recorder-page.js +34 -18
@@ -13,16 +13,34 @@ const POPUP_COMMAND_RESULT_KEY = "attach_popup_command_result";
13
13
  const RECONNECT_DELAY_MS = 3000;
14
14
  const HEARTBEAT_INTERVAL_MS = 15000;
15
15
  const MAX_CAPTURE_BYTES = 512 * 1024;
16
+ const MAX_BUFFERED_LOG_ENTRIES = 200;
16
17
 
17
18
  let socket = null;
18
19
  let reconnectTimer = null;
19
20
  let heartbeatTimer = null;
20
21
  let instanceId = null;
22
+ let attachedTabId = null;
21
23
  let manualDisconnect = false;
22
24
  const pendingManualRecordingRequests = new Map();
23
25
 
24
26
  const activeRecordingsById = new Map();
25
27
  const activeRecordingIdByTabId = new Map();
28
+ const consoleMessagesByTabId = new Map();
29
+ const pageErrorsByTabId = new Map();
30
+ const networkFailuresByTabId = new Map();
31
+
32
+ function padNumber(value, length = 2) {
33
+ return String(value).padStart(length, "0");
34
+ }
35
+
36
+ function formatLocalTimestamp(date = new Date()) {
37
+ return `${date.getFullYear()}-${padNumber(date.getMonth() + 1)}-${padNumber(date.getDate())}T${padNumber(
38
+ date.getHours(),
39
+ )}:${padNumber(date.getMinutes())}:${padNumber(date.getSeconds())}.${padNumber(
40
+ date.getMilliseconds(),
41
+ 3,
42
+ )}`;
43
+ }
26
44
 
27
45
  async function getSettings() {
28
46
  return browser.storage.local.get({
@@ -44,6 +62,10 @@ async function setLocalInstanceId(value) {
44
62
  }
45
63
 
46
64
  async function setAttachedTab(tab) {
65
+ attachedTabId =
66
+ tab && Number.isInteger(tab.tab_id)
67
+ ? Number(tab.tab_id)
68
+ : null;
47
69
  await browser.storage.local.set({
48
70
  [ATTACHED_TAB_KEY]: tab,
49
71
  });
@@ -55,6 +77,15 @@ async function setRecordingStatus(status) {
55
77
  });
56
78
  }
57
79
 
80
+ async function hydrateAttachedTabSelection() {
81
+ const stored = await browser.storage.local.get({ [ATTACHED_TAB_KEY]: null });
82
+ const tab = stored[ATTACHED_TAB_KEY];
83
+ attachedTabId =
84
+ tab && Number.isInteger(tab.tab_id)
85
+ ? Number(tab.tab_id)
86
+ : null;
87
+ }
88
+
58
89
  function buildWebSocketUrl(settings) {
59
90
  return `ws://${settings.host}:${settings.port}/browser-attach/ws`;
60
91
  }
@@ -109,14 +140,157 @@ async function ensureTabIsActive(tabId) {
109
140
  return await browser.tabs.get(tabId);
110
141
  }
111
142
 
143
+ function trimBufferedEntries(entries) {
144
+ if (entries.length <= MAX_BUFFERED_LOG_ENTRIES) {
145
+ return entries;
146
+ }
147
+ return entries.slice(entries.length - MAX_BUFFERED_LOG_ENTRIES);
148
+ }
149
+
150
+ function appendBufferedEntry(store, tabId, entry) {
151
+ if (!Number.isInteger(tabId) || tabId < 0 || !entry || typeof entry !== "object") {
152
+ return;
153
+ }
154
+ const existing = store.get(tabId) || [];
155
+ existing.push(entry);
156
+ store.set(tabId, trimBufferedEntries(existing));
157
+ }
158
+
159
+ function extractErrorStack(args) {
160
+ if (!Array.isArray(args)) {
161
+ return "";
162
+ }
163
+ for (const arg of args) {
164
+ if (!arg || typeof arg !== "object") {
165
+ continue;
166
+ }
167
+ if (typeof arg.stack === "string" && arg.stack.trim()) {
168
+ return arg.stack;
169
+ }
170
+ }
171
+ return "";
172
+ }
173
+
174
+ function extractConsoleLocation(event) {
175
+ if (!event || typeof event !== "object" || !Array.isArray(event.args)) {
176
+ return "";
177
+ }
178
+ for (const arg of event.args) {
179
+ if (!arg || typeof arg !== "object") {
180
+ continue;
181
+ }
182
+ if (typeof arg.filename === "string" && arg.filename.trim()) {
183
+ const line = Number.isFinite(arg.lineno) ? Number(arg.lineno) : 0;
184
+ const column = Number.isFinite(arg.colno) ? Number(arg.colno) : 0;
185
+ return `${arg.filename}:${line}:${column}`;
186
+ }
187
+ }
188
+ return "";
189
+ }
190
+
191
+ function recordPageEventForBuffers(tabId, event) {
192
+ if (!Number.isInteger(tabId) || tabId < 0 || !event || typeof event !== "object") {
193
+ return;
194
+ }
195
+ if (event.kind !== "console_event") {
196
+ return;
197
+ }
198
+
199
+ const timestamp =
200
+ typeof event.at === "string" && event.at.trim()
201
+ ? event.at
202
+ : formatLocalTimestamp(new Date());
203
+ const text = typeof event.text === "string" ? event.text : "";
204
+ const location = extractConsoleLocation(event);
205
+
206
+ appendBufferedEntry(consoleMessagesByTabId, tabId, {
207
+ type:
208
+ typeof event.level === "string" && event.level.trim()
209
+ ? event.level
210
+ : "log",
211
+ text,
212
+ ...(location ? { location } : {}),
213
+ timestamp,
214
+ });
215
+
216
+ if (event.level === "error" || event.level === "assert") {
217
+ const stack = extractErrorStack(event.args);
218
+ appendBufferedEntry(pageErrorsByTabId, tabId, {
219
+ message: text || "Console error",
220
+ ...(stack ? { stack } : {}),
221
+ timestamp,
222
+ });
223
+ }
224
+ }
225
+
226
+ function recordNetworkFailure(tabId, event) {
227
+ if (!Number.isInteger(tabId) || tabId < 0 || !event || typeof event !== "object") {
228
+ return;
229
+ }
230
+ appendBufferedEntry(networkFailuresByTabId, tabId, {
231
+ url: typeof event.url === "string" ? event.url : "",
232
+ method:
233
+ typeof event.method === "string" && event.method.trim()
234
+ ? event.method
235
+ : "GET",
236
+ ...(typeof event.status === "number" ? { status: event.status } : {}),
237
+ ...(typeof event.error_text === "string" ? { error_text: event.error_text } : {}),
238
+ ...(typeof event.resource_type === "string"
239
+ ? { resource_type: event.resource_type }
240
+ : {}),
241
+ timestamp:
242
+ typeof event.timestamp === "string" && event.timestamp.trim()
243
+ ? event.timestamp
244
+ : formatLocalTimestamp(new Date()),
245
+ });
246
+ }
247
+
248
+ function getBufferedLogSnapshot(tabId, limit) {
249
+ const normalize = (value) =>
250
+ Number.isInteger(value) && value > 0 ? Number(value) : undefined;
251
+ const sliceEntries = (entries) => {
252
+ const normalizedLimit = normalize(limit);
253
+ return normalizedLimit ? entries.slice(-normalizedLimit) : entries;
254
+ };
255
+
256
+ const consoleMessages = consoleMessagesByTabId.get(tabId) || [];
257
+ const pageErrors = pageErrorsByTabId.get(tabId) || [];
258
+ const networkFailures = networkFailuresByTabId.get(tabId) || [];
259
+
260
+ return {
261
+ console_messages: sliceEntries(consoleMessages),
262
+ console_total: consoleMessages.length,
263
+ page_errors: sliceEntries(pageErrors),
264
+ page_error_total: pageErrors.length,
265
+ network_failures: sliceEntries(networkFailures),
266
+ network_failure_total: networkFailures.length,
267
+ };
268
+ }
269
+
270
+ function clearBufferedLogs(tabId) {
271
+ const consoleMessages = consoleMessagesByTabId.get(tabId) || [];
272
+ const pageErrors = pageErrorsByTabId.get(tabId) || [];
273
+ const networkFailures = networkFailuresByTabId.get(tabId) || [];
274
+ consoleMessagesByTabId.delete(tabId);
275
+ pageErrorsByTabId.delete(tabId);
276
+ networkFailuresByTabId.delete(tabId);
277
+ return {
278
+ console_messages_cleared: consoleMessages.length,
279
+ page_errors_cleared: pageErrors.length,
280
+ network_failures_cleared: networkFailures.length,
281
+ };
282
+ }
283
+
112
284
  function buildTabActionCode(action, payload) {
113
285
  const serializedAction = JSON.stringify(action);
114
286
  const serializedPayload = JSON.stringify(payload || {});
115
287
 
116
- return `(() => {
288
+ return `(async () => {
117
289
  const action = ${serializedAction};
118
290
  const payload = ${serializedPayload};
119
291
  const normalize = (value) => typeof value === "string" ? value.trim() : "";
292
+ const timeoutMs = Number.isInteger(payload.timeout_ms) ? payload.timeout_ms : 30000;
293
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
120
294
  const byText = (text, exact) => {
121
295
  const needle = normalize(text);
122
296
  if (!needle) return null;
@@ -131,6 +305,11 @@ function buildTabActionCode(action, payload) {
131
305
  }
132
306
  return null;
133
307
  };
308
+ const isVisible = (element) => {
309
+ if (!element) return false;
310
+ const computed = window.getComputedStyle(element);
311
+ return computed.display !== "none" && computed.visibility !== "hidden" && computed.opacity !== "0";
312
+ };
134
313
  const resolveTarget = () => {
135
314
  const aiTag = normalize(payload.ai_tag);
136
315
  if (aiTag) {
@@ -153,10 +332,6 @@ function buildTabActionCode(action, payload) {
153
332
  if (target && typeof target.scrollIntoView === "function") {
154
333
  target.scrollIntoView({ block: "center", inline: "center" });
155
334
  }
156
- const toVisible = (element) => {
157
- const computed = window.getComputedStyle(element);
158
- return computed.display !== "none" && computed.visibility !== "hidden" && computed.opacity !== "0";
159
- };
160
335
  if (action === "dom") {
161
336
  const attributes = target
162
337
  ? Object.fromEntries(Array.from(target.attributes || []).map((attr) => [attr.name, attr.value]))
@@ -167,13 +342,49 @@ function buildTabActionCode(action, payload) {
167
342
  found: Boolean(target),
168
343
  outer_html: payload.include_html === false ? undefined : target.outerHTML,
169
344
  text_content: payload.include_text === false ? undefined : (target.textContent || "").trim(),
170
- visible: target ? toVisible(target) : false,
345
+ visible: target ? isVisible(target) : false,
171
346
  attributes,
172
347
  url: location.href,
173
348
  title: document.title,
174
349
  },
175
350
  };
176
351
  }
352
+ if (action === "computed_style") {
353
+ if (!target) {
354
+ return {
355
+ ok: true,
356
+ result: {
357
+ found: false,
358
+ url: location.href,
359
+ title: document.title,
360
+ },
361
+ };
362
+ }
363
+ const requestedProperties = Array.isArray(payload.properties) && payload.properties.length
364
+ ? payload.properties.map((item) => String(item))
365
+ : ["display","position","visibility","opacity","color","background-color","font-size","z-index","overflow"];
366
+ const computed = window.getComputedStyle(target);
367
+ const rect = target.getBoundingClientRect();
368
+ const styles = Object.fromEntries(
369
+ requestedProperties.map((property) => [property, computed.getPropertyValue(property)]),
370
+ );
371
+ return {
372
+ ok: true,
373
+ result: {
374
+ found: true,
375
+ visible: isVisible(target),
376
+ styles,
377
+ box: {
378
+ x: rect.x,
379
+ y: rect.y,
380
+ width: rect.width,
381
+ height: rect.height,
382
+ },
383
+ url: location.href,
384
+ title: document.title,
385
+ },
386
+ };
387
+ }
177
388
  if (action === "click") {
178
389
  target.click();
179
390
  return {
@@ -184,6 +395,65 @@ function buildTabActionCode(action, payload) {
184
395
  },
185
396
  };
186
397
  }
398
+ if (action === "wait_for") {
399
+ const waitState = normalize(payload.state) || "visible";
400
+ const startedAt = Date.now();
401
+ while (Date.now() - startedAt <= timeoutMs) {
402
+ const current = resolveTarget();
403
+ const visible = current ? isVisible(current) : false;
404
+ if (
405
+ (waitState === "attached" && current) ||
406
+ (waitState === "detached" && !current) ||
407
+ (waitState === "visible" && current && visible) ||
408
+ (waitState === "hidden" && (!current || !visible))
409
+ ) {
410
+ return {
411
+ ok: true,
412
+ result: {
413
+ url: location.href,
414
+ title: document.title,
415
+ },
416
+ };
417
+ }
418
+ await sleep(100);
419
+ }
420
+ return {
421
+ ok: false,
422
+ error: "Timed out waiting for the requested element state.",
423
+ };
424
+ }
425
+ if (action === "wait_for_url") {
426
+ const exactUrl = normalize(payload.url);
427
+ const containsUrl = normalize(payload.url_contains);
428
+ const startedAt = Date.now();
429
+ while (Date.now() - startedAt <= timeoutMs) {
430
+ const currentUrl = String(location.href || "");
431
+ if ((exactUrl && currentUrl === exactUrl) || (containsUrl && currentUrl.includes(containsUrl))) {
432
+ return {
433
+ ok: true,
434
+ result: {
435
+ url: currentUrl,
436
+ title: document.title,
437
+ },
438
+ };
439
+ }
440
+ await sleep(100);
441
+ }
442
+ return {
443
+ ok: false,
444
+ error: "Timed out waiting for the requested URL.",
445
+ };
446
+ }
447
+ if (action === "reload") {
448
+ location.reload();
449
+ return {
450
+ ok: true,
451
+ result: {
452
+ url: location.href,
453
+ title: document.title,
454
+ },
455
+ };
456
+ }
187
457
  if (action === "fill") {
188
458
  target.focus();
189
459
  target.value = payload.value || "";
@@ -246,16 +516,84 @@ function buildTabActionCode(action, payload) {
246
516
  async function runTabAction(tabId, action, payload) {
247
517
  const activeTab = await ensureTabIsActive(tabId);
248
518
 
519
+ if (action === "attach") {
520
+ const record = {
521
+ tab_id: activeTab.id,
522
+ window_id: activeTab.windowId,
523
+ active: activeTab.active === true,
524
+ title: activeTab.title || "",
525
+ url: activeTab.url || "",
526
+ status: activeTab.status || "",
527
+ };
528
+ await setAttachedTab(record);
529
+ try {
530
+ await injectRecorderContent(tabId);
531
+ } catch {
532
+ // ignore
533
+ }
534
+ return {
535
+ ok: true,
536
+ result: {
537
+ url: record.url,
538
+ title: record.title,
539
+ },
540
+ };
541
+ }
542
+
543
+ if (action === "detach") {
544
+ await setAttachedTab(null);
545
+ return {
546
+ ok: true,
547
+ result: {
548
+ url: activeTab.url || "",
549
+ title: activeTab.title || "",
550
+ },
551
+ };
552
+ }
553
+
554
+ if (action === "close") {
555
+ if (attachedTabId === tabId) {
556
+ await setAttachedTab(null);
557
+ }
558
+ await browser.tabs.remove(tabId);
559
+ return {
560
+ ok: true,
561
+ result: {
562
+ url: activeTab.url || "",
563
+ title: activeTab.title || "",
564
+ },
565
+ };
566
+ }
567
+
568
+ if (action === "get_logs") {
569
+ return {
570
+ ok: true,
571
+ result: getBufferedLogSnapshot(
572
+ tabId,
573
+ Number.isInteger(payload?.limit) ? Number(payload.limit) : undefined,
574
+ ),
575
+ };
576
+ }
577
+
578
+ if (action === "clear_logs") {
579
+ return {
580
+ ok: true,
581
+ result: clearBufferedLogs(tabId),
582
+ };
583
+ }
584
+
249
585
  if (action === "screenshot") {
250
- const dataUrl = await browser.tabs.captureTab(activeTab.windowId, {
586
+ await new Promise((resolve) => setTimeout(resolve, 180));
587
+ const refreshedTab = await browser.tabs.get(tabId).catch(() => activeTab);
588
+ const dataUrl = await browser.tabs.captureTab(refreshedTab.id, {
251
589
  format: "png",
252
590
  });
253
591
  return {
254
592
  ok: true,
255
593
  result: {
256
594
  png_base64: String(dataUrl).replace(/^data:image\/png;base64,/, ""),
257
- url: activeTab.url || "",
258
- title: activeTab.title || "",
595
+ url: refreshedTab.url || "",
596
+ title: refreshedTab.title || "",
259
597
  },
260
598
  };
261
599
  }
@@ -320,7 +658,7 @@ function startHeartbeat() {
320
658
  heartbeatTimer = setInterval(() => {
321
659
  sendJson({
322
660
  type: "heartbeat",
323
- sent_at: new Date().toISOString(),
661
+ sent_at: formatLocalTimestamp(new Date()),
324
662
  });
325
663
  }, HEARTBEAT_INTERVAL_MS);
326
664
  }
@@ -391,7 +729,7 @@ function sendRecordingEvent(recordingId, tabId, event) {
391
729
  tab_id: tabId,
392
730
  event: {
393
731
  ...event,
394
- at: event.at || new Date().toISOString(),
732
+ at: event.at || formatLocalTimestamp(new Date()),
395
733
  },
396
734
  });
397
735
  }
@@ -409,7 +747,7 @@ async function captureTabSnapshot(tabId, reason) {
409
747
  kind: "page_snapshot",
410
748
  source: "background",
411
749
  reason: ${JSON.stringify(reason)},
412
- at: new Date().toISOString(),
750
+ at: formatLocalTimestamp(new Date()),
413
751
  url: location.href,
414
752
  title: document.title,
415
753
  ready_state: document.readyState,
@@ -463,7 +801,7 @@ async function startRecording(message) {
463
801
  tabId,
464
802
  tabTitle: browserTab.title || "",
465
803
  tabUrl: browserTab.url || "",
466
- startedAt: new Date().toISOString(),
804
+ startedAt: formatLocalTimestamp(new Date()),
467
805
  });
468
806
  activeRecordingIdByTabId.set(tabId, message.recording_id);
469
807
 
@@ -583,6 +921,11 @@ async function handlePopupCommand(command) {
583
921
  tab: record,
584
922
  });
585
923
  await setAttachedTab(record);
924
+ try {
925
+ await injectRecorderContent(tabId);
926
+ } catch {
927
+ // ignore recorder bootstrap errors for manual attach
928
+ }
586
929
 
587
930
  return {
588
931
  ok: true,
@@ -827,7 +1170,7 @@ browser.storage.onChanged.addListener((changes, areaName) => {
827
1170
  [POPUP_COMMAND_RESULT_KEY]: {
828
1171
  command_id: command.command_id,
829
1172
  result,
830
- at: new Date().toISOString(),
1173
+ at: formatLocalTimestamp(new Date()),
831
1174
  },
832
1175
  }),
833
1176
  )
@@ -839,7 +1182,7 @@ browser.storage.onChanged.addListener((changes, areaName) => {
839
1182
  ok: false,
840
1183
  error: error instanceof Error ? error.message : String(error),
841
1184
  },
842
- at: new Date().toISOString(),
1185
+ at: formatLocalTimestamp(new Date()),
843
1186
  },
844
1187
  }),
845
1188
  );
@@ -889,34 +1232,41 @@ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
889
1232
  });
890
1233
 
891
1234
  const recording = getRecordingByTabId(tabId);
892
- if (!recording) {
1235
+ const shouldTrackTab = recording || attachedTabId === tabId;
1236
+ if (!shouldTrackTab) {
893
1237
  return;
894
1238
  }
895
1239
 
896
- sendRecordingEvent(recording.recordingId, tabId, {
897
- kind: "navigation",
898
- source: "browser",
899
- status: changeInfo.status || tab.status || "",
900
- url: tab.url || "",
901
- title: tab.title || "",
902
- });
1240
+ if (recording) {
1241
+ sendRecordingEvent(recording.recordingId, tabId, {
1242
+ kind: "navigation",
1243
+ source: "browser",
1244
+ status: changeInfo.status || tab.status || "",
1245
+ url: tab.url || "",
1246
+ title: tab.title || "",
1247
+ });
1248
+ }
903
1249
 
904
1250
  if (changeInfo.status === "complete") {
905
1251
  try {
906
1252
  await injectRecorderContent(tabId);
907
- const snapshot = await captureTabSnapshot(tabId, "tab-updated-complete");
908
- if (snapshot) {
909
- sendRecordingEvent(recording.recordingId, tabId, snapshot);
1253
+ if (recording) {
1254
+ const snapshot = await captureTabSnapshot(tabId, "tab-updated-complete");
1255
+ if (snapshot) {
1256
+ sendRecordingEvent(recording.recordingId, tabId, snapshot);
1257
+ }
910
1258
  }
911
1259
  } catch {
912
1260
  // ignore
913
1261
  }
914
- await emitCookiesSnapshot(
915
- recording.recordingId,
916
- tabId,
917
- tab.url || "",
918
- tab.title || "",
919
- );
1262
+ if (recording) {
1263
+ await emitCookiesSnapshot(
1264
+ recording.recordingId,
1265
+ tabId,
1266
+ tab.url || "",
1267
+ tab.title || "",
1268
+ );
1269
+ }
920
1270
  }
921
1271
  });
922
1272
 
@@ -967,6 +1317,11 @@ async function handleRuntimeMessage(message, sender) {
967
1317
  tab: record,
968
1318
  });
969
1319
  await setAttachedTab(record);
1320
+ try {
1321
+ await injectRecorderContent(tabId);
1322
+ } catch {
1323
+ // ignore recorder bootstrap errors for manual attach
1324
+ }
970
1325
 
971
1326
  return {
972
1327
  ok: true,
@@ -1027,9 +1382,10 @@ async function handleRuntimeMessage(message, sender) {
1027
1382
  if (!Number.isInteger(tabId)) {
1028
1383
  return undefined;
1029
1384
  }
1385
+ recordPageEventForBuffers(tabId, message.event || {});
1030
1386
  const recording = getRecordingByTabId(tabId);
1031
1387
  if (!recording) {
1032
- return undefined;
1388
+ return { ok: true };
1033
1389
  }
1034
1390
  sendRecordingEvent(recording.recordingId, tabId, message.event || {});
1035
1391
  return { ok: true };
@@ -1202,6 +1558,20 @@ browser.webRequest.onHeadersReceived.addListener(
1202
1558
 
1203
1559
  browser.webRequest.onCompleted.addListener(
1204
1560
  (details) => {
1561
+ if (
1562
+ Number.isInteger(details.tabId) &&
1563
+ details.tabId >= 0 &&
1564
+ typeof details.statusCode === "number" &&
1565
+ details.statusCode >= 400
1566
+ ) {
1567
+ recordNetworkFailure(details.tabId, {
1568
+ url: details.url,
1569
+ method: details.method,
1570
+ status: details.statusCode,
1571
+ resource_type: details.type,
1572
+ timestamp: formatLocalTimestamp(new Date()),
1573
+ });
1574
+ }
1205
1575
  const recording = getRecordingByTabId(details.tabId);
1206
1576
  if (!recording) {
1207
1577
  return;
@@ -1221,6 +1591,15 @@ browser.webRequest.onCompleted.addListener(
1221
1591
 
1222
1592
  browser.webRequest.onErrorOccurred.addListener(
1223
1593
  (details) => {
1594
+ if (Number.isInteger(details.tabId) && details.tabId >= 0) {
1595
+ recordNetworkFailure(details.tabId, {
1596
+ url: details.url,
1597
+ method: details.method,
1598
+ error_text: details.error,
1599
+ resource_type: details.type,
1600
+ timestamp: formatLocalTimestamp(new Date()),
1601
+ });
1602
+ }
1224
1603
  const recording = getRecordingByTabId(details.tabId);
1225
1604
  if (!recording) {
1226
1605
  return;
@@ -1238,5 +1617,6 @@ browser.webRequest.onErrorOccurred.addListener(
1238
1617
  { urls: ["<all_urls>"] },
1239
1618
  );
1240
1619
 
1620
+ void hydrateAttachedTabSelection();
1241
1621
  void computeInstanceId();
1242
1622
  void connect();
@@ -33,18 +33,6 @@
33
33
  "page": "options.html",
34
34
  "browser_style": true
35
35
  },
36
- "content_scripts": [
37
- {
38
- "matches": [
39
- "<all_urls>"
40
- ],
41
- "js": [
42
- "recorder-content.js"
43
- ],
44
- "run_at": "document_start",
45
- "all_frames": true
46
- }
47
- ],
48
36
  "web_accessible_resources": [
49
37
  "recorder-page.js"
50
38
  ],
@@ -18,6 +18,19 @@ const ATTACHED_TAB_KEY = "attach_selected_tab";
18
18
  const POPUP_COMMAND_KEY = "attach_popup_command";
19
19
  const POPUP_COMMAND_RESULT_KEY = "attach_popup_command_result";
20
20
 
21
+ function padNumber(value, length = 2) {
22
+ return String(value).padStart(length, "0");
23
+ }
24
+
25
+ function formatLocalTimestamp(date = new Date()) {
26
+ return `${date.getFullYear()}-${padNumber(date.getMonth() + 1)}-${padNumber(date.getDate())}T${padNumber(
27
+ date.getHours(),
28
+ )}:${padNumber(date.getMinutes())}:${padNumber(date.getSeconds())}.${padNumber(
29
+ date.getMilliseconds(),
30
+ 3,
31
+ )}`;
32
+ }
33
+
21
34
  function storageGet(defaults) {
22
35
  return browser.storage.local.get(defaults);
23
36
  }
@@ -412,7 +425,7 @@ async function sendPopupCommand(type, payload = {}, timeoutMs = 15000) {
412
425
  command_id: commandId,
413
426
  type,
414
427
  ...payload,
415
- at: new Date().toISOString(),
428
+ at: formatLocalTimestamp(new Date()),
416
429
  },
417
430
  });
418
431
  try {