@deadragdoll/tellymcp 0.0.13 → 0.0.15

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 (86) hide show
  1. package/.env.example.client +23 -39
  2. package/.env.example.gateway +52 -61
  3. package/CHANGELOG.md +57 -0
  4. package/README-ru.md +80 -6
  5. package/README.md +80 -6
  6. package/TOOLS.md +219 -10
  7. package/config/templates/env.both.template +19 -7
  8. package/config/templates/env.client.template +9 -19
  9. package/config/templates/env.gateway.template +19 -31
  10. package/dist/cli.js +252 -47
  11. package/dist/configureServer.js +966 -0
  12. package/dist/envMigration.js +316 -0
  13. package/dist/moleculer.config.js +1 -3
  14. package/dist/services/features/telegram-mcp/approval.service.js +1 -1
  15. package/dist/services/features/telegram-mcp/browser.service.js +18 -0
  16. package/dist/services/features/telegram-mcp/collaboration.service.js +2 -2
  17. package/dist/services/features/telegram-mcp/ensuredb.service.js +1 -1
  18. package/dist/services/features/telegram-mcp/file-content.service.js +94 -0
  19. package/dist/services/features/telegram-mcp/gateway-delivery.service.js +5 -1
  20. package/dist/services/features/telegram-mcp/gateway-socket.service.js +43 -11
  21. package/dist/services/features/telegram-mcp/gateway.service.js +1 -1
  22. package/dist/services/features/telegram-mcp/mcp-http.service.js +1 -0
  23. package/dist/services/features/telegram-mcp/mcp-server.service.js +20 -0
  24. package/dist/services/features/telegram-mcp/session-context.service.js +25 -1
  25. package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +79 -67
  26. package/dist/services/features/telegram-mcp/src/app/config/env.js +129 -38
  27. package/dist/services/features/telegram-mcp/src/app/config/environmentContract.js +66 -0
  28. package/dist/services/features/telegram-mcp/src/app/http.js +139 -99
  29. package/dist/services/features/telegram-mcp/src/app/oauthFacade.js +642 -0
  30. package/dist/services/features/telegram-mcp/src/app/webapp/assets.js +151 -170
  31. package/dist/services/features/telegram-mcp/src/app/webapp/auth.js +96 -99
  32. package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +88 -19
  33. package/dist/services/features/telegram-mcp/src/features/browser/model/browserAttachActiveTabTool.js +28 -0
  34. package/dist/services/features/telegram-mcp/src/features/browser/model/browserAttachTabTool.js +28 -0
  35. package/dist/services/features/telegram-mcp/src/features/browser/model/browserDetachTabTool.js +28 -0
  36. package/dist/services/features/telegram-mcp/src/features/browser/model/browserListAttachedInstancesTool.js +1 -1
  37. package/dist/services/features/telegram-mcp/src/features/browser/model/browserListTabsTool.js +1 -1
  38. package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +412 -29
  39. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/browserRecordingBundle.js +37 -3
  40. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachRegistry.js +7 -6
  41. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js +214 -37
  42. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/types.js +186 -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/gatewaySessionsService.js +5 -5
  45. package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerFileService.js +6 -3
  46. package/dist/services/features/telegram-mcp/src/features/distributed-client/model/gatewayClientAccess.js +5 -2
  47. package/dist/services/features/telegram-mcp/src/features/distributed-client/model/gatewayCollaborationBackend.js +4 -4
  48. package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +33 -36
  49. package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/remoteConsoleActionClient.js +4 -3
  50. package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileListTool.js +33 -0
  51. package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileService.js +327 -0
  52. package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileTool.js +81 -0
  53. package/dist/services/features/telegram-mcp/src/features/file-content/model/temporaryFileLinkStore.js +307 -0
  54. package/dist/services/features/telegram-mcp/src/features/file-content/model/workspaceFilePolicy.js +115 -0
  55. package/dist/services/features/telegram-mcp/src/features/notify/model/notifyService.js +9 -5
  56. package/dist/services/features/telegram-mcp/src/features/session-context/model/getRuntimeDiagnosticsTool.js +30 -0
  57. package/dist/services/features/telegram-mcp/src/features/session-context/model/sessionContextService.js +169 -7
  58. package/dist/services/features/telegram-mcp/src/shared/integrations/memory/processLocalStateStore.js +260 -0
  59. package/dist/services/features/telegram-mcp/src/shared/integrations/object-storage/minioExchangeStore.js +1 -1
  60. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +4 -1
  61. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportConsoleRegistry.js +2 -2
  62. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportFileHandoffActions.js +6 -3
  63. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportMessageFlow.js +1 -1
  64. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportProjectState.js +2 -2
  65. package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/client.js +29 -6
  66. package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/ptyRegistry.js +100 -2
  67. package/dist/services/features/telegram-mcp/src/shared/lib/bodyLimits.js +63 -0
  68. package/dist/services/features/telegram-mcp/src/shared/lib/gatewayAuth.js +13 -0
  69. package/dist/services/features/telegram-mcp/src/shared/lib/gatewayScope.js +5 -5
  70. package/dist/services/features/telegram-mcp/src/shared/lib/project-identity/projectIdentity.js +10 -0
  71. package/dist/services/features/telegram-mcp/src/shared/lib/time/localTimestamp.js +21 -0
  72. package/docs/CHAT_CONNECTOR.md +134 -0
  73. package/docs/STANDALONE-ru.md +41 -3
  74. package/docs/STANDALONE.md +41 -3
  75. package/package.json +5 -3
  76. package/packages/chrome-attach-extension/dist/background.js +572 -163
  77. package/packages/chrome-attach-extension/dist/manifest.json +2 -1
  78. package/packages/chrome-attach-extension/dist/options.js +15 -2
  79. package/packages/chrome-attach-extension/dist/recorder-content.js +14 -1
  80. package/packages/chrome-attach-extension/dist/recorder-page.js +34 -18
  81. package/packages/firefox-attach-extension/dist/background.js +413 -33
  82. package/packages/firefox-attach-extension/dist/manifest.json +0 -12
  83. package/packages/firefox-attach-extension/dist/options.js +14 -1
  84. package/packages/firefox-attach-extension/dist/recorder-content.js +14 -1
  85. package/packages/firefox-attach-extension/dist/recorder-page.js +34 -18
  86. package/scripts/postinstall.js +33 -1
@@ -15,18 +15,38 @@ const POPUP_COMMAND_RESULT_KEY = "attach_popup_command_result";
15
15
  const CONTROL_PANEL_PORT_NAME = "telly-control-panel";
16
16
  const RECONNECT_DELAY_MS = 3000;
17
17
  const HEARTBEAT_INTERVAL_MS = 15000;
18
+ const KEEPALIVE_ALARM_NAME = "telly-keepalive";
19
+ const KEEPALIVE_ALARM_PERIOD_MINUTES = 0.5;
18
20
  const MAX_CAPTURE_BYTES = 512 * 1024;
21
+ const MAX_BUFFERED_LOG_ENTRIES = 200;
19
22
 
20
23
  let socket = null;
21
24
  let reconnectTimer = null;
22
25
  let heartbeatTimer = null;
23
26
  let instanceId = null;
27
+ let attachedTabId = null;
24
28
  let manualDisconnect = false;
25
29
  const pendingManualRecordingRequests = new Map();
26
30
  const controlPanelPorts = new Set();
27
31
 
28
32
  const activeRecordingsById = new Map();
29
33
  const activeRecordingIdByTabId = new Map();
34
+ const consoleMessagesByTabId = new Map();
35
+ const pageErrorsByTabId = new Map();
36
+ const networkFailuresByTabId = new Map();
37
+
38
+ function padNumber(value, length = 2) {
39
+ return String(value).padStart(length, "0");
40
+ }
41
+
42
+ function formatLocalTimestamp(date = new Date()) {
43
+ return `${date.getFullYear()}-${padNumber(date.getMonth() + 1)}-${padNumber(date.getDate())}T${padNumber(
44
+ date.getHours(),
45
+ )}:${padNumber(date.getMinutes())}:${padNumber(date.getSeconds())}.${padNumber(
46
+ date.getMilliseconds(),
47
+ 3,
48
+ )}`;
49
+ }
30
50
 
31
51
  function storageGet(defaults) {
32
52
  return new Promise((resolve, reject) => {
@@ -106,6 +126,19 @@ function windowsUpdate(windowId, updateInfo) {
106
126
  });
107
127
  }
108
128
 
129
+ function tabsRemove(tabId) {
130
+ return new Promise((resolve, reject) => {
131
+ browser.tabs.remove(tabId, () => {
132
+ const error = browser.runtime?.lastError;
133
+ if (error) {
134
+ reject(new Error(error.message));
135
+ return;
136
+ }
137
+ resolve();
138
+ });
139
+ });
140
+ }
141
+
109
142
  function cookiesGetAll(details) {
110
143
  return new Promise((resolve, reject) => {
111
144
  browser.cookies.getAll(details, (cookies) => {
@@ -225,6 +258,10 @@ async function setLocalInstanceId(value) {
225
258
  }
226
259
 
227
260
  async function setAttachedTab(tab) {
261
+ attachedTabId =
262
+ tab && Number.isInteger(tab.tab_id)
263
+ ? Number(tab.tab_id)
264
+ : null;
228
265
  await storageSet({
229
266
  [ATTACHED_TAB_KEY]: tab,
230
267
  });
@@ -236,6 +273,15 @@ async function setRecordingStatus(status) {
236
273
  });
237
274
  }
238
275
 
276
+ async function hydrateAttachedTabSelection() {
277
+ const stored = await storageGet({ [ATTACHED_TAB_KEY]: null });
278
+ const tab = stored[ATTACHED_TAB_KEY];
279
+ attachedTabId =
280
+ tab && Number.isInteger(tab.tab_id)
281
+ ? Number(tab.tab_id)
282
+ : null;
283
+ }
284
+
239
285
  function buildWebSocketUrl(settings) {
240
286
  return `ws://${settings.host}:${settings.port}/browser-attach/ws`;
241
287
  }
@@ -290,144 +336,421 @@ async function ensureTabIsActive(tabId) {
290
336
  return await tabsGet(tabId);
291
337
  }
292
338
 
293
- function buildTabActionCode(action, payload) {
294
- const serializedAction = JSON.stringify(action);
295
- const serializedPayload = JSON.stringify(payload || {});
296
-
297
- return `(() => {
298
- const action = ${serializedAction};
299
- const payload = ${serializedPayload};
300
- const normalize = (value) => typeof value === "string" ? value.trim() : "";
301
- const byText = (text, exact) => {
302
- const needle = normalize(text);
303
- if (!needle) return null;
304
- const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
305
- while (walker.nextNode()) {
306
- const element = walker.currentNode;
307
- const textContent = normalize(element.textContent || "");
308
- if (!textContent) continue;
309
- if ((exact && textContent === needle) || (!exact && textContent.includes(needle))) {
310
- return element;
311
- }
312
- }
313
- return null;
314
- };
315
- const resolveTarget = () => {
316
- const aiTag = normalize(payload.ai_tag);
317
- if (aiTag) {
318
- return document.querySelector('[data-drive-tag="' + aiTag.replace(/"/g, '\\"') + '"], [ai-tag="' + aiTag.replace(/"/g, '\\"') + '"]');
319
- }
320
- const selector = normalize(payload.selector);
321
- if (selector) {
322
- return document.querySelector(selector);
323
- }
324
- const text = normalize(payload.text);
325
- if (text) {
326
- return byText(text, payload.exact === true);
339
+ function trimBufferedEntries(entries) {
340
+ if (entries.length <= MAX_BUFFERED_LOG_ENTRIES) {
341
+ return entries;
342
+ }
343
+ return entries.slice(entries.length - MAX_BUFFERED_LOG_ENTRIES);
344
+ }
345
+
346
+ function appendBufferedEntry(store, tabId, entry) {
347
+ if (!Number.isInteger(tabId) || tabId < 0 || !entry || typeof entry !== "object") {
348
+ return;
349
+ }
350
+ const existing = store.get(tabId) || [];
351
+ existing.push(entry);
352
+ store.set(tabId, trimBufferedEntries(existing));
353
+ }
354
+
355
+ function extractErrorStack(args) {
356
+ if (!Array.isArray(args)) {
357
+ return "";
358
+ }
359
+ for (const arg of args) {
360
+ if (!arg || typeof arg !== "object") {
361
+ continue;
362
+ }
363
+ if (typeof arg.stack === "string" && arg.stack.trim()) {
364
+ return arg.stack;
365
+ }
366
+ }
367
+ return "";
368
+ }
369
+
370
+ function extractConsoleLocation(event) {
371
+ if (!event || typeof event !== "object" || !Array.isArray(event.args)) {
372
+ return "";
373
+ }
374
+ for (const arg of event.args) {
375
+ if (!arg || typeof arg !== "object") {
376
+ continue;
377
+ }
378
+ if (typeof arg.filename === "string" && arg.filename.trim()) {
379
+ const line = Number.isFinite(arg.lineno) ? Number(arg.lineno) : 0;
380
+ const column = Number.isFinite(arg.colno) ? Number(arg.colno) : 0;
381
+ return `${arg.filename}:${line}:${column}`;
382
+ }
383
+ }
384
+ return "";
385
+ }
386
+
387
+ function recordPageEventForBuffers(tabId, event) {
388
+ if (!Number.isInteger(tabId) || tabId < 0 || !event || typeof event !== "object") {
389
+ return;
390
+ }
391
+ if (event.kind !== "console_event") {
392
+ return;
393
+ }
394
+
395
+ const timestamp =
396
+ typeof event.at === "string" && event.at.trim()
397
+ ? event.at
398
+ : formatLocalTimestamp(new Date());
399
+ const text = typeof event.text === "string" ? event.text : "";
400
+ const location = extractConsoleLocation(event);
401
+
402
+ appendBufferedEntry(consoleMessagesByTabId, tabId, {
403
+ type:
404
+ typeof event.level === "string" && event.level.trim()
405
+ ? event.level
406
+ : "log",
407
+ text,
408
+ ...(location ? { location } : {}),
409
+ timestamp,
410
+ });
411
+
412
+ if (event.level === "error" || event.level === "assert") {
413
+ const stack = extractErrorStack(event.args);
414
+ appendBufferedEntry(pageErrorsByTabId, tabId, {
415
+ message: text || "Console error",
416
+ ...(stack ? { stack } : {}),
417
+ timestamp,
418
+ });
419
+ }
420
+ }
421
+
422
+ function recordNetworkFailure(tabId, event) {
423
+ if (!Number.isInteger(tabId) || tabId < 0 || !event || typeof event !== "object") {
424
+ return;
425
+ }
426
+ appendBufferedEntry(networkFailuresByTabId, tabId, {
427
+ url: typeof event.url === "string" ? event.url : "",
428
+ method:
429
+ typeof event.method === "string" && event.method.trim()
430
+ ? event.method
431
+ : "GET",
432
+ ...(typeof event.status === "number" ? { status: event.status } : {}),
433
+ ...(typeof event.error_text === "string" ? { error_text: event.error_text } : {}),
434
+ ...(typeof event.resource_type === "string"
435
+ ? { resource_type: event.resource_type }
436
+ : {}),
437
+ timestamp:
438
+ typeof event.timestamp === "string" && event.timestamp.trim()
439
+ ? event.timestamp
440
+ : formatLocalTimestamp(new Date()),
441
+ });
442
+ }
443
+
444
+ function getBufferedLogSnapshot(tabId, limit) {
445
+ const normalize = (value) =>
446
+ Number.isInteger(value) && value > 0 ? Number(value) : undefined;
447
+ const sliceEntries = (entries) => {
448
+ const normalizedLimit = normalize(limit);
449
+ return normalizedLimit ? entries.slice(-normalizedLimit) : entries;
450
+ };
451
+
452
+ const consoleMessages = consoleMessagesByTabId.get(tabId) || [];
453
+ const pageErrors = pageErrorsByTabId.get(tabId) || [];
454
+ const networkFailures = networkFailuresByTabId.get(tabId) || [];
455
+
456
+ return {
457
+ console_messages: sliceEntries(consoleMessages),
458
+ console_total: consoleMessages.length,
459
+ page_errors: sliceEntries(pageErrors),
460
+ page_error_total: pageErrors.length,
461
+ network_failures: sliceEntries(networkFailures),
462
+ network_failure_total: networkFailures.length,
463
+ };
464
+ }
465
+
466
+ function clearBufferedLogs(tabId) {
467
+ const consoleMessages = consoleMessagesByTabId.get(tabId) || [];
468
+ const pageErrors = pageErrorsByTabId.get(tabId) || [];
469
+ const networkFailures = networkFailuresByTabId.get(tabId) || [];
470
+ consoleMessagesByTabId.delete(tabId);
471
+ pageErrorsByTabId.delete(tabId);
472
+ networkFailuresByTabId.delete(tabId);
473
+ return {
474
+ console_messages_cleared: consoleMessages.length,
475
+ page_errors_cleared: pageErrors.length,
476
+ network_failures_cleared: networkFailures.length,
477
+ };
478
+ }
479
+
480
+ async function executeTabActionInPage(action, payload) {
481
+ payload = payload || {};
482
+ const normalize = (value) => typeof value === "string" ? value.trim() : "";
483
+ const timeoutMs = Number.isInteger(payload.timeout_ms) ? payload.timeout_ms : 30000;
484
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
485
+ const byText = (text, exact) => {
486
+ const needle = normalize(text);
487
+ if (!needle) return null;
488
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
489
+ while (walker.nextNode()) {
490
+ const element = walker.currentNode;
491
+ const textContent = normalize(element.textContent || "");
492
+ if (!textContent) continue;
493
+ if ((exact && textContent === needle) || (!exact && textContent.includes(needle))) {
494
+ return element;
327
495
  }
328
- return document.body;
329
- };
330
- const target = resolveTarget();
331
- if (action !== "screenshot" && action !== "inject_script" && !target) {
332
- return { ok: false, error: "Target element was not found." };
333
496
  }
334
- if (target && typeof target.scrollIntoView === "function") {
335
- target.scrollIntoView({ block: "center", inline: "center" });
497
+ return null;
498
+ };
499
+ const isVisible = (element) => {
500
+ if (!element) return false;
501
+ const computed = window.getComputedStyle(element);
502
+ return computed.display !== "none" && computed.visibility !== "hidden" && computed.opacity !== "0";
503
+ };
504
+ const resolveTarget = () => {
505
+ const aiTag = normalize(payload.ai_tag);
506
+ if (aiTag) {
507
+ return document.querySelector('[data-drive-tag="' + aiTag.replace(/"/g, '\\"') + '"], [ai-tag="' + aiTag.replace(/"/g, '\\"') + '"]');
336
508
  }
337
- const toVisible = (element) => {
338
- const computed = window.getComputedStyle(element);
339
- return computed.display !== "none" && computed.visibility !== "hidden" && computed.opacity !== "0";
340
- };
341
- if (action === "dom") {
342
- const attributes = target
343
- ? Object.fromEntries(Array.from(target.attributes || []).map((attr) => [attr.name, attr.value]))
344
- : {};
345
- return {
346
- ok: true,
347
- result: {
348
- found: Boolean(target),
349
- outer_html: payload.include_html === false ? undefined : target.outerHTML,
350
- text_content: payload.include_text === false ? undefined : (target.textContent || "").trim(),
351
- visible: target ? toVisible(target) : false,
352
- attributes,
353
- url: location.href,
354
- title: document.title,
355
- },
356
- };
509
+ const selector = normalize(payload.selector);
510
+ if (selector) {
511
+ return document.querySelector(selector);
357
512
  }
358
- if (action === "click") {
359
- target.click();
360
- return {
361
- ok: true,
362
- result: {
363
- url: location.href,
364
- title: document.title,
365
- },
366
- };
513
+ const text = normalize(payload.text);
514
+ if (text) {
515
+ return byText(text, payload.exact === true);
367
516
  }
368
- if (action === "fill") {
369
- target.focus();
370
- target.value = payload.value || "";
371
- target.dispatchEvent(new Event("input", { bubbles: true }));
372
- target.dispatchEvent(new Event("change", { bubbles: true }));
517
+ return document.body;
518
+ };
519
+ const target = resolveTarget();
520
+ if (!target) {
521
+ return { ok: false, error: "Target element was not found." };
522
+ }
523
+ if (target && typeof target.scrollIntoView === "function") {
524
+ target.scrollIntoView({ block: "center", inline: "center" });
525
+ }
526
+ if (action === "dom") {
527
+ const attributes = target
528
+ ? Object.fromEntries(Array.from(target.attributes || []).map((attr) => [attr.name, attr.value]))
529
+ : {};
530
+ return {
531
+ ok: true,
532
+ result: {
533
+ found: Boolean(target),
534
+ outer_html: payload.include_html === false ? undefined : target.outerHTML,
535
+ text_content: payload.include_text === false ? undefined : (target.textContent || "").trim(),
536
+ visible: target ? isVisible(target) : false,
537
+ attributes,
538
+ url: location.href,
539
+ title: document.title,
540
+ },
541
+ };
542
+ }
543
+ if (action === "computed_style") {
544
+ if (!target) {
373
545
  return {
374
546
  ok: true,
375
547
  result: {
548
+ found: false,
376
549
  url: location.href,
377
550
  title: document.title,
378
551
  },
379
552
  };
380
553
  }
381
- if (action === "inject_script") {
382
- const namespace = normalize(payload.namespace) || "TELLY";
383
- const source = String(payload.source || "");
384
- if (!source) {
385
- return { ok: false, error: "Script source is required." };
386
- }
387
- const wrapped = "const __tellyNamespace=" + JSON.stringify(namespace)
388
- + ";window[__tellyNamespace]=window[__tellyNamespace]||{};var TELLY=window[__tellyNamespace];const __tellyBeforeKeys=new Set(Object.getOwnPropertyNames(window));\\n"
389
- + source
390
- + "\\nfor(const __tellyKey of Object.getOwnPropertyNames(window)){if(__tellyBeforeKeys.has(__tellyKey)){continue;}if(__tellyKey===__tellyNamespace){continue;}try{window[__tellyNamespace][__tellyKey]=window[__tellyKey];}catch{}}";
391
- const script = document.createElement("script");
392
- script.textContent = wrapped;
393
- (document.documentElement || document.head || document.body).appendChild(script);
394
- script.remove();
395
- return {
396
- ok: true,
397
- result: {
398
- namespace,
399
- bytes: source.length,
400
- url: location.href,
401
- title: document.title,
554
+ const requestedProperties = Array.isArray(payload.properties) && payload.properties.length
555
+ ? payload.properties.map((item) => String(item))
556
+ : ["display","position","visibility","opacity","color","background-color","font-size","z-index","overflow"];
557
+ const computed = window.getComputedStyle(target);
558
+ const rect = target.getBoundingClientRect();
559
+ const styles = Object.fromEntries(
560
+ requestedProperties.map((property) => [property, computed.getPropertyValue(property)]),
561
+ );
562
+ return {
563
+ ok: true,
564
+ result: {
565
+ found: true,
566
+ visible: isVisible(target),
567
+ styles,
568
+ box: {
569
+ x: rect.x,
570
+ y: rect.y,
571
+ width: rect.width,
572
+ height: rect.height,
402
573
  },
403
- };
574
+ url: location.href,
575
+ title: document.title,
576
+ },
577
+ };
578
+ }
579
+ if (action === "click") {
580
+ target.click();
581
+ return {
582
+ ok: true,
583
+ result: {
584
+ url: location.href,
585
+ title: document.title,
586
+ },
587
+ };
588
+ }
589
+ if (action === "wait_for") {
590
+ const waitState = normalize(payload.state) || "visible";
591
+ const startedAt = Date.now();
592
+ while (Date.now() - startedAt <= timeoutMs) {
593
+ const current = resolveTarget();
594
+ const visible = current ? isVisible(current) : false;
595
+ if (
596
+ (waitState === "attached" && current) ||
597
+ (waitState === "detached" && !current) ||
598
+ (waitState === "visible" && current && visible) ||
599
+ (waitState === "hidden" && (!current || !visible))
600
+ ) {
601
+ return {
602
+ ok: true,
603
+ result: {
604
+ url: location.href,
605
+ title: document.title,
606
+ },
607
+ };
608
+ }
609
+ await sleep(100);
404
610
  }
405
- if (action === "press") {
406
- const key = String(payload.key || "");
407
- if (target && typeof target.focus === "function") {
408
- target.focus();
611
+ return {
612
+ ok: false,
613
+ error: "Timed out waiting for the requested element state.",
614
+ };
615
+ }
616
+ if (action === "wait_for_url") {
617
+ const exactUrl = normalize(payload.url);
618
+ const containsUrl = normalize(payload.url_contains);
619
+ const startedAt = Date.now();
620
+ while (Date.now() - startedAt <= timeoutMs) {
621
+ const currentUrl = String(location.href || "");
622
+ if ((exactUrl && currentUrl === exactUrl) || (containsUrl && currentUrl.includes(containsUrl))) {
623
+ return {
624
+ ok: true,
625
+ result: {
626
+ url: currentUrl,
627
+ title: document.title,
628
+ },
629
+ };
409
630
  }
410
- const eventInit = { key, bubbles: true, cancelable: true };
411
- const keyboardTarget = document.activeElement || target || document.body;
412
- keyboardTarget.dispatchEvent(new KeyboardEvent("keydown", eventInit));
413
- keyboardTarget.dispatchEvent(new KeyboardEvent("keypress", eventInit));
414
- keyboardTarget.dispatchEvent(new KeyboardEvent("keyup", eventInit));
415
- return {
416
- ok: true,
417
- result: {
418
- url: location.href,
419
- title: document.title,
420
- },
421
- };
631
+ await sleep(100);
632
+ }
633
+ return {
634
+ ok: false,
635
+ error: "Timed out waiting for the requested URL.",
636
+ };
637
+ }
638
+ if (action === "reload") {
639
+ location.reload();
640
+ return {
641
+ ok: true,
642
+ result: {
643
+ url: location.href,
644
+ title: document.title,
645
+ },
646
+ };
647
+ }
648
+ if (action === "fill") {
649
+ target.focus();
650
+ target.value = payload.value || "";
651
+ target.dispatchEvent(new Event("input", { bubbles: true }));
652
+ target.dispatchEvent(new Event("change", { bubbles: true }));
653
+ return {
654
+ ok: true,
655
+ result: {
656
+ url: location.href,
657
+ title: document.title,
658
+ },
659
+ };
660
+ }
661
+ if (action === "press") {
662
+ const key = String(payload.key || "");
663
+ if (target && typeof target.focus === "function") {
664
+ target.focus();
422
665
  }
423
- return { ok: false, error: "Unsupported tab action." };
424
- })();`;
666
+ const eventInit = { key, bubbles: true, cancelable: true };
667
+ const keyboardTarget = document.activeElement || target || document.body;
668
+ keyboardTarget.dispatchEvent(new KeyboardEvent("keydown", eventInit));
669
+ keyboardTarget.dispatchEvent(new KeyboardEvent("keypress", eventInit));
670
+ keyboardTarget.dispatchEvent(new KeyboardEvent("keyup", eventInit));
671
+ return {
672
+ ok: true,
673
+ result: {
674
+ url: location.href,
675
+ title: document.title,
676
+ },
677
+ };
678
+ }
679
+ return { ok: false, error: "Unsupported tab action." };
425
680
  }
426
681
 
427
682
  async function runTabAction(tabId, action, payload) {
428
683
  const activeTab = await ensureTabIsActive(tabId);
429
684
 
685
+ if (action === "attach") {
686
+ const record = {
687
+ tab_id: activeTab.id,
688
+ window_id: activeTab.windowId,
689
+ active: activeTab.active === true,
690
+ title: activeTab.title || "",
691
+ url: activeTab.url || "",
692
+ status: activeTab.status || "",
693
+ };
694
+ await setAttachedTab(record);
695
+ try {
696
+ await injectRecorderContent(tabId);
697
+ } catch {
698
+ // ignore
699
+ }
700
+ return {
701
+ ok: true,
702
+ result: {
703
+ url: record.url,
704
+ title: record.title,
705
+ },
706
+ };
707
+ }
708
+
709
+ if (action === "detach") {
710
+ await setAttachedTab(null);
711
+ return {
712
+ ok: true,
713
+ result: {
714
+ url: activeTab.url || "",
715
+ title: activeTab.title || "",
716
+ },
717
+ };
718
+ }
719
+
720
+ if (action === "close") {
721
+ if (attachedTabId === tabId) {
722
+ await setAttachedTab(null);
723
+ }
724
+ await tabsRemove(tabId);
725
+ return {
726
+ ok: true,
727
+ result: {
728
+ url: activeTab.url || "",
729
+ title: activeTab.title || "",
730
+ },
731
+ };
732
+ }
733
+
734
+ if (action === "get_logs") {
735
+ return {
736
+ ok: true,
737
+ result: getBufferedLogSnapshot(
738
+ tabId,
739
+ Number.isInteger(payload?.limit) ? Number(payload.limit) : undefined,
740
+ ),
741
+ };
742
+ }
743
+
744
+ if (action === "clear_logs") {
745
+ return {
746
+ ok: true,
747
+ result: clearBufferedLogs(tabId),
748
+ };
749
+ }
750
+
430
751
  if (action === "screenshot") {
752
+ await new Promise((resolve) => setTimeout(resolve, 180));
753
+ const refreshedTab = await tabsGet(tabId).catch(() => activeTab);
431
754
  const dataUrl = await captureVisibleTab(activeTab.windowId, {
432
755
  format: "png",
433
756
  });
@@ -435,8 +758,8 @@ async function runTabAction(tabId, action, payload) {
435
758
  ok: true,
436
759
  result: {
437
760
  png_base64: String(dataUrl).replace(/^data:image\/png;base64,/, ""),
438
- url: activeTab.url || "",
439
- title: activeTab.title || "",
761
+ url: refreshedTab.url || "",
762
+ title: refreshedTab.title || "",
440
763
  },
441
764
  };
442
765
  }
@@ -474,7 +797,8 @@ async function runTabAction(tabId, action, payload) {
474
797
 
475
798
  const execution = await scriptingExecuteScript({
476
799
  target: { tabId },
477
- func: new Function(`return ${buildTabActionCode(action, payload)}`),
800
+ func: executeTabActionInPage,
801
+ args: [action, payload || {}],
478
802
  });
479
803
  const result = execution[0]?.result;
480
804
 
@@ -495,6 +819,14 @@ function sendJson(payload) {
495
819
  socket.send(JSON.stringify(payload));
496
820
  }
497
821
 
822
+ function isSocketOpenOrConnecting() {
823
+ return Boolean(
824
+ socket &&
825
+ (socket.readyState === WebSocket.OPEN ||
826
+ socket.readyState === WebSocket.CONNECTING),
827
+ );
828
+ }
829
+
498
830
  function clearTimers() {
499
831
  if (reconnectTimer) {
500
832
  clearTimeout(reconnectTimer);
@@ -534,7 +866,7 @@ function startHeartbeat() {
534
866
  heartbeatTimer = setInterval(() => {
535
867
  sendJson({
536
868
  type: "heartbeat",
537
- sent_at: new Date().toISOString(),
869
+ sent_at: formatLocalTimestamp(new Date()),
538
870
  });
539
871
  }, HEARTBEAT_INTERVAL_MS);
540
872
  }
@@ -581,7 +913,7 @@ function sendRecordingEvent(recordingId, tabId, event) {
581
913
  tab_id: tabId,
582
914
  event: {
583
915
  ...event,
584
- at: event.at || new Date().toISOString(),
916
+ at: event.at || formatLocalTimestamp(new Date()),
585
917
  },
586
918
  });
587
919
  }
@@ -593,19 +925,31 @@ async function injectRecorderContent(tabId) {
593
925
  });
594
926
  }
595
927
 
928
+ function capturePageSnapshotInTab(recordingReason) {
929
+ const now = new Date();
930
+ const pad = (value, length = 2) => String(value).padStart(length, "0");
931
+ const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(
932
+ now.getHours(),
933
+ )}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(
934
+ now.getMilliseconds(),
935
+ 3,
936
+ )}`;
937
+ return {
938
+ kind: "page_snapshot",
939
+ source: "background",
940
+ reason: recordingReason,
941
+ at: timestamp,
942
+ url: location.href,
943
+ title: document.title,
944
+ ready_state: document.readyState,
945
+ html: document.documentElement ? document.documentElement.outerHTML : "",
946
+ };
947
+ }
948
+
596
949
  async function captureTabSnapshot(tabId, reason) {
597
950
  const execution = await scriptingExecuteScript({
598
951
  target: { tabId },
599
- func: (recordingReason) => ({
600
- kind: "page_snapshot",
601
- source: "background",
602
- reason: recordingReason,
603
- at: new Date().toISOString(),
604
- url: location.href,
605
- title: document.title,
606
- ready_state: document.readyState,
607
- html: document.documentElement ? document.documentElement.outerHTML : ""
608
- }),
952
+ func: capturePageSnapshotInTab,
609
953
  args: [reason],
610
954
  });
611
955
  const result = execution[0]?.result;
@@ -656,7 +1000,7 @@ async function startRecording(message) {
656
1000
  tabId,
657
1001
  tabTitle: browserTab.title || "",
658
1002
  tabUrl: browserTab.url || "",
659
- startedAt: new Date().toISOString(),
1003
+ startedAt: formatLocalTimestamp(new Date()),
660
1004
  });
661
1005
  activeRecordingIdByTabId.set(tabId, message.recording_id);
662
1006
 
@@ -730,6 +1074,11 @@ async function handleAttachTabSelectedCommand(tabId) {
730
1074
  tab: record,
731
1075
  });
732
1076
  await setAttachedTab(record);
1077
+ try {
1078
+ await injectRecorderContent(tabId);
1079
+ } catch {
1080
+ // ignore recorder bootstrap errors for manual attach
1081
+ }
733
1082
 
734
1083
  return {
735
1084
  ok: true,
@@ -831,6 +1180,29 @@ async function sendManualRecordingRequest(type, payload = {}) {
831
1180
  return result;
832
1181
  }
833
1182
 
1183
+ async function handleTabActionMessage(message) {
1184
+ let result;
1185
+ try {
1186
+ result = await runTabAction(
1187
+ message.tab_id,
1188
+ message.action,
1189
+ message.payload || {},
1190
+ );
1191
+ } catch (error) {
1192
+ result = {
1193
+ ok: false,
1194
+ error: error instanceof Error ? error.message : String(error),
1195
+ };
1196
+ }
1197
+ sendJson({
1198
+ type: "tab_action_result",
1199
+ request_id: message.request_id,
1200
+ ok: result.ok === true,
1201
+ ...(result.result ? { result: result.result } : {}),
1202
+ ...(result.error ? { error: result.error } : {}),
1203
+ });
1204
+ }
1205
+
834
1206
  async function handleMessage(rawData) {
835
1207
  const message =
836
1208
  typeof rawData === "string"
@@ -865,18 +1237,7 @@ async function handleMessage(rawData) {
865
1237
  return;
866
1238
  }
867
1239
  case "tab_action": {
868
- const result = await runTabAction(
869
- message.tab_id,
870
- message.action,
871
- message.payload || {},
872
- );
873
- sendJson({
874
- type: "tab_action_result",
875
- request_id: message.request_id,
876
- ok: result.ok === true,
877
- ...(result.result ? { result: result.result } : {}),
878
- ...(result.error ? { error: result.error } : {}),
879
- });
1240
+ await handleTabActionMessage(message);
880
1241
  return;
881
1242
  }
882
1243
  case "recording_start": {
@@ -933,6 +1294,9 @@ async function connect() {
933
1294
  });
934
1295
  return;
935
1296
  }
1297
+ if (isSocketOpenOrConnecting()) {
1298
+ return;
1299
+ }
936
1300
  clearTimers();
937
1301
  await setConnectionStatus({
938
1302
  state: "connecting",
@@ -1015,7 +1379,7 @@ browser.storage.onChanged.addListener((changes, areaName) => {
1015
1379
  [POPUP_COMMAND_RESULT_KEY]: {
1016
1380
  command_id: command.command_id,
1017
1381
  result,
1018
- at: new Date().toISOString(),
1382
+ at: formatLocalTimestamp(new Date()),
1019
1383
  },
1020
1384
  }),
1021
1385
  )
@@ -1027,7 +1391,7 @@ browser.storage.onChanged.addListener((changes, areaName) => {
1027
1391
  ok: false,
1028
1392
  error: error instanceof Error ? error.message : String(error),
1029
1393
  },
1030
- at: new Date().toISOString(),
1394
+ at: formatLocalTimestamp(new Date()),
1031
1395
  },
1032
1396
  }),
1033
1397
  );
@@ -1095,34 +1459,41 @@ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
1095
1459
  });
1096
1460
 
1097
1461
  const recording = getRecordingByTabId(tabId);
1098
- if (!recording) {
1462
+ const shouldTrackTab = recording || attachedTabId === tabId;
1463
+ if (!shouldTrackTab) {
1099
1464
  return;
1100
1465
  }
1101
1466
 
1102
- sendRecordingEvent(recording.recordingId, tabId, {
1103
- kind: "navigation",
1104
- source: "browser",
1105
- status: changeInfo.status || tab.status || "",
1106
- url: tab.url || "",
1107
- title: tab.title || "",
1108
- });
1467
+ if (recording) {
1468
+ sendRecordingEvent(recording.recordingId, tabId, {
1469
+ kind: "navigation",
1470
+ source: "browser",
1471
+ status: changeInfo.status || tab.status || "",
1472
+ url: tab.url || "",
1473
+ title: tab.title || "",
1474
+ });
1475
+ }
1109
1476
 
1110
1477
  if (changeInfo.status === "complete") {
1111
1478
  try {
1112
1479
  await injectRecorderContent(tabId);
1113
- const snapshot = await captureTabSnapshot(tabId, "tab-updated-complete");
1114
- if (snapshot) {
1115
- sendRecordingEvent(recording.recordingId, tabId, snapshot);
1480
+ if (recording) {
1481
+ const snapshot = await captureTabSnapshot(tabId, "tab-updated-complete");
1482
+ if (snapshot) {
1483
+ sendRecordingEvent(recording.recordingId, tabId, snapshot);
1484
+ }
1116
1485
  }
1117
1486
  } catch {
1118
1487
  // ignore
1119
1488
  }
1120
- await emitCookiesSnapshot(
1121
- recording.recordingId,
1122
- tabId,
1123
- tab.url || "",
1124
- tab.title || "",
1125
- );
1489
+ if (recording) {
1490
+ await emitCookiesSnapshot(
1491
+ recording.recordingId,
1492
+ tabId,
1493
+ tab.url || "",
1494
+ tab.title || "",
1495
+ );
1496
+ }
1126
1497
  }
1127
1498
  });
1128
1499
 
@@ -1170,9 +1541,10 @@ async function handleRuntimeMessage(message, sender) {
1170
1541
  if (!Number.isInteger(tabId)) {
1171
1542
  return undefined;
1172
1543
  }
1544
+ recordPageEventForBuffers(tabId, message.event || {});
1173
1545
  const recording = getRecordingByTabId(tabId);
1174
1546
  if (!recording) {
1175
- return undefined;
1547
+ return { ok: true };
1176
1548
  }
1177
1549
  sendRecordingEvent(recording.recordingId, tabId, message.event || {});
1178
1550
  return { ok: true };
@@ -1286,6 +1658,20 @@ browser.webRequest.onHeadersReceived.addListener(
1286
1658
 
1287
1659
  browser.webRequest.onCompleted.addListener(
1288
1660
  (details) => {
1661
+ if (
1662
+ Number.isInteger(details.tabId) &&
1663
+ details.tabId >= 0 &&
1664
+ typeof details.statusCode === "number" &&
1665
+ details.statusCode >= 400
1666
+ ) {
1667
+ recordNetworkFailure(details.tabId, {
1668
+ url: details.url,
1669
+ method: details.method,
1670
+ status: details.statusCode,
1671
+ resource_type: details.type,
1672
+ timestamp: formatLocalTimestamp(new Date()),
1673
+ });
1674
+ }
1289
1675
  const recording = getRecordingByTabId(details.tabId);
1290
1676
  if (!recording) {
1291
1677
  return;
@@ -1305,6 +1691,15 @@ browser.webRequest.onCompleted.addListener(
1305
1691
 
1306
1692
  browser.webRequest.onErrorOccurred.addListener(
1307
1693
  (details) => {
1694
+ if (Number.isInteger(details.tabId) && details.tabId >= 0) {
1695
+ recordNetworkFailure(details.tabId, {
1696
+ url: details.url,
1697
+ method: details.method,
1698
+ error_text: details.error,
1699
+ resource_type: details.type,
1700
+ timestamp: formatLocalTimestamp(new Date()),
1701
+ });
1702
+ }
1308
1703
  const recording = getRecordingByTabId(details.tabId);
1309
1704
  if (!recording) {
1310
1705
  return;
@@ -1322,5 +1717,19 @@ browser.webRequest.onErrorOccurred.addListener(
1322
1717
  { urls: ["<all_urls>"] },
1323
1718
  );
1324
1719
 
1720
+ browser.alarms.onAlarm.addListener((alarm) => {
1721
+ if (alarm.name !== KEEPALIVE_ALARM_NAME || manualDisconnect) {
1722
+ return;
1723
+ }
1724
+ if (!isSocketOpenOrConnecting()) {
1725
+ void connect();
1726
+ }
1727
+ });
1728
+
1729
+ browser.alarms.create(KEEPALIVE_ALARM_NAME, {
1730
+ periodInMinutes: KEEPALIVE_ALARM_PERIOD_MINUTES,
1731
+ });
1732
+
1733
+ void hydrateAttachedTabSelection();
1325
1734
  void computeInstanceId();
1326
1735
  void connect();