@leg3ndy/otto-bridge 0.8.1 → 0.8.2

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.
package/dist/runtime.js CHANGED
@@ -4,6 +4,7 @@ import { MockJobExecutor } from "./executors/mock.js";
4
4
  import { NativeMacOSJobExecutor } from "./executors/native_macos.js";
5
5
  import { JobCancelledError } from "./executors/shared.js";
6
6
  import { isManagedBridgeExtensionSlug, loadManagedBridgeExtensionState, } from "./extensions.js";
7
+ import { LocalAutomationRuntime } from "./local_automations.js";
7
8
  function delay(ms) {
8
9
  return new Promise((resolve) => setTimeout(resolve, ms));
9
10
  }
@@ -72,6 +73,7 @@ export class BridgeRuntime {
72
73
  config;
73
74
  reconnectDelayMs = DEFAULT_RECONNECT_BASE_DELAY_MS;
74
75
  executor;
76
+ localAutomationRuntime;
75
77
  lastBridgeReleaseNoticeKey = null;
76
78
  activeSocket = null;
77
79
  stopped = false;
@@ -81,6 +83,7 @@ export class BridgeRuntime {
81
83
  constructor(config, executor) {
82
84
  this.config = config;
83
85
  this.executor = executor ?? this.createDefaultExecutor(config);
86
+ this.localAutomationRuntime = new LocalAutomationRuntime(config);
84
87
  }
85
88
  async buildHelloMetadata() {
86
89
  const metadata = {
@@ -144,6 +147,10 @@ export class BridgeRuntime {
144
147
  if (typeof this.executor.start === "function") {
145
148
  await this.executor.start();
146
149
  }
150
+ await this.localAutomationRuntime.start().catch((error) => {
151
+ const detail = error instanceof Error ? error.message : String(error);
152
+ console.error(`[otto-bridge] local automation runtime failed to start: ${detail}`);
153
+ });
147
154
  }
148
155
  console.log(`[otto-bridge] runtime start device=${this.config.deviceId}`);
149
156
  while (!this.stopped) {
@@ -179,6 +186,7 @@ export class BridgeRuntime {
179
186
  this.activeCancels.delete(jobId);
180
187
  }
181
188
  }
189
+ await this.localAutomationRuntime.stop().catch(() => undefined);
182
190
  try {
183
191
  this.activeSocket?.close();
184
192
  }
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const BRIDGE_CONFIG_VERSION = 1;
2
- export const BRIDGE_VERSION = "0.8.1";
2
+ export const BRIDGE_VERSION = "0.8.2";
3
3
  export const BRIDGE_PACKAGE_NAME = "@leg3ndy/otto-bridge";
4
4
  export const DEFAULT_API_BASE_URL = "http://localhost:8000";
5
5
  export const DEFAULT_POLL_INTERVAL_MS = 3000;
@@ -693,6 +693,113 @@ export class WhatsAppBackgroundBrowser {
693
693
  : "(sem mensagens visiveis na conversa)",
694
694
  };
695
695
  }
696
+ async scanInboxConversations(limit, options) {
697
+ if (this.helperRuntime) {
698
+ return await this.helperRuntime.scanInboxConversations(limit, options);
699
+ }
700
+ await this.ensureReady();
701
+ const unreadOnly = options?.unreadOnly === true;
702
+ const result = await this.withPage((page) => page.evaluate((input) => {
703
+ const payload = input && typeof input === "object"
704
+ ? input
705
+ : {};
706
+ const maxItems = Math.max(1, Number(payload.limit || 10));
707
+ const unreadOnly = payload.unreadOnly === true;
708
+ const normalize = (value) => String(value || "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim();
709
+ function isVisible(element) {
710
+ if (!(element instanceof HTMLElement))
711
+ return false;
712
+ const rect = element.getBoundingClientRect();
713
+ if (rect.width < 6 || rect.height < 6)
714
+ return false;
715
+ const style = window.getComputedStyle(element);
716
+ if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0)
717
+ return false;
718
+ return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
719
+ }
720
+ const uniqueContainers = new Set();
721
+ const containers = Array.from(document.querySelectorAll('#pane-side [role="listitem"], #pane-side [role="gridcell"], #pane-side [data-testid="cell-frame-container"], #pane-side div[tabindex]'))
722
+ .filter((node) => node instanceof HTMLElement)
723
+ .filter((node) => {
724
+ if (!isVisible(node) || uniqueContainers.has(node)) {
725
+ return false;
726
+ }
727
+ uniqueContainers.add(node);
728
+ return true;
729
+ });
730
+ const conversations = containers.map((container) => {
731
+ const titleCandidates = Array.from(container.querySelectorAll('span[title], div[title]'))
732
+ .filter((node) => node instanceof HTMLElement)
733
+ .filter((node) => isVisible(node))
734
+ .map((node) => {
735
+ const text = String(node.getAttribute("title") || node.textContent || "").trim();
736
+ const score = text ? (normalize(text).length >= 2 ? 100 : 0) : 0;
737
+ return { text, score };
738
+ })
739
+ .filter((item) => item.score > 0)
740
+ .sort((left, right) => right.score - left.score);
741
+ const contact = String(titleCandidates[0]?.text || "").trim();
742
+ if (!contact) {
743
+ return null;
744
+ }
745
+ const lines = String(container.innerText || "")
746
+ .split(/\n+/)
747
+ .map((item) => item.trim())
748
+ .filter(Boolean);
749
+ const preview = lines.find((line) => normalize(line) !== normalize(contact) && !/^\d+$/.test(line)) || "";
750
+ const unreadSources = [
751
+ ...Array.from(container.querySelectorAll('[aria-label*="unread"], [aria-label*="Unread"], [aria-label*="não l"], [data-testid*="unread"], [data-icon*="unread"]')),
752
+ container,
753
+ ];
754
+ let unreadCount = 0;
755
+ for (const source of unreadSources) {
756
+ if (!(source instanceof HTMLElement)) {
757
+ continue;
758
+ }
759
+ const label = `${source.getAttribute("aria-label") || ""} ${source.innerText || ""}`.trim();
760
+ const lower = normalize(label);
761
+ if (!lower || (!lower.includes("unread") && !lower.includes("nao l") && !lower.includes("não l") && !/^\d+$/.test(lower))) {
762
+ continue;
763
+ }
764
+ const match = label.match(/(\d{1,3})/);
765
+ if (match) {
766
+ unreadCount = Math.max(unreadCount, Number(match[1] || 0));
767
+ }
768
+ else if (lower.includes("unread") || lower.includes("nao l") || lower.includes("não l")) {
769
+ unreadCount = Math.max(unreadCount, 1);
770
+ }
771
+ }
772
+ return {
773
+ contact,
774
+ preview: String(preview || "").slice(0, 240),
775
+ unreadCount,
776
+ };
777
+ }).filter((item) => item !== null);
778
+ const deduped = new Map();
779
+ for (const item of conversations) {
780
+ const key = normalize(item.contact);
781
+ const current = deduped.get(key);
782
+ if (!current || item.unreadCount > current.unreadCount || item.preview.length > current.preview.length) {
783
+ deduped.set(key, item);
784
+ }
785
+ }
786
+ const values = Array.from(deduped.values())
787
+ .filter((item) => !unreadOnly || item.unreadCount > 0)
788
+ .sort((left, right) => right.unreadCount - left.unreadCount || left.contact.localeCompare(right.contact))
789
+ .slice(0, maxItems);
790
+ return { conversations: values };
791
+ }, { limit, unreadOnly }));
792
+ const rawConversations = Array.isArray(result.conversations)
793
+ ? result.conversations
794
+ : [];
795
+ return rawConversations
796
+ .map((item) => ({
797
+ contact: clipText(String(item.contact || ""), 120),
798
+ preview: clipText(String(item.preview || ""), 240),
799
+ unreadCount: Math.max(0, Number(item.unreadCount || 0)),
800
+ }))
801
+ .filter((item) => item.contact);
802
+ }
696
803
  async verifyLastMessage(expectedText, previousMessages) {
697
804
  if (this.helperRuntime) {
698
805
  return await this.helperRuntime.verifyLastMessage(expectedText, previousMessages);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leg3ndy/otto-bridge",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local companion for Otto Bridge device pairing and WebSocket runtime.",