agent-messenger 2.27.1 → 2.28.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 (194) hide show
  1. package/.claude-plugin/README.md +11 -1
  2. package/.claude-plugin/marketplace.json +14 -1
  3. package/.claude-plugin/plugin.json +4 -2
  4. package/AGENTS.md +4 -0
  5. package/README.md +40 -9
  6. package/dist/package.json +10 -2
  7. package/dist/src/cli.d.ts.map +1 -1
  8. package/dist/src/cli.js +3 -0
  9. package/dist/src/cli.js.map +1 -1
  10. package/dist/src/platforms/discordbot/cli.js +1 -1
  11. package/dist/src/platforms/imessage/cli.d.ts +5 -0
  12. package/dist/src/platforms/imessage/cli.d.ts.map +1 -0
  13. package/dist/src/platforms/imessage/cli.js +31 -0
  14. package/dist/src/platforms/imessage/cli.js.map +1 -0
  15. package/dist/src/platforms/imessage/client.d.ts +43 -0
  16. package/dist/src/platforms/imessage/client.d.ts.map +1 -0
  17. package/dist/src/platforms/imessage/client.js +190 -0
  18. package/dist/src/platforms/imessage/client.js.map +1 -0
  19. package/dist/src/platforms/imessage/commands/auth.d.ts +31 -0
  20. package/dist/src/platforms/imessage/commands/auth.d.ts.map +1 -0
  21. package/dist/src/platforms/imessage/commands/auth.js +100 -0
  22. package/dist/src/platforms/imessage/commands/auth.js.map +1 -0
  23. package/dist/src/platforms/imessage/commands/chat.d.ts +3 -0
  24. package/dist/src/platforms/imessage/commands/chat.d.ts.map +1 -0
  25. package/dist/src/platforms/imessage/commands/chat.js +42 -0
  26. package/dist/src/platforms/imessage/commands/chat.js.map +1 -0
  27. package/dist/src/platforms/imessage/commands/doctor.d.ts +25 -0
  28. package/dist/src/platforms/imessage/commands/doctor.d.ts.map +1 -0
  29. package/dist/src/platforms/imessage/commands/doctor.js +101 -0
  30. package/dist/src/platforms/imessage/commands/doctor.js.map +1 -0
  31. package/dist/src/platforms/imessage/commands/index.d.ts +7 -0
  32. package/dist/src/platforms/imessage/commands/index.d.ts.map +1 -0
  33. package/dist/src/platforms/imessage/commands/index.js +7 -0
  34. package/dist/src/platforms/imessage/commands/index.js.map +1 -0
  35. package/dist/src/platforms/imessage/commands/message.d.ts +5 -0
  36. package/dist/src/platforms/imessage/commands/message.d.ts.map +1 -0
  37. package/dist/src/platforms/imessage/commands/message.js +127 -0
  38. package/dist/src/platforms/imessage/commands/message.js.map +1 -0
  39. package/dist/src/platforms/imessage/commands/setup.d.ts +20 -0
  40. package/dist/src/platforms/imessage/commands/setup.d.ts.map +1 -0
  41. package/dist/src/platforms/imessage/commands/setup.js +57 -0
  42. package/dist/src/platforms/imessage/commands/setup.js.map +1 -0
  43. package/dist/src/platforms/imessage/commands/shared.d.ts +10 -0
  44. package/dist/src/platforms/imessage/commands/shared.d.ts.map +1 -0
  45. package/dist/src/platforms/imessage/commands/shared.js +53 -0
  46. package/dist/src/platforms/imessage/commands/shared.js.map +1 -0
  47. package/dist/src/platforms/imessage/commands/whoami.d.ts +3 -0
  48. package/dist/src/platforms/imessage/commands/whoami.d.ts.map +1 -0
  49. package/dist/src/platforms/imessage/commands/whoami.js +20 -0
  50. package/dist/src/platforms/imessage/commands/whoami.js.map +1 -0
  51. package/dist/src/platforms/imessage/credential-manager.d.ts +22 -0
  52. package/dist/src/platforms/imessage/credential-manager.d.ts.map +1 -0
  53. package/dist/src/platforms/imessage/credential-manager.js +111 -0
  54. package/dist/src/platforms/imessage/credential-manager.js.map +1 -0
  55. package/dist/src/platforms/imessage/ensure-auth.d.ts +2 -0
  56. package/dist/src/platforms/imessage/ensure-auth.d.ts.map +1 -0
  57. package/dist/src/platforms/imessage/ensure-auth.js +13 -0
  58. package/dist/src/platforms/imessage/ensure-auth.js.map +1 -0
  59. package/dist/src/platforms/imessage/errors.d.ts +3 -0
  60. package/dist/src/platforms/imessage/errors.d.ts.map +1 -0
  61. package/dist/src/platforms/imessage/errors.js +25 -0
  62. package/dist/src/platforms/imessage/errors.js.map +1 -0
  63. package/dist/src/platforms/imessage/index.d.ts +7 -0
  64. package/dist/src/platforms/imessage/index.d.ts.map +1 -0
  65. package/dist/src/platforms/imessage/index.js +5 -0
  66. package/dist/src/platforms/imessage/index.js.map +1 -0
  67. package/dist/src/platforms/imessage/rpc.d.ts +20 -0
  68. package/dist/src/platforms/imessage/rpc.d.ts.map +1 -0
  69. package/dist/src/platforms/imessage/rpc.js +133 -0
  70. package/dist/src/platforms/imessage/rpc.js.map +1 -0
  71. package/dist/src/platforms/imessage/types.d.ts +59 -0
  72. package/dist/src/platforms/imessage/types.d.ts.map +1 -0
  73. package/dist/src/platforms/imessage/types.js +29 -0
  74. package/dist/src/platforms/imessage/types.js.map +1 -0
  75. package/dist/src/platforms/instagram/cli.js +1 -1
  76. package/dist/src/platforms/instagram/client.d.ts +6 -1
  77. package/dist/src/platforms/instagram/client.d.ts.map +1 -1
  78. package/dist/src/platforms/instagram/client.js +17 -0
  79. package/dist/src/platforms/instagram/client.js.map +1 -1
  80. package/dist/src/platforms/instagram/hybrid-listener.d.ts +48 -0
  81. package/dist/src/platforms/instagram/hybrid-listener.d.ts.map +1 -0
  82. package/dist/src/platforms/instagram/hybrid-listener.js +154 -0
  83. package/dist/src/platforms/instagram/hybrid-listener.js.map +1 -0
  84. package/dist/src/platforms/instagram/index.d.ts +2 -0
  85. package/dist/src/platforms/instagram/index.d.ts.map +1 -1
  86. package/dist/src/platforms/instagram/index.js +2 -0
  87. package/dist/src/platforms/instagram/index.js.map +1 -1
  88. package/dist/src/platforms/instagram/mqtt/connection.d.ts +4 -0
  89. package/dist/src/platforms/instagram/mqtt/connection.d.ts.map +1 -0
  90. package/dist/src/platforms/instagram/mqtt/connection.js +66 -0
  91. package/dist/src/platforms/instagram/mqtt/connection.js.map +1 -0
  92. package/dist/src/platforms/instagram/mqtt/thrift.d.ts +22 -0
  93. package/dist/src/platforms/instagram/mqtt/thrift.d.ts.map +1 -0
  94. package/dist/src/platforms/instagram/mqtt/thrift.js +143 -0
  95. package/dist/src/platforms/instagram/mqtt/thrift.js.map +1 -0
  96. package/dist/src/platforms/instagram/mqtt/transport.d.ts +42 -0
  97. package/dist/src/platforms/instagram/mqtt/transport.d.ts.map +1 -0
  98. package/dist/src/platforms/instagram/mqtt/transport.js +246 -0
  99. package/dist/src/platforms/instagram/mqtt/transport.js.map +1 -0
  100. package/dist/src/platforms/instagram/realtime-listener.d.ts +33 -0
  101. package/dist/src/platforms/instagram/realtime-listener.d.ts.map +1 -0
  102. package/dist/src/platforms/instagram/realtime-listener.js +151 -0
  103. package/dist/src/platforms/instagram/realtime-listener.js.map +1 -0
  104. package/dist/src/platforms/kakaotalk/cli.js +1 -1
  105. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  106. package/dist/src/platforms/kakaotalk/client.js +4 -4
  107. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  108. package/dist/src/platforms/line/cli.js +1 -1
  109. package/dist/src/platforms/webex/cli.js +1 -1
  110. package/dist/src/platforms/webexbot/cli.js +1 -1
  111. package/dist/src/platforms/wechatbot/cli.js +1 -1
  112. package/dist/src/platforms/whatsapp/cli.js +1 -1
  113. package/dist/src/platforms/whatsappbot/cli.js +1 -1
  114. package/dist/src/tui/adapters/imessage-adapter.d.ts +21 -0
  115. package/dist/src/tui/adapters/imessage-adapter.d.ts.map +1 -0
  116. package/dist/src/tui/adapters/imessage-adapter.js +110 -0
  117. package/dist/src/tui/adapters/imessage-adapter.js.map +1 -0
  118. package/dist/src/tui/app.d.ts.map +1 -1
  119. package/dist/src/tui/app.js +9 -0
  120. package/dist/src/tui/app.js.map +1 -1
  121. package/docs/content/docs/agent-skills.mdx +1 -1
  122. package/docs/content/docs/cli/imessage.mdx +99 -0
  123. package/docs/content/docs/cli/instagram.mdx +1 -1
  124. package/docs/content/docs/cli/meta.json +1 -0
  125. package/docs/content/docs/index.mdx +9 -6
  126. package/docs/content/docs/quick-start.mdx +2 -0
  127. package/docs/content/docs/sdk/instagram.mdx +90 -5
  128. package/docs/content/docs/tui.mdx +4 -3
  129. package/package.json +10 -2
  130. package/scripts/postbuild.ts +12 -15
  131. package/skills/agent-channeltalk/SKILL.md +1 -1
  132. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  133. package/skills/agent-discord/SKILL.md +1 -1
  134. package/skills/agent-discordbot/SKILL.md +1 -1
  135. package/skills/agent-imessage/SKILL.md +133 -0
  136. package/skills/agent-imessage/references/authentication.md +66 -0
  137. package/skills/agent-imessage/references/permissions.md +31 -0
  138. package/skills/agent-imessage/references/setup.md +49 -0
  139. package/skills/agent-instagram/SKILL.md +72 -2
  140. package/skills/agent-instagram/references/common-patterns.md +36 -0
  141. package/skills/agent-instagram/templates/monitor-chat.sh +5 -0
  142. package/skills/agent-kakaotalk/SKILL.md +1 -1
  143. package/skills/agent-line/SKILL.md +1 -1
  144. package/skills/agent-slack/SKILL.md +1 -1
  145. package/skills/agent-slackbot/SKILL.md +1 -1
  146. package/skills/agent-teams/SKILL.md +1 -1
  147. package/skills/agent-telegram/SKILL.md +1 -1
  148. package/skills/agent-telegrambot/SKILL.md +1 -1
  149. package/skills/agent-webex/SKILL.md +1 -1
  150. package/skills/agent-webexbot/SKILL.md +1 -1
  151. package/skills/agent-wechatbot/SKILL.md +1 -1
  152. package/skills/agent-whatsapp/SKILL.md +1 -1
  153. package/skills/agent-whatsappbot/SKILL.md +1 -1
  154. package/src/cli.ts +4 -0
  155. package/src/platforms/imessage/cli.ts +39 -0
  156. package/src/platforms/imessage/client.test.ts +137 -0
  157. package/src/platforms/imessage/client.ts +253 -0
  158. package/src/platforms/imessage/commands/auth.ts +142 -0
  159. package/src/platforms/imessage/commands/chat.ts +51 -0
  160. package/src/platforms/imessage/commands/doctor.test.ts +80 -0
  161. package/src/platforms/imessage/commands/doctor.ts +139 -0
  162. package/src/platforms/imessage/commands/index.ts +6 -0
  163. package/src/platforms/imessage/commands/message.test.ts +20 -0
  164. package/src/platforms/imessage/commands/message.ts +151 -0
  165. package/src/platforms/imessage/commands/setup.test.ts +52 -0
  166. package/src/platforms/imessage/commands/setup.ts +76 -0
  167. package/src/platforms/imessage/commands/shared.test.ts +19 -0
  168. package/src/platforms/imessage/commands/shared.ts +69 -0
  169. package/src/platforms/imessage/commands/whoami.ts +22 -0
  170. package/src/platforms/imessage/credential-manager.test.ts +122 -0
  171. package/src/platforms/imessage/credential-manager.ts +123 -0
  172. package/src/platforms/imessage/ensure-auth.ts +18 -0
  173. package/src/platforms/imessage/errors.ts +27 -0
  174. package/src/platforms/imessage/index.test.ts +13 -0
  175. package/src/platforms/imessage/index.ts +16 -0
  176. package/src/platforms/imessage/rpc.ts +157 -0
  177. package/src/platforms/imessage/test-stub-imsg.mjs +147 -0
  178. package/src/platforms/imessage/types.test.ts +47 -0
  179. package/src/platforms/imessage/types.ts +95 -0
  180. package/src/platforms/instagram/client.ts +23 -0
  181. package/src/platforms/instagram/hybrid-listener.test.ts +135 -0
  182. package/src/platforms/instagram/hybrid-listener.ts +196 -0
  183. package/src/platforms/instagram/index.ts +10 -0
  184. package/src/platforms/instagram/mqtt/connection.ts +72 -0
  185. package/src/platforms/instagram/mqtt/thrift.test.ts +91 -0
  186. package/src/platforms/instagram/mqtt/thrift.ts +159 -0
  187. package/src/platforms/instagram/mqtt/transport.ts +285 -0
  188. package/src/platforms/instagram/realtime-listener.test.ts +34 -0
  189. package/src/platforms/instagram/realtime-listener.ts +201 -0
  190. package/src/platforms/kakaotalk/client.test.ts +40 -0
  191. package/src/platforms/kakaotalk/client.ts +6 -4
  192. package/src/platforms/webexbot/cli.ts +0 -0
  193. package/src/tui/adapters/imessage-adapter.ts +131 -0
  194. package/src/tui/app.ts +13 -0
@@ -0,0 +1,66 @@
1
+ # Authentication
2
+
3
+ iMessage authentication is unlike the other platforms: there are **no tokens, cookies, or login flow**. `agent-imessage` drives the local [imsg](https://github.com/openclaw/imsg) tool, which relies on **macOS permissions** (Full Disk Access + Automation) granted to the process that launches it. "Authentication" here means: imsg is installed, Messages.app is signed into your Apple ID, and the permissions are granted.
4
+
5
+ ## What gets stored
6
+
7
+ The credential manager stores an **account record with no secrets** in `~/.config/agent-messenger/imessage-credentials.json` (mode `0600`):
8
+
9
+ ```jsonc
10
+ {
11
+ "current": "default",
12
+ "accounts": {
13
+ "default": {
14
+ "account_id": "default",
15
+ "provider": "imsg", // backend-agnostic discriminator
16
+ "binary_path": "/opt/homebrew/bin/imsg", // optional; defaults to "imsg" on PATH
17
+ "region": "US", // optional; default region for local-format phone numbers
18
+ "created_at": "…",
19
+ "updated_at": "…"
20
+ }
21
+ }
22
+ }
23
+ ```
24
+
25
+ There is no password/token field — access is governed entirely by macOS TCC permissions.
26
+
27
+ ## Configuring an account
28
+
29
+ Guided:
30
+ ```bash
31
+ agent-imessage setup # verifies imsg + permissions (honors --bin/--region), then saves
32
+ ```
33
+
34
+ Scriptable:
35
+ ```bash
36
+ agent-imessage auth set --bin /opt/homebrew/bin/imsg --region US --current
37
+ agent-imessage auth status # via: agent-imessage doctor
38
+ agent-imessage auth list
39
+ agent-imessage auth use <account-id>
40
+ agent-imessage auth remove <account-id>
41
+ agent-imessage auth logout # clears all stored accounts
42
+ ```
43
+
44
+ ## Credential resolution order
45
+
46
+ When a command runs, the active configuration is resolved as:
47
+
48
+ 1. `--account <id>` (explicit account selection), then
49
+ 2. `AGENT_IMESSAGE_BIN` / `AGENT_IMESSAGE_REGION` environment variables (runtime override — **never persisted to disk**), then
50
+ 3. the stored **current** account, then
51
+ 4. defaults (`imsg` on PATH, no region).
52
+
53
+ If nothing resolves, commands exit with `{ "code": "not_authenticated" }` and point to `agent-imessage setup`.
54
+
55
+ ## Permissions (the real "auth")
56
+
57
+ imsg needs two macOS permissions, granted to the **parent process** (the terminal / agent runtime that launches `agent-messenger`), not to `imsg` in isolation:
58
+
59
+ - **Full Disk Access** — to read `~/Library/Messages/chat.db`. Missing → `{ "code": "full_disk_access" }`.
60
+ - **Automation → Messages** — to send. Missing → `{ "code": "automation_denied" }`.
61
+
62
+ These are user-consent TCC grants and **cannot be scripted**. See [permissions](permissions.md) for the exact steps and the parent-process caveat. Run `agent-imessage doctor` to check status.
63
+
64
+ ## Multi-account
65
+
66
+ Each account is an imsg configuration (binary path + default region). Messages.app has no from-selector, so **send is single-account per Mac**; multiple accounts are only meaningful if you run distinct imsg setups.
@@ -0,0 +1,31 @@
1
+ # macOS Permissions for imsg
2
+
3
+ `agent-imessage` drives [imsg](https://github.com/openclaw/imsg), which needs two macOS permissions. These are TCC (Transparency, Consent, and Control) grants that **cannot be scripted** — a human must approve them once in System Settings.
4
+
5
+ ## Full Disk Access (required for everything)
6
+
7
+ imsg reads the Messages database at `~/Library/Messages/chat.db`, which macOS protects behind Full Disk Access.
8
+
9
+ - **Grant to:** the app/terminal/agent-runtime that launches `agent-messenger` — **the parent process**, not `imsg` alone.
10
+ - **Where:** System Settings → Privacy & Security → Full Disk Access → enable your terminal/app → restart it.
11
+ - **Symptom when missing:** `agent-imessage doctor` reports `full_disk_access: denied`; commands return `{ "code": "full_disk_access" }`.
12
+
13
+ ## Automation → Messages (required to send)
14
+
15
+ Sending uses AppleScript automation of Messages.app.
16
+
17
+ - **Grant to:** same parent process.
18
+ - **Where:** System Settings → Privacy & Security → Automation → (your app) → enable **Messages**. macOS prompts automatically on the first send attempt.
19
+ - **Symptom when missing:** sends fail with `{ "code": "automation_denied" }`.
20
+
21
+ ## Contacts (optional)
22
+
23
+ Used only to resolve sender names. Nothing breaks without it; `from_name` is simply absent.
24
+
25
+ ## Why these can't be automated
26
+
27
+ TCC grants are deliberately user-consent-only. `tccutil` can reset permissions but cannot grant them, and the TCC database is protected by SIP. There is no supported way to grant Full Disk Access or Automation programmatically on an unmanaged Mac — plan for a one-time manual approval (a quick Screen Sharing/VNC session works for a headless Mac).
28
+
29
+ ## SIP and the advanced bridge
30
+
31
+ Core features (send/read/watch/standard tapbacks) need **no SIP changes**. Only the advanced bridge (`imsg launch`: typing, edit/unsend, group management, targeted/custom reactions) requires **disabling SIP**, which is a separate, deliberate step and out of scope for the core `agent-imessage` features.
@@ -0,0 +1,49 @@
1
+ # imsg Setup (Operator Guide)
2
+
3
+ iMessage has no public API. `agent-imessage` runs **on a Mac** and uses [imsg](https://github.com/openclaw/imsg) — a native Swift CLI — to read the local Messages database and send through Messages.app. The agent must run on the same Mac (imsg is local-only; there is no network/server).
4
+
5
+ ## Requirements
6
+
7
+ - **macOS 14+** (Sonoma or newer). imsg supports macOS 26/Tahoe with caveats (group sends via AppleScript can fail; imsg reports the failure honestly rather than lying).
8
+ - **Messages.app signed in** to your Apple ID. A **dedicated Apple ID** is recommended to avoid mixing automated traffic with your personal account.
9
+ - **imsg installed**: `brew install steipete/tap/imsg`.
10
+ - **Full Disk Access** and **Automation → Messages** permissions (see below).
11
+
12
+ ## One-time setup
13
+
14
+ 1. **Install imsg**
15
+ ```bash
16
+ brew install steipete/tap/imsg
17
+ imsg --version
18
+ ```
19
+ 2. **Sign Messages.app into iMessage** with your (dedicated) Apple ID.
20
+ 3. **Grant Full Disk Access** — System Settings → Privacy & Security → Full Disk Access → add the app/terminal that will launch `agent-messenger`. This is required to read `~/Library/Messages/chat.db`.
21
+ 4. **Grant Automation → Messages** — triggered on first send; approve the prompt (System Settings → Privacy & Security → Automation → Messages).
22
+ 5. **Verify and save an account**
23
+ ```bash
24
+ agent-imessage setup # guided: checks imsg + permissions, saves account
25
+ agent-imessage doctor # re-check anytime
26
+ ```
27
+
28
+ ### The parent-process permission caveat (important)
29
+
30
+ macOS grants Full Disk Access and Automation to the **process that launches imsg** — i.e. the terminal, IDE, or agent runtime that runs `agent-imessage`, not `imsg` in isolation. If `doctor` reports `full_disk_access: denied`, grant Full Disk Access to **that launching app** and restart it.
31
+
32
+ ## Feature tiers
33
+
34
+ - **Core (no SIP):** list/search chats, read history, send text, watch new messages, standard tapbacks (to the most recent incoming message). This is all that `agent-imessage` v1 uses.
35
+ - **Bridge (advanced):** typing indicators, read receipts, edit/unsend, group create/rename/members, and reacting to a *specific* message or with custom emoji. These require `imsg launch` (which injects into IMCore) and **disabling SIP**. Not used by v1; documented for completeness.
36
+
37
+ ## Multi-account
38
+
39
+ Each `agent-imessage` account is an imsg configuration (binary path + default region). Messages.app has no from-selector, so **send is single-account** per Mac. Configure additional accounts only if you run multiple imsg binaries/setups:
40
+ ```bash
41
+ agent-imessage auth set --bin /opt/homebrew/bin/imsg --region US --account home --current
42
+ agent-imessage auth use home
43
+ ```
44
+
45
+ ## Reliability notes
46
+
47
+ - The Mac must stay awake and logged in for live `watch` to keep receiving.
48
+ - imsg's watcher uses filesystem events + polling and re-arms across Messages database (WAL) rotation, so it keeps up on busy/iCloud-synced databases.
49
+ - Pin macOS updates if stability matters — major updates can change AppleScript/IMCore behavior.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-instagram
3
3
  description: Interact with Instagram DMs - send messages, read conversations, manage accounts
4
- version: 2.27.1
4
+ version: 2.28.0
5
5
  allowed-tools: Bash(agent-instagram:*)
6
6
  metadata:
7
7
  openclaw:
@@ -27,7 +27,7 @@ Use one of these entrypoints:
27
27
  - **Thread ID** = Instagram's identifier for a DM conversation. Numeric string returned by `chat list`.
28
28
  - **Browser cookie extraction** = recommended auth method. Extracts cookies from Chromium browsers (Chrome, Chrome Canary, Edge, Arc, Brave, Vivaldi, Chromium) where you're logged into instagram.com. Zero-config.
29
29
  - **Username/password auth** = fallback method. Authenticates using Instagram credentials. Supports two-factor authentication via SMS or authenticator app.
30
- - **HTTP-based** = each command makes HTTP requests and returns. No persistent connection or background process.
30
+ - **HTTP-based** = each CLI command makes HTTP requests and returns. No persistent connection or background process. The SDK additionally offers real-time listeners (`InstagramHybridListener`, `InstagramRealtimeListener`) for streaming DMs — see [SDK: Real-Time Events](#sdk-real-time-events) below.
31
31
  - **Multi-account** = multiple Instagram accounts can be stored. Use `auth list` and `auth use` to switch between them.
32
32
  - **DM-only** = this CLI focuses on Instagram Direct Messages. It does not manage posts, stories, or followers.
33
33
 
@@ -440,3 +440,73 @@ Instagram invalidates sessions based on device fingerprinting and activity patte
440
440
  1. Avoid frequent re-authentication
441
441
  2. Space out API calls
442
442
  3. Don't switch between multiple IP addresses rapidly
443
+
444
+ ## SDK: Real-Time Events
445
+
446
+ `InstagramHybridListener` connects over Instagram's MQTToT transport (a persistent TLS connection to `edge-mqtt.facebook.com`) and falls back to polling automatically if the connection fails. It retries realtime with capped exponential backoff. This is the recommended listener for most use cases.
447
+
448
+ `InstagramRealtimeListener` is the pure-realtime option with no polling fallback — use it when you want direct MQTToT and prefer to handle reconnection yourself.
449
+
450
+ Both use the unofficial private API, the same risk surface as the existing polling listener.
451
+
452
+ ### Setup
453
+
454
+ ```typescript
455
+ import { InstagramClient, InstagramHybridListener } from 'agent-messenger/instagram'
456
+
457
+ const client = await new InstagramClient().login()
458
+ const listener = new InstagramHybridListener(client, {
459
+ pollInterval: 5000, // fallback poll interval in ms
460
+ realtimeRetryBaseMs: 2000, // backoff base for realtime reconnect
461
+ realtimeRetryMaxMs: 60000, // backoff cap
462
+ })
463
+ ```
464
+
465
+ Or for pure realtime:
466
+
467
+ ```typescript
468
+ import { InstagramClient, InstagramRealtimeListener } from 'agent-messenger/instagram'
469
+
470
+ const client = await new InstagramClient().login()
471
+ const listener = new InstagramRealtimeListener(client)
472
+ ```
473
+
474
+ ### Listening for Events
475
+
476
+ ```typescript
477
+ listener.on('connected', ({ userId, transport }) => {
478
+ // transport is 'realtime' or 'polling' (hybrid only)
479
+ console.log(`Listening as ${userId} via ${transport}`)
480
+ })
481
+
482
+ listener.on('message', (msg) => {
483
+ // msg.id, msg.thread_id, msg.from, msg.timestamp
484
+ // msg.is_outgoing, msg.type, msg.text, msg.media_url
485
+ if (msg.is_outgoing) return
486
+ console.log(`[${msg.thread_id}] ${msg.from}: ${msg.text}`)
487
+ })
488
+
489
+ listener.on('error', (err) => {
490
+ console.error(err.message)
491
+ })
492
+
493
+ listener.on('disconnected', () => {
494
+ // hybrid listener retries realtime automatically
495
+ })
496
+ ```
497
+
498
+ ### Lifecycle
499
+
500
+ ```typescript
501
+ await listener.start() // connects via MQTToT (or starts polling fallback)
502
+ listener.stop() // clean shutdown
503
+ ```
504
+
505
+ ### Event Types
506
+
507
+ | Event | Payload (Hybrid) | Payload (Realtime / Polling) | Description |
508
+ | -------------- | --------------------------------------------------------- | ---------------------------- | ------------------------------ |
509
+ | `message` | `InstagramMessageSummary` | `InstagramMessageSummary` | New DM received |
510
+ | `connected` | `{ userId: string; transport: 'realtime' \| 'polling' }` | `{ userId: string }` | Listener active |
511
+ | `disconnected` | — | — | Connection closed |
512
+ | `error` | `Error` | `Error` | Connection or API error |
@@ -323,6 +323,39 @@ agent-instagram message send-to alice "Hello!"
323
323
  agent-instagram message send 12345678901 "Hello!"
324
324
  ```
325
325
 
326
+ ## Pattern 11: Real-Time DM Monitor (SDK)
327
+
328
+ **Use case**: Receive new DMs as they arrive, without polling
329
+
330
+ The SDK's `InstagramHybridListener` is the right tool for continuous DM monitoring. It connects over Instagram's MQTToT transport and falls back to polling automatically — no manual loop or `sleep` needed.
331
+
332
+ ```typescript
333
+ import { InstagramClient, InstagramHybridListener } from 'agent-messenger/instagram'
334
+
335
+ const client = await new InstagramClient().login()
336
+ const listener = new InstagramHybridListener(client)
337
+
338
+ listener.on('connected', ({ userId, transport }) => {
339
+ console.log(`Listening as ${userId} via ${transport}`)
340
+ })
341
+
342
+ listener.on('message', (msg) => {
343
+ if (msg.is_outgoing) return
344
+ console.log(`[${msg.thread_id}] ${msg.from}: ${msg.text ?? `[${msg.type}]`}`)
345
+ })
346
+
347
+ listener.on('error', (err) => {
348
+ console.error('Listener error:', err.message)
349
+ })
350
+
351
+ await listener.start()
352
+ // Runs until listener.stop() is called
353
+ ```
354
+
355
+ **When to use**: Any time you need to react to incoming DMs in real time — notifications, bots, monitoring dashboards. Prefer this over a polling shell loop.
356
+
357
+ **Contrast with shell polling**: The `monitor-chat.sh` template uses `sleep` + `message list` in a loop. That works for simple scripts but makes one HTTP request per interval and can't react faster than the interval. The SDK listener holds a persistent connection and delivers messages as they arrive.
358
+
326
359
  ## Anti-Patterns
327
360
 
328
361
  ### Don't Poll Too Frequently
@@ -341,6 +374,8 @@ while true; do
341
374
  done
342
375
  ```
343
376
 
377
+ For real-time monitoring, the SDK's `InstagramHybridListener` is a better alternative — it holds a persistent connection and avoids repeated HTTP requests entirely. See [Pattern 11](#pattern-11-real-time-dm-monitor-sdk) above.
378
+
344
379
  ### Don't Spam Threads
345
380
 
346
381
  ```bash
@@ -360,3 +395,4 @@ agent-instagram message send "$THREAD" "$MESSAGE"
360
395
  ## See Also
361
396
 
362
397
  - [Authentication Guide](authentication.md) - Setting up credentials
398
+ - For real-time events, use the `InstagramHybridListener` SDK (see SKILL.md → SDK: Real-Time Events)
@@ -12,6 +12,11 @@
12
12
  # Example:
13
13
  # ./monitor-chat.sh 340282366841710300949128138443434234567
14
14
  # ./monitor-chat.sh 340282366841710300949128138443434234567 10
15
+ #
16
+ # NOTE: For real-time monitoring, prefer the SDK's InstagramHybridListener over
17
+ # this poll-based approach. It connects over Instagram's MQTToT transport and
18
+ # delivers messages as they arrive, with automatic fallback to polling if needed.
19
+ # See SKILL.md -> SDK: Real-Time Events for setup and usage.
15
20
 
16
21
  set -euo pipefail
17
22
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-kakaotalk
3
3
  description: Interact with KakaoTalk - send messages, read chats, manage conversations
4
- version: 2.27.1
4
+ version: 2.28.0
5
5
  allowed-tools: Bash(agent-kakaotalk:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-line
3
3
  description: Interact with LINE - send messages, read chats, manage conversations
4
- version: 2.27.1
4
+ version: 2.28.0
5
5
  allowed-tools: Bash(agent-line:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-slack
3
3
  description: Interact with Slack workspaces - send messages, read channels, manage reactions
4
- version: 2.27.1
4
+ version: 2.28.0
5
5
  allowed-tools: Bash(agent-slack:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-slackbot
3
3
  description: Interact with Slack workspaces using bot tokens - send messages, read channels, manage reactions
4
- version: 2.27.1
4
+ version: 2.28.0
5
5
  allowed-tools: Bash(agent-slackbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-teams
3
3
  description: Interact with Microsoft Teams - send messages, read channels, manage reactions
4
- version: 2.27.1
4
+ version: 2.28.0
5
5
  allowed-tools: Bash(agent-teams:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-telegram
3
3
  description: Interact with Telegram through TDLib - authenticate, inspect chats, and send messages
4
- version: 2.27.1
4
+ version: 2.28.0
5
5
  allowed-tools: Bash(agent-telegram:*)
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-telegrambot
3
3
  description: Interact with Telegram using bot tokens - send messages, read chats, manage reactions
4
- version: 2.27.1
4
+ version: 2.28.0
5
5
  allowed-tools: Bash(agent-telegrambot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-webex
3
3
  description: Interact with Cisco Webex - send messages, read spaces, manage memberships
4
- version: 2.27.1
4
+ version: 2.28.0
5
5
  allowed-tools: Bash(agent-webex:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-webexbot
3
3
  description: Interact with Cisco Webex using bot tokens - send messages, reply in threads, upload and download files, look up people, read spaces, manage memberships, stream real-time events
4
- version: 2.27.1
4
+ version: 2.28.0
5
5
  allowed-tools: Bash(agent-webexbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-wechatbot
3
3
  description: Interact with WeChat Official Account using API credentials - send messages, manage templates, list followers
4
- version: 2.27.1
4
+ version: 2.28.0
5
5
  allowed-tools: Bash(agent-wechatbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-whatsapp
3
3
  description: Interact with WhatsApp - send messages, read chats, manage conversations
4
- version: 2.27.1
4
+ version: 2.28.0
5
5
  allowed-tools: Bash(agent-whatsapp:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-whatsappbot
3
3
  description: Interact with WhatsApp using Cloud API credentials - send messages, manage templates
4
- version: 2.27.1
4
+ version: 2.28.0
5
5
  allowed-tools: Bash(agent-whatsappbot:*)
6
6
  metadata:
7
7
  openclaw:
package/src/cli.ts CHANGED
@@ -68,6 +68,10 @@ program.command('channeltalk', 'Interact with Channel Talk', {
68
68
  executableFile: join(__dirname, 'platforms', 'channeltalk', `cli${ext}`),
69
69
  })
70
70
 
71
+ program.command('imessage', 'Interact with iMessage via imsg (local, on-Mac)', {
72
+ executableFile: join(__dirname, 'platforms', 'imessage', `cli${ext}`),
73
+ })
74
+
71
75
  program.command('channeltalkbot', 'Interact with Channel Talk using API credentials', {
72
76
  executableFile: join(__dirname, 'platforms', 'channeltalkbot', `cli${ext}`),
73
77
  })
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import type { Command as CommandType } from 'commander'
4
+ import { Command } from 'commander'
5
+
6
+ import pkg from '../../../package.json' with { type: 'json' }
7
+ import { authCommand, chatCommand, doctorCommand, messageCommand, setupCommand, whoamiCommand } from './commands/index'
8
+ import { ensureIMessageAuth } from './ensure-auth'
9
+
10
+ const UNGUARDED = new Set(['auth', 'setup', 'doctor'])
11
+
12
+ function isUnguarded(command: CommandType): boolean {
13
+ let cmd: CommandType | null = command
14
+ while (cmd) {
15
+ if (UNGUARDED.has(cmd.name())) return true
16
+ cmd = cmd.parent
17
+ }
18
+ return false
19
+ }
20
+
21
+ const program = new Command()
22
+
23
+ program.name('agent-imessage').description('CLI tool for iMessage via imsg (local, on-Mac)').version(pkg.version)
24
+
25
+ program.hook('preAction', async (_thisCommand, actionCommand) => {
26
+ if (isUnguarded(actionCommand)) return
27
+ await ensureIMessageAuth()
28
+ })
29
+
30
+ program.addCommand(authCommand)
31
+ program.addCommand(setupCommand)
32
+ program.addCommand(doctorCommand)
33
+ program.addCommand(chatCommand)
34
+ program.addCommand(messageCommand)
35
+ program.addCommand(whoamiCommand)
36
+
37
+ program.parse(process.argv)
38
+
39
+ export default program
@@ -0,0 +1,137 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { join } from 'node:path'
3
+
4
+ import { ImsgClient } from './client'
5
+ import { IMessageError } from './types'
6
+
7
+ const STUB = join(import.meta.dir, 'test-stub-imsg.mjs')
8
+
9
+ function clientWith(mode = 'ok'): ImsgClient {
10
+ process.env.IMSG_STUB_MODE = mode
11
+ return new ImsgClient()
12
+ }
13
+
14
+ describe('ImsgClient (against stub imsg binary)', () => {
15
+ it('getVersion returns the imsg version', async () => {
16
+ const c = await clientWith().login({ binaryPath: STUB })
17
+ expect(await c.getVersion()).toBe('0.11.1')
18
+ await c.close()
19
+ })
20
+
21
+ it('connect succeeds and listChats maps group/individual + fields', async () => {
22
+ const c = await clientWith().login({ binaryPath: STUB })
23
+ await c.connect()
24
+ const chats = await c.listChats(10)
25
+ expect(chats[0]).toMatchObject({ id: 42, name: 'Jane', is_group: false, service: 'iMessage' })
26
+ expect(chats[1]).toMatchObject({ id: 43, name: 'Crew', is_group: true })
27
+ expect(chats[1]?.participants).toHaveLength(2)
28
+ await c.close()
29
+ })
30
+
31
+ it('connect throws full_disk_access when the DB is not readable', async () => {
32
+ const c = await clientWith('fda_denied').login({ binaryPath: STUB })
33
+ try {
34
+ await c.connect()
35
+ throw new Error('should have thrown')
36
+ } catch (error) {
37
+ expect((error as IMessageError).code).toBe('full_disk_access')
38
+ expect((error as IMessageError).suggestion).toContain('Full Disk Access')
39
+ } finally {
40
+ await c.close()
41
+ }
42
+ })
43
+
44
+ it('getMessages maps fields oldest-first with is_outgoing', async () => {
45
+ const c = await clientWith().login({ binaryPath: STUB })
46
+ await c.connect()
47
+ const msgs = await c.getMessages(42, 10)
48
+ expect(msgs.map((m) => m.guid)).toEqual(['m1', 'm2'])
49
+ expect(msgs[0]?.is_outgoing).toBe(false)
50
+ expect(msgs[1]?.is_outgoing).toBe(true)
51
+ await c.close()
52
+ })
53
+
54
+ it('sendMessage returns guid/id; send failure maps to send_failed', async () => {
55
+ const ok = await clientWith('ok').login({ binaryPath: STUB })
56
+ await ok.connect()
57
+ expect(await ok.sendMessage({ chatId: 42 }, 'hi')).toMatchObject({ guid: 'sent-guid', id: 99, is_outgoing: true })
58
+ await ok.close()
59
+
60
+ const fail = await clientWith('send_fail').login({ binaryPath: STUB })
61
+ await fail.connect()
62
+ try {
63
+ await fail.sendMessage({ chatId: 42 }, 'hi')
64
+ throw new Error('should have thrown')
65
+ } catch (error) {
66
+ expect((error as IMessageError).code).toBe('send_failed')
67
+ } finally {
68
+ await fail.close()
69
+ }
70
+ })
71
+
72
+ it('watch delivers live notifications', async () => {
73
+ const c = await clientWith().login({ binaryPath: STUB })
74
+ await c.connect()
75
+ const received: string[] = []
76
+ const stop = await c.watch((m) => received.push(m.guid), { chatId: 42 })
77
+ await new Promise((r) => setTimeout(r, 120))
78
+ await stop()
79
+ await c.close()
80
+ expect(received).toContain('m3')
81
+ })
82
+
83
+ it('sendReaction: standard via CLI; custom + message-target rejected', async () => {
84
+ const c = await clientWith().login({ binaryPath: STUB })
85
+ await c.connect()
86
+ await expect(c.sendReaction(42, 'love')).resolves.toBeUndefined()
87
+ await expect(c.sendReaction(42, 'sparkles')).rejects.toMatchObject({ code: 'private_api_required' })
88
+ await expect(c.sendReaction(42, 'love', 'some-guid')).rejects.toMatchObject({ code: 'private_api_required' })
89
+ await c.close()
90
+ })
91
+
92
+ it('maps an Automation-denied "imsg react" failure to automation_denied (not send_failed)', async () => {
93
+ const c = await clientWith('react_automation_denied').login({ binaryPath: STUB })
94
+ await c.connect()
95
+ try {
96
+ await c.sendReaction(42, 'love')
97
+ throw new Error('should have thrown')
98
+ } catch (error) {
99
+ expect((error as IMessageError).code).toBe('automation_denied')
100
+ } finally {
101
+ await c.close()
102
+ }
103
+ })
104
+
105
+ it('maps AppleScript "not authorized" send error to automation_denied (not send_failed)', async () => {
106
+ const c = await clientWith('automation_denied').login({ binaryPath: STUB })
107
+ await c.connect()
108
+ try {
109
+ await c.sendMessage({ chatId: 42 }, 'hi')
110
+ throw new Error('should have thrown')
111
+ } catch (error) {
112
+ expect((error as IMessageError).code).toBe('automation_denied')
113
+ } finally {
114
+ await c.close()
115
+ }
116
+ })
117
+
118
+ it('close() rejects in-flight requests instead of hanging', async () => {
119
+ const c = await clientWith().login({ binaryPath: STUB })
120
+ await c.connect()
121
+ const pending = c.listChats(1)
122
+ await c.close()
123
+ await expect(pending).rejects.toMatchObject({ code: 'rpc_error' })
124
+ })
125
+
126
+ it('imsg_not_found when the binary does not exist', async () => {
127
+ const c = await new ImsgClient().login({ binaryPath: '/nonexistent/imsg-xyz' })
128
+ try {
129
+ await c.connect()
130
+ throw new Error('should have thrown')
131
+ } catch (error) {
132
+ expect((error as IMessageError).code).toBe('imsg_not_found')
133
+ } finally {
134
+ await c.close()
135
+ }
136
+ })
137
+ })