@spinabot/brigade 1.7.0 → 1.9.0

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 (257) hide show
  1. package/README.md +19 -0
  2. package/dist/agents/agent-loop.d.ts +13 -0
  3. package/dist/agents/agent-loop.d.ts.map +1 -1
  4. package/dist/agents/agent-loop.js +5 -0
  5. package/dist/agents/agent-loop.js.map +1 -1
  6. package/dist/agents/channels/access-control/group-tool-policy.d.ts +73 -0
  7. package/dist/agents/channels/access-control/group-tool-policy.d.ts.map +1 -0
  8. package/dist/agents/channels/access-control/group-tool-policy.js +193 -0
  9. package/dist/agents/channels/access-control/group-tool-policy.js.map +1 -0
  10. package/dist/agents/channels/access-control/index.d.ts +1 -0
  11. package/dist/agents/channels/access-control/index.d.ts.map +1 -1
  12. package/dist/agents/channels/access-control/index.js +1 -0
  13. package/dist/agents/channels/access-control/index.js.map +1 -1
  14. package/dist/agents/channels/bluebubbles/account-config.d.ts +253 -0
  15. package/dist/agents/channels/bluebubbles/account-config.d.ts.map +1 -0
  16. package/dist/agents/channels/bluebubbles/account-config.js +486 -0
  17. package/dist/agents/channels/bluebubbles/account-config.js.map +1 -0
  18. package/dist/agents/channels/bluebubbles/account-registry.d.ts +33 -0
  19. package/dist/agents/channels/bluebubbles/account-registry.d.ts.map +1 -0
  20. package/dist/agents/channels/bluebubbles/account-registry.js +46 -0
  21. package/dist/agents/channels/bluebubbles/account-registry.js.map +1 -0
  22. package/dist/agents/channels/bluebubbles/adapter.d.ts +45 -0
  23. package/dist/agents/channels/bluebubbles/adapter.d.ts.map +1 -0
  24. package/dist/agents/channels/bluebubbles/adapter.js +366 -0
  25. package/dist/agents/channels/bluebubbles/adapter.js.map +1 -0
  26. package/dist/agents/channels/bluebubbles/catchup-cursor.d.ts +52 -0
  27. package/dist/agents/channels/bluebubbles/catchup-cursor.d.ts.map +1 -0
  28. package/dist/agents/channels/bluebubbles/catchup-cursor.js +118 -0
  29. package/dist/agents/channels/bluebubbles/catchup-cursor.js.map +1 -0
  30. package/dist/agents/channels/bluebubbles/catchup.d.ts +114 -0
  31. package/dist/agents/channels/bluebubbles/catchup.d.ts.map +1 -0
  32. package/dist/agents/channels/bluebubbles/catchup.js +220 -0
  33. package/dist/agents/channels/bluebubbles/catchup.js.map +1 -0
  34. package/dist/agents/channels/bluebubbles/chat.d.ts +75 -0
  35. package/dist/agents/channels/bluebubbles/chat.d.ts.map +1 -0
  36. package/dist/agents/channels/bluebubbles/chat.js +182 -0
  37. package/dist/agents/channels/bluebubbles/chat.js.map +1 -0
  38. package/dist/agents/channels/bluebubbles/connection.d.ts +95 -0
  39. package/dist/agents/channels/bluebubbles/connection.d.ts.map +1 -0
  40. package/dist/agents/channels/bluebubbles/connection.js +413 -0
  41. package/dist/agents/channels/bluebubbles/connection.js.map +1 -0
  42. package/dist/agents/channels/bluebubbles/contact-names.d.ts +79 -0
  43. package/dist/agents/channels/bluebubbles/contact-names.d.ts.map +1 -0
  44. package/dist/agents/channels/bluebubbles/contact-names.js +188 -0
  45. package/dist/agents/channels/bluebubbles/contact-names.js.map +1 -0
  46. package/dist/agents/channels/bluebubbles/debounce.d.ts +78 -0
  47. package/dist/agents/channels/bluebubbles/debounce.d.ts.map +1 -0
  48. package/dist/agents/channels/bluebubbles/debounce.js +173 -0
  49. package/dist/agents/channels/bluebubbles/debounce.js.map +1 -0
  50. package/dist/agents/channels/bluebubbles/dedupe.d.ts +25 -0
  51. package/dist/agents/channels/bluebubbles/dedupe.d.ts.map +1 -0
  52. package/dist/agents/channels/bluebubbles/dedupe.js +35 -0
  53. package/dist/agents/channels/bluebubbles/dedupe.js.map +1 -0
  54. package/dist/agents/channels/bluebubbles/effects.d.ts +17 -0
  55. package/dist/agents/channels/bluebubbles/effects.d.ts.map +1 -0
  56. package/dist/agents/channels/bluebubbles/effects.js +57 -0
  57. package/dist/agents/channels/bluebubbles/effects.js.map +1 -0
  58. package/dist/agents/channels/bluebubbles/history.d.ts +56 -0
  59. package/dist/agents/channels/bluebubbles/history.d.ts.map +1 -0
  60. package/dist/agents/channels/bluebubbles/history.js +140 -0
  61. package/dist/agents/channels/bluebubbles/history.js.map +1 -0
  62. package/dist/agents/channels/bluebubbles/index.d.ts +18 -0
  63. package/dist/agents/channels/bluebubbles/index.d.ts.map +1 -0
  64. package/dist/agents/channels/bluebubbles/index.js +18 -0
  65. package/dist/agents/channels/bluebubbles/index.js.map +1 -0
  66. package/dist/agents/channels/bluebubbles/media.d.ts +104 -0
  67. package/dist/agents/channels/bluebubbles/media.d.ts.map +1 -0
  68. package/dist/agents/channels/bluebubbles/media.js +222 -0
  69. package/dist/agents/channels/bluebubbles/media.js.map +1 -0
  70. package/dist/agents/channels/bluebubbles/messaging.d.ts +13 -0
  71. package/dist/agents/channels/bluebubbles/messaging.d.ts.map +1 -0
  72. package/dist/agents/channels/bluebubbles/messaging.js +36 -0
  73. package/dist/agents/channels/bluebubbles/messaging.js.map +1 -0
  74. package/dist/agents/channels/bluebubbles/module.d.ts +24 -0
  75. package/dist/agents/channels/bluebubbles/module.d.ts.map +1 -0
  76. package/dist/agents/channels/bluebubbles/module.js +52 -0
  77. package/dist/agents/channels/bluebubbles/module.js.map +1 -0
  78. package/dist/agents/channels/bluebubbles/normalize.d.ts +111 -0
  79. package/dist/agents/channels/bluebubbles/normalize.d.ts.map +1 -0
  80. package/dist/agents/channels/bluebubbles/normalize.js +239 -0
  81. package/dist/agents/channels/bluebubbles/normalize.js.map +1 -0
  82. package/dist/agents/channels/bluebubbles/plugin.d.ts +46 -0
  83. package/dist/agents/channels/bluebubbles/plugin.d.ts.map +1 -0
  84. package/dist/agents/channels/bluebubbles/plugin.js +242 -0
  85. package/dist/agents/channels/bluebubbles/plugin.js.map +1 -0
  86. package/dist/agents/channels/bluebubbles/probe.d.ts +79 -0
  87. package/dist/agents/channels/bluebubbles/probe.d.ts.map +1 -0
  88. package/dist/agents/channels/bluebubbles/probe.js +138 -0
  89. package/dist/agents/channels/bluebubbles/probe.js.map +1 -0
  90. package/dist/agents/channels/bluebubbles/reactions.d.ts +46 -0
  91. package/dist/agents/channels/bluebubbles/reactions.d.ts.map +1 -0
  92. package/dist/agents/channels/bluebubbles/reactions.js +207 -0
  93. package/dist/agents/channels/bluebubbles/reactions.js.map +1 -0
  94. package/dist/agents/channels/bluebubbles/send.d.ts +132 -0
  95. package/dist/agents/channels/bluebubbles/send.d.ts.map +1 -0
  96. package/dist/agents/channels/bluebubbles/send.js +327 -0
  97. package/dist/agents/channels/bluebubbles/send.js.map +1 -0
  98. package/dist/agents/channels/bluebubbles/status-issues.d.ts +57 -0
  99. package/dist/agents/channels/bluebubbles/status-issues.d.ts.map +1 -0
  100. package/dist/agents/channels/bluebubbles/status-issues.js +93 -0
  101. package/dist/agents/channels/bluebubbles/status-issues.js.map +1 -0
  102. package/dist/agents/channels/bluebubbles/types.d.ts +69 -0
  103. package/dist/agents/channels/bluebubbles/types.d.ts.map +1 -0
  104. package/dist/agents/channels/bluebubbles/types.js +118 -0
  105. package/dist/agents/channels/bluebubbles/types.js.map +1 -0
  106. package/dist/agents/channels/bluebubbles/webhook.d.ts +97 -0
  107. package/dist/agents/channels/bluebubbles/webhook.d.ts.map +1 -0
  108. package/dist/agents/channels/bluebubbles/webhook.js +249 -0
  109. package/dist/agents/channels/bluebubbles/webhook.js.map +1 -0
  110. package/dist/agents/channels/bundled-channel-metas.d.ts +13 -0
  111. package/dist/agents/channels/bundled-channel-metas.d.ts.map +1 -1
  112. package/dist/agents/channels/bundled-channel-metas.js +33 -0
  113. package/dist/agents/channels/bundled-channel-metas.js.map +1 -1
  114. package/dist/agents/channels/imessage/account-config.d.ts +178 -0
  115. package/dist/agents/channels/imessage/account-config.d.ts.map +1 -0
  116. package/dist/agents/channels/imessage/account-config.js +371 -0
  117. package/dist/agents/channels/imessage/account-config.js.map +1 -0
  118. package/dist/agents/channels/imessage/adapter.d.ts +36 -0
  119. package/dist/agents/channels/imessage/adapter.d.ts.map +1 -0
  120. package/dist/agents/channels/imessage/adapter.js +286 -0
  121. package/dist/agents/channels/imessage/adapter.js.map +1 -0
  122. package/dist/agents/channels/imessage/client.d.ts +99 -0
  123. package/dist/agents/channels/imessage/client.d.ts.map +1 -0
  124. package/dist/agents/channels/imessage/client.js +231 -0
  125. package/dist/agents/channels/imessage/client.js.map +1 -0
  126. package/dist/agents/channels/imessage/connection.d.ts +101 -0
  127. package/dist/agents/channels/imessage/connection.d.ts.map +1 -0
  128. package/dist/agents/channels/imessage/connection.js +367 -0
  129. package/dist/agents/channels/imessage/connection.js.map +1 -0
  130. package/dist/agents/channels/imessage/format.d.ts +45 -0
  131. package/dist/agents/channels/imessage/format.d.ts.map +1 -0
  132. package/dist/agents/channels/imessage/format.js +170 -0
  133. package/dist/agents/channels/imessage/format.js.map +1 -0
  134. package/dist/agents/channels/imessage/history.d.ts +49 -0
  135. package/dist/agents/channels/imessage/history.d.ts.map +1 -0
  136. package/dist/agents/channels/imessage/history.js +74 -0
  137. package/dist/agents/channels/imessage/history.js.map +1 -0
  138. package/dist/agents/channels/imessage/index.d.ts +25 -0
  139. package/dist/agents/channels/imessage/index.d.ts.map +1 -0
  140. package/dist/agents/channels/imessage/index.js +25 -0
  141. package/dist/agents/channels/imessage/index.js.map +1 -0
  142. package/dist/agents/channels/imessage/media.d.ts +92 -0
  143. package/dist/agents/channels/imessage/media.d.ts.map +1 -0
  144. package/dist/agents/channels/imessage/media.js +196 -0
  145. package/dist/agents/channels/imessage/media.js.map +1 -0
  146. package/dist/agents/channels/imessage/messaging.d.ts +14 -0
  147. package/dist/agents/channels/imessage/messaging.d.ts.map +1 -0
  148. package/dist/agents/channels/imessage/messaging.js +37 -0
  149. package/dist/agents/channels/imessage/messaging.js.map +1 -0
  150. package/dist/agents/channels/imessage/module.d.ts +16 -0
  151. package/dist/agents/channels/imessage/module.d.ts.map +1 -0
  152. package/dist/agents/channels/imessage/module.js +23 -0
  153. package/dist/agents/channels/imessage/module.js.map +1 -0
  154. package/dist/agents/channels/imessage/monitor.d.ts +207 -0
  155. package/dist/agents/channels/imessage/monitor.d.ts.map +1 -0
  156. package/dist/agents/channels/imessage/monitor.js +504 -0
  157. package/dist/agents/channels/imessage/monitor.js.map +1 -0
  158. package/dist/agents/channels/imessage/plugin.d.ts +53 -0
  159. package/dist/agents/channels/imessage/plugin.d.ts.map +1 -0
  160. package/dist/agents/channels/imessage/plugin.js +215 -0
  161. package/dist/agents/channels/imessage/plugin.js.map +1 -0
  162. package/dist/agents/channels/imessage/probe.d.ts +68 -0
  163. package/dist/agents/channels/imessage/probe.d.ts.map +1 -0
  164. package/dist/agents/channels/imessage/probe.js +134 -0
  165. package/dist/agents/channels/imessage/probe.js.map +1 -0
  166. package/dist/agents/channels/imessage/remote-attachments.d.ts +61 -0
  167. package/dist/agents/channels/imessage/remote-attachments.d.ts.map +1 -0
  168. package/dist/agents/channels/imessage/remote-attachments.js +131 -0
  169. package/dist/agents/channels/imessage/remote-attachments.js.map +1 -0
  170. package/dist/agents/channels/imessage/send.d.ts +61 -0
  171. package/dist/agents/channels/imessage/send.d.ts.map +1 -0
  172. package/dist/agents/channels/imessage/send.js +108 -0
  173. package/dist/agents/channels/imessage/send.js.map +1 -0
  174. package/dist/agents/channels/imessage/targets.d.ts +91 -0
  175. package/dist/agents/channels/imessage/targets.d.ts.map +1 -0
  176. package/dist/agents/channels/imessage/targets.js +245 -0
  177. package/dist/agents/channels/imessage/targets.js.map +1 -0
  178. package/dist/agents/channels/imessage/watch-error.d.ts +23 -0
  179. package/dist/agents/channels/imessage/watch-error.d.ts.map +1 -0
  180. package/dist/agents/channels/imessage/watch-error.js +69 -0
  181. package/dist/agents/channels/imessage/watch-error.js.map +1 -0
  182. package/dist/agents/channels/inbound-pipeline.d.ts +11 -0
  183. package/dist/agents/channels/inbound-pipeline.d.ts.map +1 -1
  184. package/dist/agents/channels/inbound-pipeline.js +20 -1
  185. package/dist/agents/channels/inbound-pipeline.js.map +1 -1
  186. package/dist/agents/channels/manager.d.ts +9 -0
  187. package/dist/agents/channels/manager.d.ts.map +1 -1
  188. package/dist/agents/channels/manager.js.map +1 -1
  189. package/dist/agents/channels/sdk.d.ts +4 -0
  190. package/dist/agents/channels/sdk.d.ts.map +1 -1
  191. package/dist/agents/channels/sdk.js +4 -0
  192. package/dist/agents/channels/sdk.js.map +1 -1
  193. package/dist/agents/extensions/modules/index.d.ts.map +1 -1
  194. package/dist/agents/extensions/modules/index.js +12 -0
  195. package/dist/agents/extensions/modules/index.js.map +1 -1
  196. package/dist/agents/session-wiring.d.ts +31 -0
  197. package/dist/agents/session-wiring.d.ts.map +1 -1
  198. package/dist/agents/session-wiring.js +45 -2
  199. package/dist/agents/session-wiring.js.map +1 -1
  200. package/dist/agents/tools/bluebubbles-action-tool.d.ts +66 -0
  201. package/dist/agents/tools/bluebubbles-action-tool.d.ts.map +1 -0
  202. package/dist/agents/tools/bluebubbles-action-tool.js +234 -0
  203. package/dist/agents/tools/bluebubbles-action-tool.js.map +1 -0
  204. package/dist/agents/tools/registry.d.ts.map +1 -1
  205. package/dist/agents/tools/registry.js +18 -0
  206. package/dist/agents/tools/registry.js.map +1 -1
  207. package/dist/buildstamp.json +1 -1
  208. package/dist/cli/commands/expose.d.ts +40 -0
  209. package/dist/cli/commands/expose.d.ts.map +1 -0
  210. package/dist/cli/commands/expose.js +200 -0
  211. package/dist/cli/commands/expose.js.map +1 -0
  212. package/dist/cli/program/build-program.d.ts.map +1 -1
  213. package/dist/cli/program/build-program.js +61 -0
  214. package/dist/cli/program/build-program.js.map +1 -1
  215. package/dist/config/io.d.ts +41 -0
  216. package/dist/config/io.d.ts.map +1 -1
  217. package/dist/config/io.js.map +1 -1
  218. package/dist/core/server.d.ts.map +1 -1
  219. package/dist/core/server.js +48 -2
  220. package/dist/core/server.js.map +1 -1
  221. package/dist/core/tunnel/auth-proxy.d.ts +55 -0
  222. package/dist/core/tunnel/auth-proxy.d.ts.map +1 -0
  223. package/dist/core/tunnel/auth-proxy.js +179 -0
  224. package/dist/core/tunnel/auth-proxy.js.map +1 -0
  225. package/dist/core/tunnel/manager.d.ts +42 -0
  226. package/dist/core/tunnel/manager.d.ts.map +1 -0
  227. package/dist/core/tunnel/manager.js +102 -0
  228. package/dist/core/tunnel/manager.js.map +1 -0
  229. package/dist/core/tunnel/providers/bore.d.ts +18 -0
  230. package/dist/core/tunnel/providers/bore.d.ts.map +1 -0
  231. package/dist/core/tunnel/providers/bore.js +117 -0
  232. package/dist/core/tunnel/providers/bore.js.map +1 -0
  233. package/dist/core/tunnel/providers/cloudflared.d.ts +24 -0
  234. package/dist/core/tunnel/providers/cloudflared.d.ts.map +1 -0
  235. package/dist/core/tunnel/providers/cloudflared.js +179 -0
  236. package/dist/core/tunnel/providers/cloudflared.js.map +1 -0
  237. package/dist/core/tunnel/providers/custom.d.ts +21 -0
  238. package/dist/core/tunnel/providers/custom.d.ts.map +1 -0
  239. package/dist/core/tunnel/providers/custom.js +124 -0
  240. package/dist/core/tunnel/providers/custom.js.map +1 -0
  241. package/dist/core/tunnel/registry.d.ts +15 -0
  242. package/dist/core/tunnel/registry.d.ts.map +1 -0
  243. package/dist/core/tunnel/registry.js +26 -0
  244. package/dist/core/tunnel/registry.js.map +1 -0
  245. package/dist/core/tunnel/state.d.ts +39 -0
  246. package/dist/core/tunnel/state.d.ts.map +1 -0
  247. package/dist/core/tunnel/state.js +57 -0
  248. package/dist/core/tunnel/state.js.map +1 -0
  249. package/dist/core/tunnel/types.d.ts +61 -0
  250. package/dist/core/tunnel/types.d.ts.map +1 -0
  251. package/dist/core/tunnel/types.js +20 -0
  252. package/dist/core/tunnel/types.js.map +1 -0
  253. package/dist/infra/net/fetch-guard.d.ts +24 -2
  254. package/dist/infra/net/fetch-guard.d.ts.map +1 -1
  255. package/dist/infra/net/fetch-guard.js +78 -31
  256. package/dist/infra/net/fetch-guard.js.map +1 -1
  257. package/package.json +5 -1
@@ -0,0 +1,188 @@
1
+ /**
2
+ * BlueBubbles contact-name resolution.
3
+ *
4
+ * An inbound BlueBubbles message carries the sender as a raw handle — a phone
5
+ * number (`+15551234567`) or an email. For the agent's context it reads far
6
+ * better as a human display name ("Alex Rivera"). The BlueBubbles server exposes
7
+ * the host Mac's Contacts via `GET /api/v1/contact`, so this module asks the
8
+ * server for the address book ONCE per account, indexes it by a normalised phone
9
+ * key + lowercased email, and resolves a sender address → display name from that
10
+ * cache.
11
+ *
12
+ * The directory is cached per account in a small LRU with a TTL (like Discord's
13
+ * directory cache): the first inbound for an account warms it, subsequent
14
+ * inbound hits the in-memory index. A NEGATIVE result (no match) is cached for a
15
+ * shorter TTL so an unknown number isn't re-queried on every message. `fetch` is
16
+ * INJECTABLE (the test seam) so the whole path runs with no live server.
17
+ */
18
+ import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js";
19
+ /** How long a fetched directory index stays warm before a re-fetch (1h). */
20
+ const DIRECTORY_TTL_MS = 60 * 60 * 1000;
21
+ /** How long a per-address negative (no-match) result is cached (5m). */
22
+ const NEGATIVE_TTL_MS = 5 * 60 * 1000;
23
+ /** Cap on the number of accounts whose directory we keep indexed at once. */
24
+ const MAX_ACCOUNT_DIRECTORIES = 16;
25
+ /** Module-level per-account directory cache (LRU by insertion order). */
26
+ const directoryCache = new Map();
27
+ /**
28
+ * Normalise a phone-ish string to a comparison key: digits only, dropping a
29
+ * leading US country code so `+1 (555) 123-4567` and `5551234567` collide.
30
+ * Returns null for a too-short / non-numeric value (e.g. an email).
31
+ */
32
+ export function normalizePhoneKey(value) {
33
+ const digits = (value ?? "").replace(/\D/g, "");
34
+ if (!digits)
35
+ return null;
36
+ const trimmed = digits.length === 11 && digits.startsWith("1") ? digits.slice(1) : digits;
37
+ return trimmed.length >= 7 ? trimmed : null;
38
+ }
39
+ /** Build the lookup key for a sender address (email lowercased, phone digit-normalised). */
40
+ export function contactLookupKey(address) {
41
+ const trimmed = (address ?? "").trim();
42
+ if (!trimmed)
43
+ return null;
44
+ if (trimmed.includes("@"))
45
+ return trimmed.toLowerCase();
46
+ return normalizePhoneKey(trimmed);
47
+ }
48
+ /** Pull a display name out of a contact record (display → first+last → nickname). */
49
+ function contactDisplayName(c) {
50
+ const display = (c.displayName ?? "").trim();
51
+ if (display)
52
+ return display;
53
+ const first = (c.firstName ?? "").trim();
54
+ const last = (c.lastName ?? "").trim();
55
+ const full = `${first} ${last}`.trim();
56
+ if (full)
57
+ return full;
58
+ const nick = (c.nickname ?? "").trim();
59
+ return nick || undefined;
60
+ }
61
+ /** Read an address out of a `{ address }` object OR a bare string entry. */
62
+ function readAddressEntry(entry) {
63
+ if (typeof entry === "string")
64
+ return entry.trim() || undefined;
65
+ const addr = (entry?.address ?? "").trim();
66
+ return addr || undefined;
67
+ }
68
+ /** Build the phone/email → name index from the raw contact list. */
69
+ export function buildContactIndex(contacts) {
70
+ const byKey = new Map();
71
+ for (const c of contacts) {
72
+ if (!c || typeof c !== "object")
73
+ continue;
74
+ const name = contactDisplayName(c);
75
+ if (!name)
76
+ continue;
77
+ for (const phone of c.phoneNumbers ?? []) {
78
+ const addr = readAddressEntry(phone);
79
+ const key = addr ? normalizePhoneKey(addr) : null;
80
+ if (key && !byKey.has(key))
81
+ byKey.set(key, name);
82
+ }
83
+ for (const email of c.emails ?? []) {
84
+ const addr = readAddressEntry(email);
85
+ const key = addr ? addr.toLowerCase() : null;
86
+ if (key && !byKey.has(key))
87
+ byKey.set(key, name);
88
+ }
89
+ }
90
+ return byKey;
91
+ }
92
+ /** Touch an account directory to the MRU position (LRU bookkeeping). */
93
+ function touch(accountId, dir) {
94
+ directoryCache.delete(accountId);
95
+ directoryCache.set(accountId, dir);
96
+ while (directoryCache.size > MAX_ACCOUNT_DIRECTORIES) {
97
+ const oldest = directoryCache.keys().next().value;
98
+ if (oldest === undefined)
99
+ break;
100
+ directoryCache.delete(oldest);
101
+ }
102
+ }
103
+ /** Fetch + index the account's contact directory from the server. Never throws. */
104
+ async function fetchDirectory(args, now) {
105
+ let contacts = [];
106
+ try {
107
+ const url = buildBlueBubblesApiUrl({ serverUrl: args.serverUrl, path: "contact", password: args.password });
108
+ const res = await blueBubblesFetchWithTimeout(url, { method: "GET" }, { ...(args.timeoutMs !== undefined ? { timeoutMs: args.timeoutMs } : {}), ...(args.fetchImpl ? { fetchImpl: args.fetchImpl } : {}), ...(args.allowPrivateNetwork === false ? { allowPrivateNetwork: false } : {}) });
109
+ if (res.ok) {
110
+ const text = await res.text();
111
+ const body = text ? JSON.parse(text) : {};
112
+ const data = body.data;
113
+ if (Array.isArray(data))
114
+ contacts = data;
115
+ }
116
+ }
117
+ catch {
118
+ contacts = [];
119
+ }
120
+ const dir = {
121
+ byKey: buildContactIndex(contacts),
122
+ negative: new Map(),
123
+ expiresAt: now + DIRECTORY_TTL_MS,
124
+ };
125
+ return dir;
126
+ }
127
+ /**
128
+ * Resolve an inbound sender address → a human display name, or `undefined` when
129
+ * unknown. Warms (and caches) the account's contact directory on first use;
130
+ * subsequent calls hit the in-memory index. Negative results are cached briefly
131
+ * so an unknown number isn't re-queried every message. NEVER throws — a transport
132
+ * failure just yields `undefined` (the message still flows with the raw handle).
133
+ */
134
+ export async function resolveBlueBubblesContactName(address, args, now = Date.now()) {
135
+ const key = contactLookupKey(address);
136
+ if (!key)
137
+ return undefined;
138
+ let dir = directoryCache.get(args.accountId);
139
+ if (!dir || dir.expiresAt <= now) {
140
+ dir = await fetchDirectory(args, now);
141
+ touch(args.accountId, dir);
142
+ }
143
+ else {
144
+ touch(args.accountId, dir);
145
+ }
146
+ const hit = dir.byKey.get(key);
147
+ if (hit)
148
+ return hit;
149
+ // Negative cache: don't re-query the directory mid-TTL for a known miss.
150
+ const negExpiry = dir.negative.get(key);
151
+ if (negExpiry && negExpiry > now)
152
+ return undefined;
153
+ dir.negative.set(key, now + NEGATIVE_TTL_MS);
154
+ return undefined;
155
+ }
156
+ /**
157
+ * SYNCHRONOUS cache-only peek: resolve an address from an ALREADY-WARM account
158
+ * directory, or `undefined` when the directory isn't cached yet / it's a miss.
159
+ * Never fetches — use this on the hot inbound path so dispatch stays synchronous;
160
+ * pair it with `warmBlueBubblesContactDirectory` to populate the cache in the
161
+ * background.
162
+ */
163
+ export function peekBlueBubblesContactName(address, accountId, now = Date.now()) {
164
+ const key = contactLookupKey(address);
165
+ if (!key)
166
+ return undefined;
167
+ const dir = directoryCache.get(accountId);
168
+ if (!dir || dir.expiresAt <= now)
169
+ return undefined;
170
+ return dir.byKey.get(key);
171
+ }
172
+ /**
173
+ * Warm the account's contact directory cache in the background (best-effort).
174
+ * Fetches + indexes ONLY when the cache is cold/expired; a warm cache is a
175
+ * no-op. Never throws. Returns a promise the caller can ignore.
176
+ */
177
+ export async function warmBlueBubblesContactDirectory(args, now = Date.now()) {
178
+ const existing = directoryCache.get(args.accountId);
179
+ if (existing && existing.expiresAt > now)
180
+ return;
181
+ const dir = await fetchDirectory(args, now);
182
+ touch(args.accountId, dir);
183
+ }
184
+ /** Drop every cached account directory (test isolation + reload teardown). */
185
+ export function clearBlueBubblesContactCache() {
186
+ directoryCache.clear();
187
+ }
188
+ //# sourceMappingURL=contact-names.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contact-names.js","sourceRoot":"","sources":["../../../../src/agents/channels/bluebubbles/contact-names.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * BlueBubbles inbound debounce / balloon-coalescing.
3
+ *
4
+ * BlueBubbles fires SEPARATE `new-message` webhook events for things that are
5
+ * really ONE logical message:
6
+ *
7
+ * - a URL text + its link-preview "balloon" (the balloon carries an
8
+ * `associatedMessageGuid` pointing back at the text + a `balloonBundleId`);
9
+ * - a text and its attachment that the server splits across two webhooks
10
+ * (same `messageGuid`, one with text, one with the now-indexed media);
11
+ * - rapid-fire bubbles a sender taps out back-to-back.
12
+ *
13
+ * Dispatching each as its own turn produces TWO agent replies for one user
14
+ * action. This module collects messages that share a coalescing key inside a
15
+ * short window (default 500 ms) and MERGES them into one (`combineDebounceEntries`):
16
+ * the texts join (de-duped), the attachments concatenate, the latest timestamp
17
+ * wins, and reply context is preserved. The merged message is then dispatched
18
+ * once.
19
+ *
20
+ * Self-contained + channel-local: it owns its own timer map (no central
21
+ * dependency). The connection wires it in ONLY when `inboundDebounceMs > 0`; the
22
+ * default (0) keeps the historical synchronous dispatch path so nothing changes
23
+ * for installs that don't opt in.
24
+ */
25
+ import type { BlueBubblesInboundMessage } from "./connection.js";
26
+ /** Default debounce window (ms) when coalescing is enabled but no value is given. */
27
+ export declare const BLUEBUBBLES_DEFAULT_INBOUND_DEBOUNCE_MS = 500;
28
+ /** Hard ceiling on the debounce window (defends against a misconfigured huge value). */
29
+ export declare const BLUEBUBBLES_MAX_INBOUND_DEBOUNCE_MS = 10000;
30
+ /** One buffered inbound awaiting a possible merge. */
31
+ interface DebounceEntry {
32
+ message: BlueBubblesInboundMessage;
33
+ }
34
+ /**
35
+ * Merge multiple buffered inbound messages into ONE. The first entry is the
36
+ * base (typically the originating text); texts from the rest are appended unless
37
+ * a case-insensitive duplicate (a URL echoed in both the text and its balloon),
38
+ * attachments concatenate, the latest timestamp wins, and the first reply target
39
+ * is preserved. The deferred-media thunks are chained so the merged message
40
+ * resolves ALL of them.
41
+ */
42
+ export declare function combineDebounceEntries(entries: DebounceEntry[]): BlueBubblesInboundMessage;
43
+ /**
44
+ * The coalescing key for an inbound message. A balloon (URL preview / sticker)
45
+ * uses a different `messageGuid` than its parent text but carries the parent's
46
+ * `associatedMessageGuid` — key on THAT so text + balloon collapse. Otherwise key
47
+ * on the stable `messageGuid` (a text + its late-indexed attachment share it).
48
+ * Falls back to chat + sender so two bare bubbles in the same chat still merge.
49
+ */
50
+ export declare function resolveDebounceKey(accountId: string, msg: BlueBubblesInboundMessage): string;
51
+ /** A debouncer: enqueue an inbound + a way to tear all pending timers down. */
52
+ export interface BlueBubblesInboundDebouncer {
53
+ /** Buffer an inbound; it (and any that share its key) flush after the window. */
54
+ enqueue(msg: BlueBubblesInboundMessage): void;
55
+ /** Flush everything immediately (e.g. on close). */
56
+ flushAll(): void;
57
+ /** Clear all pending timers without flushing (teardown). */
58
+ clear(): void;
59
+ }
60
+ export interface CreateInboundDebouncerArgs {
61
+ accountId: string;
62
+ /** Debounce window (ms). Clamped to `(0, MAX]`. */
63
+ debounceMs: number;
64
+ /** Called with the (possibly merged) message once a key's window elapses. */
65
+ dispatch: (msg: BlueBubblesInboundMessage) => void;
66
+ /** Optional verbose logger. */
67
+ log?: (msg: string) => void;
68
+ }
69
+ /** Clamp a requested debounce window into `[1, MAX]` ms. */
70
+ export declare function clampInboundDebounceMs(raw: number | undefined): number;
71
+ /**
72
+ * Build a per-conversation inbound debouncer. Each coalescing key gets its own
73
+ * buffer + timer; when the timer fires the buffered entries are merged (or passed
74
+ * through when there's only one) and handed to `dispatch`.
75
+ */
76
+ export declare function createBlueBubblesInboundDebouncer(args: CreateInboundDebouncerArgs): BlueBubblesInboundDebouncer;
77
+ export {};
78
+ //# sourceMappingURL=debounce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debounce.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/bluebubbles/debounce.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAEjE,qFAAqF;AACrF,eAAO,MAAM,uCAAuC,MAAM,CAAC;AAC3D,wFAAwF;AACxF,eAAO,MAAM,mCAAmC,QAAS,CAAC;AAE1D,sDAAsD;AACtD,UAAU,aAAa;IACtB,OAAO,EAAE,yBAAyB,CAAC;CACnC;AAQD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,yBAAyB,CAmD1F;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,yBAAyB,GAAG,MAAM,CAS5F;AAED,+EAA+E;AAC/E,MAAM,WAAW,2BAA2B;IAC3C,iFAAiF;IACjF,OAAO,CAAC,GAAG,EAAE,yBAAyB,GAAG,IAAI,CAAC;IAC9C,oDAAoD;IACpD,QAAQ,IAAI,IAAI,CAAC;IACjB,4DAA4D;IAC5D,KAAK,IAAI,IAAI,CAAC;CACd;AAED,MAAM,WAAW,0BAA0B;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,QAAQ,EAAE,CAAC,GAAG,EAAE,yBAAyB,KAAK,IAAI,CAAC;IACnD,+BAA+B;IAC/B,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5B;AAED,4DAA4D;AAC5D,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAItE;AAED;;;;GAIG;AACH,wBAAgB,iCAAiC,CAAC,IAAI,EAAE,0BAA0B,GAAG,2BAA2B,CAoD/G"}
@@ -0,0 +1,173 @@
1
+ /**
2
+ * BlueBubbles inbound debounce / balloon-coalescing.
3
+ *
4
+ * BlueBubbles fires SEPARATE `new-message` webhook events for things that are
5
+ * really ONE logical message:
6
+ *
7
+ * - a URL text + its link-preview "balloon" (the balloon carries an
8
+ * `associatedMessageGuid` pointing back at the text + a `balloonBundleId`);
9
+ * - a text and its attachment that the server splits across two webhooks
10
+ * (same `messageGuid`, one with text, one with the now-indexed media);
11
+ * - rapid-fire bubbles a sender taps out back-to-back.
12
+ *
13
+ * Dispatching each as its own turn produces TWO agent replies for one user
14
+ * action. This module collects messages that share a coalescing key inside a
15
+ * short window (default 500 ms) and MERGES them into one (`combineDebounceEntries`):
16
+ * the texts join (de-duped), the attachments concatenate, the latest timestamp
17
+ * wins, and reply context is preserved. The merged message is then dispatched
18
+ * once.
19
+ *
20
+ * Self-contained + channel-local: it owns its own timer map (no central
21
+ * dependency). The connection wires it in ONLY when `inboundDebounceMs > 0`; the
22
+ * default (0) keeps the historical synchronous dispatch path so nothing changes
23
+ * for installs that don't opt in.
24
+ */
25
+ /** Default debounce window (ms) when coalescing is enabled but no value is given. */
26
+ export const BLUEBUBBLES_DEFAULT_INBOUND_DEBOUNCE_MS = 500;
27
+ /** Hard ceiling on the debounce window (defends against a misconfigured huge value). */
28
+ export const BLUEBUBBLES_MAX_INBOUND_DEBOUNCE_MS = 10_000;
29
+ /**
30
+ * Merge multiple buffered inbound messages into ONE. The first entry is the
31
+ * base (typically the originating text); texts from the rest are appended unless
32
+ * a case-insensitive duplicate (a URL echoed in both the text and its balloon),
33
+ * attachments concatenate, the latest timestamp wins, and the first reply target
34
+ * is preserved. The deferred-media thunks are chained so the merged message
35
+ * resolves ALL of them.
36
+ */
37
+ export function combineDebounceEntries(entries) {
38
+ if (entries.length === 0)
39
+ throw new Error("cannot combine zero debounce entries");
40
+ const first = entries[0].message;
41
+ if (entries.length === 1)
42
+ return first;
43
+ // Join texts, skipping empties + case-insensitive duplicates.
44
+ const seen = new Set();
45
+ const parts = [];
46
+ for (const { message } of entries) {
47
+ const text = (typeof message.text === "string" ? message.text : "").trim();
48
+ if (!text)
49
+ continue;
50
+ const norm = text.toLowerCase();
51
+ if (seen.has(norm))
52
+ continue;
53
+ seen.add(norm);
54
+ parts.push(text);
55
+ }
56
+ // Concatenate raw attachment descriptors across all entries.
57
+ const attachments = entries.flatMap((e) => e.message.attachments ?? []);
58
+ // Latest timestamp wins.
59
+ const stamps = entries
60
+ .map((e) => e.message.timestampMs)
61
+ .filter((t) => typeof t === "number");
62
+ const latest = stamps.length > 0 ? Math.max(...stamps) : first.timestampMs;
63
+ // Chain every deferred-media thunk so the merged message resolves them all.
64
+ const thunks = entries.map((e) => e.message.resolveMedia).filter((r) => !!r);
65
+ const resolveMedia = thunks.length > 0
66
+ ? async () => {
67
+ const all = await Promise.all(thunks.map((t) => t().catch(() => [])));
68
+ return all.flat();
69
+ }
70
+ : first.resolveMedia;
71
+ // Prefer a reply target from any entry that carries one.
72
+ const withReply = entries.find((e) => e.message.replyToGuid)?.message;
73
+ const merged = {
74
+ ...first,
75
+ text: parts.join(" "),
76
+ attachments,
77
+ ...(latest !== undefined ? { timestampMs: latest } : {}),
78
+ ...(withReply?.replyToGuid ? { replyToGuid: withReply.replyToGuid } : {}),
79
+ // The merged message is no longer "just a balloon".
80
+ ...(resolveMedia ? { resolveMedia } : {}),
81
+ };
82
+ // Drop the balloon-only markers now that we've coalesced.
83
+ delete merged.balloonBundleId;
84
+ return merged;
85
+ }
86
+ /**
87
+ * The coalescing key for an inbound message. A balloon (URL preview / sticker)
88
+ * uses a different `messageGuid` than its parent text but carries the parent's
89
+ * `associatedMessageGuid` — key on THAT so text + balloon collapse. Otherwise key
90
+ * on the stable `messageGuid` (a text + its late-indexed attachment share it).
91
+ * Falls back to chat + sender so two bare bubbles in the same chat still merge.
92
+ */
93
+ export function resolveDebounceKey(accountId, msg) {
94
+ // A balloon keys on its PARENT's guid so it collides with the parent text
95
+ // (which keys on its OWN messageGuid) — same `msg:` namespace either way.
96
+ const assoc = msg.associatedMessageGuid?.trim();
97
+ if (msg.balloonBundleId?.trim() && assoc)
98
+ return `${accountId}:msg:${assoc}`;
99
+ const guid = msg.messageGuid?.trim();
100
+ if (guid)
101
+ return `${accountId}:msg:${guid}`;
102
+ const chat = msg.chatGuid?.trim() || "chat";
103
+ return `${accountId}:${chat}:${msg.from}`;
104
+ }
105
+ /** Clamp a requested debounce window into `[1, MAX]` ms. */
106
+ export function clampInboundDebounceMs(raw) {
107
+ const n = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : 0;
108
+ if (n <= 0)
109
+ return 0;
110
+ return Math.min(n, BLUEBUBBLES_MAX_INBOUND_DEBOUNCE_MS);
111
+ }
112
+ /**
113
+ * Build a per-conversation inbound debouncer. Each coalescing key gets its own
114
+ * buffer + timer; when the timer fires the buffered entries are merged (or passed
115
+ * through when there's only one) and handed to `dispatch`.
116
+ */
117
+ export function createBlueBubblesInboundDebouncer(args) {
118
+ const windowMs = clampInboundDebounceMs(args.debounceMs);
119
+ const slots = new Map();
120
+ const flush = (key) => {
121
+ const slot = slots.get(key);
122
+ if (!slot)
123
+ return;
124
+ slots.delete(key);
125
+ clearTimeout(slot.timer);
126
+ if (slot.entries.length === 0)
127
+ return;
128
+ try {
129
+ const out = slot.entries.length === 1 ? slot.entries[0].message : combineDebounceEntries(slot.entries);
130
+ if (slot.entries.length > 1) {
131
+ args.log?.(`coalesced ${slot.entries.length} messages into one turn`);
132
+ }
133
+ args.dispatch(out);
134
+ }
135
+ catch (err) {
136
+ args.log?.(`debounce flush failed: ${err instanceof Error ? err.message : String(err)}`);
137
+ }
138
+ };
139
+ return {
140
+ enqueue(msg) {
141
+ // Window disabled → straight through (defensive; callers gate on > 0).
142
+ if (windowMs <= 0) {
143
+ args.dispatch(msg);
144
+ return;
145
+ }
146
+ const key = resolveDebounceKey(args.accountId, msg);
147
+ const existing = slots.get(key);
148
+ if (existing) {
149
+ existing.entries.push({ message: msg });
150
+ clearTimeout(existing.timer);
151
+ existing.timer = arm(key);
152
+ return;
153
+ }
154
+ slots.set(key, { entries: [{ message: msg }], timer: arm(key) });
155
+ },
156
+ flushAll() {
157
+ for (const key of [...slots.keys()])
158
+ flush(key);
159
+ },
160
+ clear() {
161
+ for (const slot of slots.values())
162
+ clearTimeout(slot.timer);
163
+ slots.clear();
164
+ },
165
+ };
166
+ function arm(key) {
167
+ const t = setTimeout(() => flush(key), windowMs);
168
+ if (typeof t.unref === "function")
169
+ t.unref();
170
+ return t;
171
+ }
172
+ }
173
+ //# sourceMappingURL=debounce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debounce.js","sourceRoot":"","sources":["../../../../src/agents/channels/bluebubbles/debounce.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * BlueBubbles inbound dedupe key.
3
+ *
4
+ * BlueBubbles has no inbound ack/sequence and its message poller can REPLAY a
5
+ * lookback window of `new-message` events after a server restart, so the same
6
+ * message GUID can arrive twice. The connection layer claims each inbound through
7
+ * the shared `createDedupeCache` (claim-once, LRU + TTL); this module just
8
+ * computes the dedupe KEY.
9
+ *
10
+ * The key is the message GUID, namespaced per account. An `updated-message`
11
+ * (attachment-indexing follow-up) gets a distinct `:updated` suffix so the text
12
+ * pass and the attachment follow-up aren't collapsed into one (the follow-up
13
+ * must be allowed through to attach the now-indexed media).
14
+ */
15
+ /** The default dedupe cache TTL — generous (BlueBubbles replays up to ~1 week). */
16
+ export declare const BLUEBUBBLES_DEDUPE_TTL_MS: number;
17
+ /** The default dedupe cache size cap. */
18
+ export declare const BLUEBUBBLES_DEDUPE_MAX_ENTRIES = 5000;
19
+ /**
20
+ * Resolve the dedupe key for a webhook payload (or undefined when the payload has
21
+ * no usable message GUID — those are handled by the normalize skip path anyway).
22
+ * `accountId` namespaces the key so two accounts never collide.
23
+ */
24
+ export declare function resolveBlueBubblesDedupeKey(accountId: string, payload: unknown, eventType?: string): string | undefined;
25
+ //# sourceMappingURL=dedupe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dedupe.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/bluebubbles/dedupe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,mFAAmF;AACnF,eAAO,MAAM,yBAAyB,QAA0B,CAAC;AACjE,yCAAyC;AACzC,eAAO,MAAM,8BAA8B,OAAO,CAAC;AAEnD;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOvH"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * BlueBubbles inbound dedupe key.
3
+ *
4
+ * BlueBubbles has no inbound ack/sequence and its message poller can REPLAY a
5
+ * lookback window of `new-message` events after a server restart, so the same
6
+ * message GUID can arrive twice. The connection layer claims each inbound through
7
+ * the shared `createDedupeCache` (claim-once, LRU + TTL); this module just
8
+ * computes the dedupe KEY.
9
+ *
10
+ * The key is the message GUID, namespaced per account. An `updated-message`
11
+ * (attachment-indexing follow-up) gets a distinct `:updated` suffix so the text
12
+ * pass and the attachment follow-up aren't collapsed into one (the follow-up
13
+ * must be allowed through to attach the now-indexed media).
14
+ */
15
+ import { normalizeBlueBubblesWebhook } from "./normalize.js";
16
+ /** The default dedupe cache TTL — generous (BlueBubbles replays up to ~1 week). */
17
+ export const BLUEBUBBLES_DEDUPE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
18
+ /** The default dedupe cache size cap. */
19
+ export const BLUEBUBBLES_DEDUPE_MAX_ENTRIES = 5000;
20
+ /**
21
+ * Resolve the dedupe key for a webhook payload (or undefined when the payload has
22
+ * no usable message GUID — those are handled by the normalize skip path anyway).
23
+ * `accountId` namespaces the key so two accounts never collide.
24
+ */
25
+ export function resolveBlueBubblesDedupeKey(accountId, payload, eventType) {
26
+ const result = normalizeBlueBubblesWebhook(payload, eventType);
27
+ if (result.kind !== "message")
28
+ return undefined;
29
+ const guid = result.message.messageGuid.trim();
30
+ if (!guid)
31
+ return undefined;
32
+ const base = `${accountId}:${guid}`;
33
+ return eventType === "updated-message" ? `${base}:updated` : base;
34
+ }
35
+ //# sourceMappingURL=dedupe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../../../src/agents/channels/bluebubbles/dedupe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAE7D,mFAAmF;AACnF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AACjE,yCAAyC;AACzC,MAAM,CAAC,MAAM,8BAA8B,GAAG,IAAI,CAAC;AAEnD;;;;GAIG;AACH,MAAM,UAAU,2BAA2B,CAAC,SAAiB,EAAE,OAAgB,EAAE,SAAkB;IAClG,MAAM,MAAM,GAAG,2BAA2B,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IAC/D,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAChD,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;IAC/C,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,MAAM,IAAI,GAAG,GAAG,SAAS,IAAI,IAAI,EAAE,CAAC;IACpC,OAAO,SAAS,KAAK,iBAAiB,CAAC,CAAC,CAAC,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;AACnE,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * iMessage send-effect (balloon / screen) id resolution.
3
+ *
4
+ * The Private-API `message/text` endpoint accepts an `effectId` — Apple's bundle
5
+ * identifier for a bubble or screen effect (`com.apple.MobileSMS.expressivesend.*`
6
+ * / `com.apple.messages.effect.*`). `resolveEffectId` maps a friendly short name
7
+ * (`"confetti"`, `"slam"`, `"invisible ink"`) onto that id; an already-qualified
8
+ * Apple id passes through.
9
+ */
10
+ /**
11
+ * Resolve an effect name to an Apple effect id. Tries the literal name, then a
12
+ * `[\s_]`→`-` variant, then a separator-stripped compact form; an already-fully-
13
+ * qualified `com.apple.*` id passes through. Returns undefined for an unknown
14
+ * name (the caller then sends with no effect).
15
+ */
16
+ export declare function resolveEffectId(raw: string): string | undefined;
17
+ //# sourceMappingURL=effects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effects.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/bluebubbles/effects.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA2BH;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAW/D"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * iMessage send-effect (balloon / screen) id resolution.
3
+ *
4
+ * The Private-API `message/text` endpoint accepts an `effectId` — Apple's bundle
5
+ * identifier for a bubble or screen effect (`com.apple.MobileSMS.expressivesend.*`
6
+ * / `com.apple.messages.effect.*`). `resolveEffectId` maps a friendly short name
7
+ * (`"confetti"`, `"slam"`, `"invisible ink"`) onto that id; an already-qualified
8
+ * Apple id passes through.
9
+ */
10
+ /** Friendly name → Apple effect bundle id. */
11
+ const EFFECT_MAP = {
12
+ // Bubble effects
13
+ slam: "com.apple.MobileSMS.expressivesend.impact",
14
+ impact: "com.apple.MobileSMS.expressivesend.impact",
15
+ loud: "com.apple.MobileSMS.expressivesend.loud",
16
+ gentle: "com.apple.MobileSMS.expressivesend.gentle",
17
+ invisible: "com.apple.MobileSMS.expressivesend.invisibleink",
18
+ "invisible-ink": "com.apple.MobileSMS.expressivesend.invisibleink",
19
+ "invisible ink": "com.apple.MobileSMS.expressivesend.invisibleink",
20
+ invisibleink: "com.apple.MobileSMS.expressivesend.invisibleink",
21
+ // Screen effects
22
+ echo: "com.apple.messages.effect.CKEchoEffect",
23
+ spotlight: "com.apple.messages.effect.CKSpotlightEffect",
24
+ balloons: "com.apple.messages.effect.CKHappyBirthdayEffect",
25
+ confetti: "com.apple.messages.effect.CKConfettiEffect",
26
+ love: "com.apple.messages.effect.CKHeartEffect",
27
+ heart: "com.apple.messages.effect.CKHeartEffect",
28
+ hearts: "com.apple.messages.effect.CKHeartEffect",
29
+ lasers: "com.apple.messages.effect.CKLasersEffect",
30
+ fireworks: "com.apple.messages.effect.CKFireworksEffect",
31
+ celebration: "com.apple.messages.effect.CKSparklesEffect",
32
+ sparkles: "com.apple.messages.effect.CKSparklesEffect",
33
+ };
34
+ /**
35
+ * Resolve an effect name to an Apple effect id. Tries the literal name, then a
36
+ * `[\s_]`→`-` variant, then a separator-stripped compact form; an already-fully-
37
+ * qualified `com.apple.*` id passes through. Returns undefined for an unknown
38
+ * name (the caller then sends with no effect).
39
+ */
40
+ export function resolveEffectId(raw) {
41
+ const trimmed = (raw ?? "").trim();
42
+ if (!trimmed)
43
+ return undefined;
44
+ if (/^com\.apple\./i.test(trimmed))
45
+ return trimmed;
46
+ const lower = trimmed.toLowerCase();
47
+ if (EFFECT_MAP[lower])
48
+ return EFFECT_MAP[lower];
49
+ const dashed = lower.replace(/[\s_]+/g, "-");
50
+ if (EFFECT_MAP[dashed])
51
+ return EFFECT_MAP[dashed];
52
+ const compact = lower.replace(/[\s_-]+/g, "");
53
+ if (EFFECT_MAP[compact])
54
+ return EFFECT_MAP[compact];
55
+ return undefined;
56
+ }
57
+ //# sourceMappingURL=effects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effects.js","sourceRoot":"","sources":["../../../../src/agents/channels/bluebubbles/effects.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,8CAA8C;AAC9C,MAAM,UAAU,GAA2B;IAC1C,iBAAiB;IACjB,IAAI,EAAE,2CAA2C;IACjD,MAAM,EAAE,2CAA2C;IACnD,IAAI,EAAE,yCAAyC;IAC/C,MAAM,EAAE,2CAA2C;IACnD,SAAS,EAAE,iDAAiD;IAC5D,eAAe,EAAE,iDAAiD;IAClE,eAAe,EAAE,iDAAiD;IAClE,YAAY,EAAE,iDAAiD;IAC/D,iBAAiB;IACjB,IAAI,EAAE,wCAAwC;IAC9C,SAAS,EAAE,6CAA6C;IACxD,QAAQ,EAAE,iDAAiD;IAC3D,QAAQ,EAAE,4CAA4C;IACtD,IAAI,EAAE,yCAAyC;IAC/C,KAAK,EAAE,yCAAyC;IAChD,MAAM,EAAE,yCAAyC;IACjD,MAAM,EAAE,0CAA0C;IAClD,SAAS,EAAE,6CAA6C;IACxD,WAAW,EAAE,4CAA4C;IACzD,QAAQ,EAAE,4CAA4C;CACtD,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW;IAC1C,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACnC,IAAI,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IAC/B,IAAI,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IACnD,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IACpC,IAAI,UAAU,CAAC,KAAK,CAAC;QAAE,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAC7C,IAAI,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAC9C,IAAI,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC;IACpD,OAAO,SAAS,CAAC;AAClB,CAAC"}