agent-messenger 2.27.2 → 2.29.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 (316) 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 +58 -10
  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/discord/client.d.ts +6 -2
  11. package/dist/src/platforms/discord/client.d.ts.map +1 -1
  12. package/dist/src/platforms/discord/client.js +10 -3
  13. package/dist/src/platforms/discord/client.js.map +1 -1
  14. package/dist/src/platforms/discordbot/cli.js +1 -1
  15. package/dist/src/platforms/imessage/cli.d.ts +5 -0
  16. package/dist/src/platforms/imessage/cli.d.ts.map +1 -0
  17. package/dist/src/platforms/imessage/cli.js +31 -0
  18. package/dist/src/platforms/imessage/cli.js.map +1 -0
  19. package/dist/src/platforms/imessage/client.d.ts +43 -0
  20. package/dist/src/platforms/imessage/client.d.ts.map +1 -0
  21. package/dist/src/platforms/imessage/client.js +190 -0
  22. package/dist/src/platforms/imessage/client.js.map +1 -0
  23. package/dist/src/platforms/imessage/commands/auth.d.ts +31 -0
  24. package/dist/src/platforms/imessage/commands/auth.d.ts.map +1 -0
  25. package/dist/src/platforms/imessage/commands/auth.js +100 -0
  26. package/dist/src/platforms/imessage/commands/auth.js.map +1 -0
  27. package/dist/src/platforms/imessage/commands/chat.d.ts +3 -0
  28. package/dist/src/platforms/imessage/commands/chat.d.ts.map +1 -0
  29. package/dist/src/platforms/imessage/commands/chat.js +42 -0
  30. package/dist/src/platforms/imessage/commands/chat.js.map +1 -0
  31. package/dist/src/platforms/imessage/commands/doctor.d.ts +25 -0
  32. package/dist/src/platforms/imessage/commands/doctor.d.ts.map +1 -0
  33. package/dist/src/platforms/imessage/commands/doctor.js +101 -0
  34. package/dist/src/platforms/imessage/commands/doctor.js.map +1 -0
  35. package/dist/src/platforms/imessage/commands/index.d.ts +7 -0
  36. package/dist/src/platforms/imessage/commands/index.d.ts.map +1 -0
  37. package/dist/src/platforms/imessage/commands/index.js +7 -0
  38. package/dist/src/platforms/imessage/commands/index.js.map +1 -0
  39. package/dist/src/platforms/imessage/commands/message.d.ts +5 -0
  40. package/dist/src/platforms/imessage/commands/message.d.ts.map +1 -0
  41. package/dist/src/platforms/imessage/commands/message.js +127 -0
  42. package/dist/src/platforms/imessage/commands/message.js.map +1 -0
  43. package/dist/src/platforms/imessage/commands/setup.d.ts +20 -0
  44. package/dist/src/platforms/imessage/commands/setup.d.ts.map +1 -0
  45. package/dist/src/platforms/imessage/commands/setup.js +57 -0
  46. package/dist/src/platforms/imessage/commands/setup.js.map +1 -0
  47. package/dist/src/platforms/imessage/commands/shared.d.ts +10 -0
  48. package/dist/src/platforms/imessage/commands/shared.d.ts.map +1 -0
  49. package/dist/src/platforms/imessage/commands/shared.js +53 -0
  50. package/dist/src/platforms/imessage/commands/shared.js.map +1 -0
  51. package/dist/src/platforms/imessage/commands/whoami.d.ts +3 -0
  52. package/dist/src/platforms/imessage/commands/whoami.d.ts.map +1 -0
  53. package/dist/src/platforms/imessage/commands/whoami.js +20 -0
  54. package/dist/src/platforms/imessage/commands/whoami.js.map +1 -0
  55. package/dist/src/platforms/imessage/credential-manager.d.ts +22 -0
  56. package/dist/src/platforms/imessage/credential-manager.d.ts.map +1 -0
  57. package/dist/src/platforms/imessage/credential-manager.js +111 -0
  58. package/dist/src/platforms/imessage/credential-manager.js.map +1 -0
  59. package/dist/src/platforms/imessage/ensure-auth.d.ts +2 -0
  60. package/dist/src/platforms/imessage/ensure-auth.d.ts.map +1 -0
  61. package/dist/src/platforms/imessage/ensure-auth.js +13 -0
  62. package/dist/src/platforms/imessage/ensure-auth.js.map +1 -0
  63. package/dist/src/platforms/imessage/errors.d.ts +3 -0
  64. package/dist/src/platforms/imessage/errors.d.ts.map +1 -0
  65. package/dist/src/platforms/imessage/errors.js +25 -0
  66. package/dist/src/platforms/imessage/errors.js.map +1 -0
  67. package/dist/src/platforms/imessage/index.d.ts +7 -0
  68. package/dist/src/platforms/imessage/index.d.ts.map +1 -0
  69. package/dist/src/platforms/imessage/index.js +5 -0
  70. package/dist/src/platforms/imessage/index.js.map +1 -0
  71. package/dist/src/platforms/imessage/rpc.d.ts +20 -0
  72. package/dist/src/platforms/imessage/rpc.d.ts.map +1 -0
  73. package/dist/src/platforms/imessage/rpc.js +133 -0
  74. package/dist/src/platforms/imessage/rpc.js.map +1 -0
  75. package/dist/src/platforms/imessage/types.d.ts +59 -0
  76. package/dist/src/platforms/imessage/types.d.ts.map +1 -0
  77. package/dist/src/platforms/imessage/types.js +29 -0
  78. package/dist/src/platforms/imessage/types.js.map +1 -0
  79. package/dist/src/platforms/instagram/cli.js +1 -1
  80. package/dist/src/platforms/instagram/client.d.ts +29 -2
  81. package/dist/src/platforms/instagram/client.d.ts.map +1 -1
  82. package/dist/src/platforms/instagram/client.js +360 -53
  83. package/dist/src/platforms/instagram/client.js.map +1 -1
  84. package/dist/src/platforms/instagram/commands/auth.d.ts +1 -0
  85. package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
  86. package/dist/src/platforms/instagram/commands/auth.js +123 -11
  87. package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
  88. package/dist/src/platforms/instagram/credential-manager.d.ts +4 -1
  89. package/dist/src/platforms/instagram/credential-manager.d.ts.map +1 -1
  90. package/dist/src/platforms/instagram/credential-manager.js +17 -1
  91. package/dist/src/platforms/instagram/credential-manager.js.map +1 -1
  92. package/dist/src/platforms/instagram/hybrid-listener.d.ts +48 -0
  93. package/dist/src/platforms/instagram/hybrid-listener.d.ts.map +1 -0
  94. package/dist/src/platforms/instagram/hybrid-listener.js +154 -0
  95. package/dist/src/platforms/instagram/hybrid-listener.js.map +1 -0
  96. package/dist/src/platforms/instagram/index.d.ts +2 -0
  97. package/dist/src/platforms/instagram/index.d.ts.map +1 -1
  98. package/dist/src/platforms/instagram/index.js +2 -0
  99. package/dist/src/platforms/instagram/index.js.map +1 -1
  100. package/dist/src/platforms/instagram/mqtt/connection.d.ts +4 -0
  101. package/dist/src/platforms/instagram/mqtt/connection.d.ts.map +1 -0
  102. package/dist/src/platforms/instagram/mqtt/connection.js +66 -0
  103. package/dist/src/platforms/instagram/mqtt/connection.js.map +1 -0
  104. package/dist/src/platforms/instagram/mqtt/thrift.d.ts +22 -0
  105. package/dist/src/platforms/instagram/mqtt/thrift.d.ts.map +1 -0
  106. package/dist/src/platforms/instagram/mqtt/thrift.js +143 -0
  107. package/dist/src/platforms/instagram/mqtt/thrift.js.map +1 -0
  108. package/dist/src/platforms/instagram/mqtt/transport.d.ts +42 -0
  109. package/dist/src/platforms/instagram/mqtt/transport.d.ts.map +1 -0
  110. package/dist/src/platforms/instagram/mqtt/transport.js +246 -0
  111. package/dist/src/platforms/instagram/mqtt/transport.js.map +1 -0
  112. package/dist/src/platforms/instagram/realtime-listener.d.ts +33 -0
  113. package/dist/src/platforms/instagram/realtime-listener.d.ts.map +1 -0
  114. package/dist/src/platforms/instagram/realtime-listener.js +151 -0
  115. package/dist/src/platforms/instagram/realtime-listener.js.map +1 -0
  116. package/dist/src/platforms/instagram/types.d.ts +10 -8
  117. package/dist/src/platforms/instagram/types.d.ts.map +1 -1
  118. package/dist/src/platforms/instagram/types.js.map +1 -1
  119. package/dist/src/platforms/kakaotalk/cli.js +1 -1
  120. package/dist/src/platforms/line/cli.js +1 -1
  121. package/dist/src/platforms/teams/app-config.d.ts +36 -0
  122. package/dist/src/platforms/teams/app-config.d.ts.map +1 -0
  123. package/dist/src/platforms/teams/app-config.js +69 -0
  124. package/dist/src/platforms/teams/app-config.js.map +1 -0
  125. package/dist/src/platforms/teams/client.d.ts +19 -2
  126. package/dist/src/platforms/teams/client.d.ts.map +1 -1
  127. package/dist/src/platforms/teams/client.js +295 -7
  128. package/dist/src/platforms/teams/client.js.map +1 -1
  129. package/dist/src/platforms/teams/commands/auth.d.ts +10 -0
  130. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
  131. package/dist/src/platforms/teams/commands/auth.js +137 -3
  132. package/dist/src/platforms/teams/commands/auth.js.map +1 -1
  133. package/dist/src/platforms/teams/commands/file.d.ts +3 -0
  134. package/dist/src/platforms/teams/commands/file.d.ts.map +1 -1
  135. package/dist/src/platforms/teams/commands/file.js +59 -2
  136. package/dist/src/platforms/teams/commands/file.js.map +1 -1
  137. package/dist/src/platforms/teams/commands/message.d.ts +11 -0
  138. package/dist/src/platforms/teams/commands/message.d.ts.map +1 -1
  139. package/dist/src/platforms/teams/commands/message.js +121 -2
  140. package/dist/src/platforms/teams/commands/message.js.map +1 -1
  141. package/dist/src/platforms/teams/credential-manager.d.ts +23 -1
  142. package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
  143. package/dist/src/platforms/teams/credential-manager.js +46 -2
  144. package/dist/src/platforms/teams/credential-manager.js.map +1 -1
  145. package/dist/src/platforms/teams/device-code.d.ts +48 -0
  146. package/dist/src/platforms/teams/device-code.d.ts.map +1 -0
  147. package/dist/src/platforms/teams/device-code.js +199 -0
  148. package/dist/src/platforms/teams/device-code.js.map +1 -0
  149. package/dist/src/platforms/teams/device-login.d.ts +37 -0
  150. package/dist/src/platforms/teams/device-login.d.ts.map +1 -0
  151. package/dist/src/platforms/teams/device-login.js +129 -0
  152. package/dist/src/platforms/teams/device-login.js.map +1 -0
  153. package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
  154. package/dist/src/platforms/teams/ensure-auth.js +12 -0
  155. package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
  156. package/dist/src/platforms/teams/index.d.ts +7 -3
  157. package/dist/src/platforms/teams/index.d.ts.map +1 -1
  158. package/dist/src/platforms/teams/index.js +5 -2
  159. package/dist/src/platforms/teams/index.js.map +1 -1
  160. package/dist/src/platforms/teams/listener.d.ts +35 -0
  161. package/dist/src/platforms/teams/listener.d.ts.map +1 -0
  162. package/dist/src/platforms/teams/listener.js +236 -0
  163. package/dist/src/platforms/teams/listener.js.map +1 -0
  164. package/dist/src/platforms/teams/realm-discovery.d.ts +3 -0
  165. package/dist/src/platforms/teams/realm-discovery.d.ts.map +1 -0
  166. package/dist/src/platforms/teams/realm-discovery.js +33 -0
  167. package/dist/src/platforms/teams/realm-discovery.js.map +1 -0
  168. package/dist/src/platforms/teams/token-extractor.d.ts +3 -0
  169. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  170. package/dist/src/platforms/teams/token-extractor.js +60 -0
  171. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  172. package/dist/src/platforms/teams/token-provider.d.ts +25 -0
  173. package/dist/src/platforms/teams/token-provider.d.ts.map +1 -0
  174. package/dist/src/platforms/teams/token-provider.js +190 -0
  175. package/dist/src/platforms/teams/token-provider.js.map +1 -0
  176. package/dist/src/platforms/teams/trouter.d.ts +31 -0
  177. package/dist/src/platforms/teams/trouter.d.ts.map +1 -0
  178. package/dist/src/platforms/teams/trouter.js +181 -0
  179. package/dist/src/platforms/teams/trouter.js.map +1 -0
  180. package/dist/src/platforms/teams/types.d.ts +94 -0
  181. package/dist/src/platforms/teams/types.d.ts.map +1 -1
  182. package/dist/src/platforms/teams/types.js +30 -0
  183. package/dist/src/platforms/teams/types.js.map +1 -1
  184. package/dist/src/platforms/webex/cli.js +1 -1
  185. package/dist/src/platforms/webex/types.d.ts +1 -1
  186. package/dist/src/platforms/webexbot/cli.js +1 -1
  187. package/dist/src/platforms/wechatbot/cli.js +1 -1
  188. package/dist/src/platforms/whatsapp/cli.js +1 -1
  189. package/dist/src/platforms/whatsappbot/cli.js +1 -1
  190. package/dist/src/tui/adapters/imessage-adapter.d.ts +21 -0
  191. package/dist/src/tui/adapters/imessage-adapter.d.ts.map +1 -0
  192. package/dist/src/tui/adapters/imessage-adapter.js +110 -0
  193. package/dist/src/tui/adapters/imessage-adapter.js.map +1 -0
  194. package/dist/src/tui/adapters/instagram-adapter.d.ts.map +1 -1
  195. package/dist/src/tui/adapters/instagram-adapter.js +16 -0
  196. package/dist/src/tui/adapters/instagram-adapter.js.map +1 -1
  197. package/dist/src/tui/adapters/teams-adapter.d.ts +3 -0
  198. package/dist/src/tui/adapters/teams-adapter.d.ts.map +1 -1
  199. package/dist/src/tui/adapters/teams-adapter.js +21 -0
  200. package/dist/src/tui/adapters/teams-adapter.js.map +1 -1
  201. package/dist/src/tui/app.d.ts.map +1 -1
  202. package/dist/src/tui/app.js +12 -3
  203. package/dist/src/tui/app.js.map +1 -1
  204. package/docs/content/docs/agent-skills.mdx +1 -1
  205. package/docs/content/docs/cli/imessage.mdx +99 -0
  206. package/docs/content/docs/cli/instagram.mdx +2 -1
  207. package/docs/content/docs/cli/meta.json +1 -0
  208. package/docs/content/docs/cli/teams.mdx +22 -0
  209. package/docs/content/docs/index.mdx +9 -6
  210. package/docs/content/docs/quick-start.mdx +2 -0
  211. package/docs/content/docs/sdk/instagram.mdx +90 -5
  212. package/docs/content/docs/tui.mdx +4 -3
  213. package/package.json +10 -2
  214. package/scripts/postbuild.ts +12 -15
  215. package/skills/agent-channeltalk/SKILL.md +1 -1
  216. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  217. package/skills/agent-discord/SKILL.md +1 -1
  218. package/skills/agent-discordbot/SKILL.md +1 -1
  219. package/skills/agent-imessage/SKILL.md +133 -0
  220. package/skills/agent-imessage/references/authentication.md +66 -0
  221. package/skills/agent-imessage/references/permissions.md +31 -0
  222. package/skills/agent-imessage/references/setup.md +49 -0
  223. package/skills/agent-instagram/SKILL.md +72 -2
  224. package/skills/agent-instagram/references/authentication.md +11 -0
  225. package/skills/agent-instagram/references/common-patterns.md +36 -0
  226. package/skills/agent-instagram/templates/monitor-chat.sh +5 -0
  227. package/skills/agent-kakaotalk/SKILL.md +1 -1
  228. package/skills/agent-line/SKILL.md +1 -1
  229. package/skills/agent-slack/SKILL.md +1 -1
  230. package/skills/agent-slackbot/SKILL.md +1 -1
  231. package/skills/agent-teams/SKILL.md +59 -6
  232. package/skills/agent-teams/references/authentication.md +31 -1
  233. package/skills/agent-telegram/SKILL.md +1 -1
  234. package/skills/agent-telegrambot/SKILL.md +1 -1
  235. package/skills/agent-webex/SKILL.md +1 -1
  236. package/skills/agent-webexbot/SKILL.md +1 -1
  237. package/skills/agent-wechatbot/SKILL.md +1 -1
  238. package/skills/agent-whatsapp/SKILL.md +1 -1
  239. package/skills/agent-whatsappbot/SKILL.md +1 -1
  240. package/src/cli.ts +4 -0
  241. package/src/platforms/discord/client.test.ts +86 -0
  242. package/src/platforms/discord/client.ts +10 -3
  243. package/src/platforms/imessage/cli.ts +39 -0
  244. package/src/platforms/imessage/client.test.ts +137 -0
  245. package/src/platforms/imessage/client.ts +253 -0
  246. package/src/platforms/imessage/commands/auth.ts +142 -0
  247. package/src/platforms/imessage/commands/chat.ts +51 -0
  248. package/src/platforms/imessage/commands/doctor.test.ts +80 -0
  249. package/src/platforms/imessage/commands/doctor.ts +139 -0
  250. package/src/platforms/imessage/commands/index.ts +6 -0
  251. package/src/platforms/imessage/commands/message.test.ts +20 -0
  252. package/src/platforms/imessage/commands/message.ts +151 -0
  253. package/src/platforms/imessage/commands/setup.test.ts +52 -0
  254. package/src/platforms/imessage/commands/setup.ts +76 -0
  255. package/src/platforms/imessage/commands/shared.test.ts +19 -0
  256. package/src/platforms/imessage/commands/shared.ts +69 -0
  257. package/src/platforms/imessage/commands/whoami.ts +22 -0
  258. package/src/platforms/imessage/credential-manager.test.ts +122 -0
  259. package/src/platforms/imessage/credential-manager.ts +123 -0
  260. package/src/platforms/imessage/ensure-auth.ts +18 -0
  261. package/src/platforms/imessage/errors.ts +27 -0
  262. package/src/platforms/imessage/index.test.ts +13 -0
  263. package/src/platforms/imessage/index.ts +16 -0
  264. package/src/platforms/imessage/rpc.ts +157 -0
  265. package/src/platforms/imessage/test-stub-imsg.mjs +147 -0
  266. package/src/platforms/imessage/types.test.ts +47 -0
  267. package/src/platforms/imessage/types.ts +95 -0
  268. package/src/platforms/instagram/client.test.ts +478 -33
  269. package/src/platforms/instagram/client.ts +472 -68
  270. package/src/platforms/instagram/commands/auth.test.ts +177 -1
  271. package/src/platforms/instagram/commands/auth.ts +182 -11
  272. package/src/platforms/instagram/credential-manager.test.ts +30 -0
  273. package/src/platforms/instagram/credential-manager.ts +23 -1
  274. package/src/platforms/instagram/hybrid-listener.test.ts +135 -0
  275. package/src/platforms/instagram/hybrid-listener.ts +196 -0
  276. package/src/platforms/instagram/index.ts +10 -0
  277. package/src/platforms/instagram/mqtt/connection.ts +72 -0
  278. package/src/platforms/instagram/mqtt/thrift.test.ts +91 -0
  279. package/src/platforms/instagram/mqtt/thrift.ts +159 -0
  280. package/src/platforms/instagram/mqtt/transport.ts +285 -0
  281. package/src/platforms/instagram/realtime-listener.test.ts +34 -0
  282. package/src/platforms/instagram/realtime-listener.ts +201 -0
  283. package/src/platforms/instagram/types.ts +11 -8
  284. package/src/platforms/teams/app-config.ts +92 -0
  285. package/src/platforms/teams/client.test.ts +335 -1
  286. package/src/platforms/teams/client.ts +342 -7
  287. package/src/platforms/teams/commands/auth.test.ts +93 -1
  288. package/src/platforms/teams/commands/auth.ts +182 -3
  289. package/src/platforms/teams/commands/file.test.ts +92 -1
  290. package/src/platforms/teams/commands/file.ts +76 -1
  291. package/src/platforms/teams/commands/message.test.ts +95 -1
  292. package/src/platforms/teams/commands/message.ts +145 -3
  293. package/src/platforms/teams/credential-manager.test.ts +20 -0
  294. package/src/platforms/teams/credential-manager.ts +68 -3
  295. package/src/platforms/teams/device-code.test.ts +311 -0
  296. package/src/platforms/teams/device-code.ts +277 -0
  297. package/src/platforms/teams/device-login.test.ts +288 -0
  298. package/src/platforms/teams/device-login.ts +206 -0
  299. package/src/platforms/teams/ensure-auth.ts +11 -0
  300. package/src/platforms/teams/index.test.ts +10 -0
  301. package/src/platforms/teams/index.ts +16 -1
  302. package/src/platforms/teams/listener.ts +295 -0
  303. package/src/platforms/teams/realm-discovery.test.ts +73 -0
  304. package/src/platforms/teams/realm-discovery.ts +42 -0
  305. package/src/platforms/teams/token-extractor.ts +69 -0
  306. package/src/platforms/teams/token-provider.test.ts +227 -0
  307. package/src/platforms/teams/token-provider.ts +284 -0
  308. package/src/platforms/teams/trouter.test.ts +114 -0
  309. package/src/platforms/teams/trouter.ts +228 -0
  310. package/src/platforms/teams/types.test.ts +17 -0
  311. package/src/platforms/teams/types.ts +86 -0
  312. package/src/platforms/webexbot/cli.ts +0 -0
  313. package/src/tui/adapters/imessage-adapter.ts +131 -0
  314. package/src/tui/adapters/instagram-adapter.ts +13 -0
  315. package/src/tui/adapters/teams-adapter.ts +23 -0
  316. package/src/tui/app.ts +16 -3
@@ -0,0 +1,114 @@
1
+ import { expect, it } from 'bun:test'
2
+ import { gzipSync } from 'node:zlib'
3
+
4
+ import {
5
+ buildActivityFrame,
6
+ buildAuthenticateFrame,
7
+ buildEventAck,
8
+ buildPingFrame,
9
+ buildRequestAck,
10
+ buildWebSocketUrl,
11
+ decodeMessageBody,
12
+ extractChatId,
13
+ isMessageLossFrame,
14
+ parseRequestFrame,
15
+ type TrouterInfo,
16
+ } from './trouter'
17
+
18
+ const info: TrouterInfo = {
19
+ socketio: 'https://pub-ent-test.trouter.teams.microsoft.com:443/',
20
+ surl: 'https://pub-ent-test.trouter.teams.microsoft.com:3443/v4/f/HASH/',
21
+ connectparams: { sr: 'abc', issuer: 'prod-2', sig: 'xyz' },
22
+ }
23
+
24
+ it('buildWebSocketUrl targets the socket.io v1 websocket path', () => {
25
+ const url = buildWebSocketUrl(info, 'SESSION123', 'endpoint-uuid')
26
+ expect(url.startsWith(`${info.socketio}socket.io/1/websocket/SESSION123?`)).toBe(true)
27
+ expect(url).toContain('epid=endpoint-uuid')
28
+ expect(url).toContain('sr=abc')
29
+ expect(url).toContain('auth=true')
30
+ })
31
+
32
+ it('buildAuthenticateFrame embeds the Bearer id_token and connectparams', () => {
33
+ const frame = buildAuthenticateFrame(info, 'ID_TOKEN')
34
+ expect(frame.startsWith('5:::')).toBe(true)
35
+ const payload = JSON.parse(frame.slice(4))
36
+ expect(payload.name).toBe('user.authenticate')
37
+ expect(payload.args[0].headers.Authorization).toBe('Bearer ID_TOKEN')
38
+ expect(payload.args[0].connectparams).toEqual(info.connectparams)
39
+ })
40
+
41
+ it('buildActivityFrame and buildPingFrame use the sequence number', () => {
42
+ expect(buildActivityFrame(3)).toBe('5:3::{"name":"user.activity","args":[{"state":"active"}]}')
43
+ expect(buildPingFrame(7)).toBe('5:7::{"name":"ping"}')
44
+ })
45
+
46
+ it('buildRequestAck returns a 200 ack for the request id', () => {
47
+ const ack = buildRequestAck(42)
48
+ expect(ack.startsWith('3:::')).toBe(true)
49
+ expect(JSON.parse(ack.slice(4))).toEqual({ id: 42, status: 200, body: '' })
50
+ })
51
+
52
+ it('buildEventAck acks numbered event frames and ignores others', () => {
53
+ expect(buildEventAck('5:12::{"name":"trouter.message_loss"}')).toBe('6:12::')
54
+ expect(buildEventAck('5:9+::{"name":"x"}')).toBe('6:9::')
55
+ expect(buildEventAck('1::')).toBeNull()
56
+ expect(buildEventAck('6:::1["pong"]')).toBeNull()
57
+ })
58
+
59
+ it('parseRequestFrame parses 3::: frames and rejects others', () => {
60
+ const frame = '3:::{"id":1,"method":"POST","url":"/v4/f/HASH/messaging","body":"{}"}'
61
+ const parsed = parseRequestFrame(frame)
62
+ expect(parsed?.id).toBe(1)
63
+ expect(parsed?.url).toBe('/v4/f/HASH/messaging')
64
+ expect(parseRequestFrame('5:1::{}')).toBeNull()
65
+ expect(parseRequestFrame('3:::not-json')).toBeNull()
66
+ })
67
+
68
+ it('parseRequestFrame rejects frames without a numeric id', () => {
69
+ expect(parseRequestFrame('3:::{"url":"/x/messaging"}')).toBeNull()
70
+ expect(parseRequestFrame('3:::{"id":"abc"}')).toBeNull()
71
+ expect(parseRequestFrame('3:::"just-a-string"')).toBeNull()
72
+ expect(parseRequestFrame('3:::null')).toBeNull()
73
+ })
74
+
75
+ it('isMessageLossFrame detects trouter.message_loss events', () => {
76
+ expect(isMessageLossFrame('5:2::{"name":"trouter.message_loss","args":[{}]}')).toBe(true)
77
+ expect(isMessageLossFrame('5:1::{"name":"trouter.connected","args":[{}]}')).toBe(false)
78
+ expect(isMessageLossFrame('1::')).toBe(false)
79
+ })
80
+
81
+ it('extractChatId pulls the conversation id from a link', () => {
82
+ expect(extractChatId('https://notifications.skype.net/v1/users/ME/conversations/19:uni01_abc@thread.v2')).toBe(
83
+ '19:uni01_abc@thread.v2',
84
+ )
85
+ expect(extractChatId('https://x/conversations/19:uni01_abc@thread.v2/messages/123')).toBe('19:uni01_abc@thread.v2')
86
+ expect(extractChatId(undefined)).toBeNull()
87
+ expect(extractChatId('no-conversation-here')).toBeNull()
88
+ })
89
+
90
+ it('decodeMessageBody parses plain JSON bodies', () => {
91
+ const body = JSON.stringify({ resourceType: 'NewMessage', resource: { id: '1' } })
92
+ expect(decodeMessageBody({}, body)).toEqual({ resourceType: 'NewMessage', resource: { id: '1' } })
93
+ })
94
+
95
+ it('decodeMessageBody inflates gzip+base64 bodies', () => {
96
+ const inner = JSON.stringify({ resourceType: 'NewMessage' })
97
+ const gzipped = gzipSync(Buffer.from(inner)).toString('base64')
98
+ const decoded = decodeMessageBody({ 'X-Microsoft-Skype-Content-Encoding': 'gzip' }, gzipped)
99
+ expect(decoded).toEqual({ resourceType: 'NewMessage' })
100
+ })
101
+
102
+ it('decodeMessageBody unwraps nested cp (gzip+base64) payloads', () => {
103
+ const inner = JSON.stringify({ resourceType: 'NewMessage', resource: { content: 'hi' } })
104
+ const cp = gzipSync(Buffer.from(inner)).toString('base64')
105
+ const decoded = decodeMessageBody({}, JSON.stringify({ cp }))
106
+ expect(decoded).toEqual({ resourceType: 'NewMessage', resource: { content: 'hi' } })
107
+ })
108
+
109
+ it('decodeMessageBody unwraps nested gp (base64) payloads', () => {
110
+ const inner = JSON.stringify({ resourceType: 'NewMessage' })
111
+ const gp = Buffer.from(inner).toString('base64')
112
+ const decoded = decodeMessageBody({}, JSON.stringify({ gp }))
113
+ expect(decoded).toEqual({ resourceType: 'NewMessage' })
114
+ })
@@ -0,0 +1,228 @@
1
+ import { gunzipSync } from 'node:zlib'
2
+
3
+ import type { TeamsAccountType } from './types'
4
+
5
+ // Trouter is Teams' internal real-time notification service. Protocol shape was
6
+ // reverse-engineered from the Teams web client and cross-checked against the
7
+ // open-source purple-teams and eisbaw/ost implementations. It speaks socket.io
8
+ // v1 (colon-prefixed frames) over a WebSocket, NOT engine.io.
9
+
10
+ const TROUTER_BOOTSTRAP_URL = 'https://go.trouter.teams.microsoft.com/v4/a'
11
+ // Personal/TFL accounts register against edge.skype.com; work accounts use
12
+ // teams.microsoft.com.
13
+ const REGISTRAR_URL_PERSONAL = 'https://edge.skype.com/registrar/prod/v2/registrations'
14
+ const REGISTRAR_URL_WORK = 'https://teams.microsoft.com/registrar/prod/V2/registrations'
15
+ const CLIENTINFO_VERSION = '27/1.0.0.2024101502'
16
+ const TC = JSON.stringify({ cv: '2024.04.01.1', ua: 'TeamsCDL', hr: '', v: CLIENTINFO_VERSION })
17
+ const USER_AGENT =
18
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Electron/35.3.0'
19
+ const REGISTRATION_TTL_SECONDS = 86400
20
+
21
+ export interface TrouterConnectParams {
22
+ [key: string]: string
23
+ }
24
+
25
+ export interface TrouterInfo {
26
+ socketio: string
27
+ surl: string
28
+ connectparams: TrouterConnectParams
29
+ ccid?: string
30
+ }
31
+
32
+ export interface TrouterRequestFrame {
33
+ id: number
34
+ url?: string
35
+ headers?: Record<string, string>
36
+ body?: string
37
+ }
38
+
39
+ interface RegistrationSpec {
40
+ appId: string
41
+ templateKey: string
42
+ pathSuffix: string
43
+ productContext: string
44
+ }
45
+
46
+ const REGISTRATION_SPECS: RegistrationSpec[] = [
47
+ {
48
+ appId: 'NextGenCalling',
49
+ templateKey: 'DesktopNgc_2.5:SkypeNgc',
50
+ pathSuffix: 'NGCallManagerWin',
51
+ productContext: '',
52
+ },
53
+ { appId: 'SkypeSpacesWeb', templateKey: 'SkypeSpacesWeb_2.4', pathSuffix: 'SkypeSpacesWeb', productContext: '' },
54
+ { appId: 'TeamsCDLWebWorker', templateKey: 'TeamsCDLWebWorker_2.6', pathSuffix: '', productContext: 'TFL' },
55
+ ]
56
+
57
+ export function buildTrouterQuery(info: TrouterInfo, endpointId: string): string {
58
+ const params = new URLSearchParams()
59
+ params.set('v', 'v4')
60
+ for (const [key, value] of Object.entries(info.connectparams)) params.set(key, value)
61
+ params.set('tc', TC)
62
+ params.set('timeout', '40')
63
+ params.set('auth', 'true')
64
+ params.set('epid', endpointId)
65
+ if (info.ccid) params.set('ccid', info.ccid)
66
+ params.set('con_num', `${Date.now()}_1`)
67
+ return params.toString()
68
+ }
69
+
70
+ export async function fetchTrouterInfo(skypeToken: string, endpointId: string): Promise<TrouterInfo> {
71
+ const response = await fetch(`${TROUTER_BOOTSTRAP_URL}?epid=${encodeURIComponent(endpointId)}`, {
72
+ method: 'POST',
73
+ headers: { 'x-skypetoken': skypeToken, 'Content-Length': '0', 'User-Agent': USER_AGENT },
74
+ })
75
+ if (!response.ok) {
76
+ throw new Error(`Trouter bootstrap failed: ${response.status} ${response.statusText}`)
77
+ }
78
+ return (await response.json()) as TrouterInfo
79
+ }
80
+
81
+ export async function fetchTrouterSessionId(
82
+ info: TrouterInfo,
83
+ skypeToken: string,
84
+ endpointId: string,
85
+ ): Promise<string> {
86
+ const url = `${info.socketio}socket.io/1/?${buildTrouterQuery(info, endpointId)}`
87
+ const response = await fetch(url, { headers: { 'X-Skypetoken': skypeToken, 'User-Agent': USER_AGENT } })
88
+ if (!response.ok) {
89
+ throw new Error(`Trouter handshake failed: ${response.status} ${response.statusText}`)
90
+ }
91
+ const body = await response.text()
92
+ return body.split(':')[0]
93
+ }
94
+
95
+ export function buildWebSocketUrl(info: TrouterInfo, sessionId: string, endpointId: string): string {
96
+ return `${info.socketio}socket.io/1/websocket/${sessionId}?${buildTrouterQuery(info, endpointId)}`
97
+ }
98
+
99
+ export function buildAuthenticateFrame(info: TrouterInfo, idToken: string): string {
100
+ const message = {
101
+ name: 'user.authenticate',
102
+ args: [
103
+ {
104
+ headers: {
105
+ 'X-Ms-Test-User': 'False',
106
+ Authorization: `Bearer ${idToken}`,
107
+ 'X-MS-Migration': 'True',
108
+ },
109
+ connectparams: info.connectparams,
110
+ },
111
+ ],
112
+ }
113
+ return `5:::${JSON.stringify(message)}`
114
+ }
115
+
116
+ export function buildActivityFrame(sequence: number): string {
117
+ return `5:${sequence}::{"name":"user.activity","args":[{"state":"active"}]}`
118
+ }
119
+
120
+ export function buildPingFrame(sequence: number): string {
121
+ return `5:${sequence}::{"name":"ping"}`
122
+ }
123
+
124
+ // Trouter delivers messages as socket.io "3:::" data frames, which require an
125
+ // HTTP-style 200 acknowledgement or the server marks the endpoint unresponsive
126
+ // and stops routing.
127
+ export function buildRequestAck(requestId: number): string {
128
+ return `3:::${JSON.stringify({ id: requestId, status: 200, body: '' })}`
129
+ }
130
+
131
+ // Server "5:<id>+::" event frames must be acked with "6:<id>::" or trouter
132
+ // eventually drops the connection.
133
+ export function buildEventAck(frame: string): string | null {
134
+ const match = frame.match(/^5:(\d+)\+?::/)
135
+ return match ? `6:${match[1]}::` : null
136
+ }
137
+
138
+ export async function registerEndpoint(
139
+ info: TrouterInfo,
140
+ skypeToken: string,
141
+ idToken: string,
142
+ endpointId: string,
143
+ accountType: TeamsAccountType,
144
+ makeId: () => string,
145
+ ): Promise<void> {
146
+ const registrarUrl = accountType === 'personal' ? REGISTRAR_URL_PERSONAL : REGISTRAR_URL_WORK
147
+
148
+ for (const spec of REGISTRATION_SPECS) {
149
+ // The messaging worker must reuse the endpoint id; call apps get fresh ids.
150
+ const registrationId = spec.appId === 'TeamsCDLWebWorker' ? endpointId : makeId()
151
+ const payload = {
152
+ clientDescription: {
153
+ appId: spec.appId,
154
+ aesKey: '',
155
+ languageId: 'en-US',
156
+ platform: 'edge',
157
+ templateKey: spec.templateKey,
158
+ platformUIVersion: CLIENTINFO_VERSION,
159
+ productContext: accountType === 'personal' ? spec.productContext : '',
160
+ },
161
+ registrationId,
162
+ nodeId: '',
163
+ transports: { TROUTER: [{ context: '', path: `${info.surl}${spec.pathSuffix}`, ttl: REGISTRATION_TTL_SECONDS }] },
164
+ }
165
+
166
+ const response = await fetch(registrarUrl, {
167
+ method: 'POST',
168
+ headers: {
169
+ 'Content-Type': 'application/json',
170
+ 'X-Skypetoken': skypeToken,
171
+ Authorization: `Bearer ${idToken}`,
172
+ 'User-Agent': USER_AGENT,
173
+ },
174
+ body: JSON.stringify(payload),
175
+ })
176
+ if (!response.ok) {
177
+ throw new Error(`Trouter registration for ${spec.appId} failed: ${response.status}`)
178
+ }
179
+ }
180
+ }
181
+
182
+ export function parseRequestFrame(frame: string): TrouterRequestFrame | null {
183
+ if (!frame.startsWith('3:::')) return null
184
+ try {
185
+ const parsed = JSON.parse(frame.slice(4)) as unknown
186
+ if (typeof parsed !== 'object' || parsed === null) return null
187
+ const { id } = parsed as { id?: unknown }
188
+ if (typeof id !== 'number' || !Number.isFinite(id)) return null
189
+ return parsed as TrouterRequestFrame
190
+ } catch {
191
+ return null
192
+ }
193
+ }
194
+
195
+ export function isMessageLossFrame(frame: string): boolean {
196
+ if (!frame.startsWith('5:')) return false
197
+ const jsonStart = frame.indexOf('{')
198
+ if (jsonStart === -1) return false
199
+ try {
200
+ const payload = JSON.parse(frame.slice(jsonStart)) as { name?: string }
201
+ return payload.name === 'trouter.message_loss'
202
+ } catch {
203
+ return false
204
+ }
205
+ }
206
+
207
+ // Trouter message bodies may be gzip+base64 encoded, and nest a further encoded
208
+ // payload under `cp` (gzip+base64) or `gp` (base64).
209
+ export function decodeMessageBody(headers: Record<string, string>, body: string): Record<string, unknown> {
210
+ let raw = body
211
+ if (headers['X-Microsoft-Skype-Content-Encoding'] === 'gzip') {
212
+ raw = gunzipSync(Buffer.from(body, 'base64')).toString('utf8')
213
+ }
214
+ const obj = JSON.parse(raw) as Record<string, unknown>
215
+ if (typeof obj.cp === 'string') {
216
+ return JSON.parse(gunzipSync(Buffer.from(obj.cp, 'base64')).toString('utf8')) as Record<string, unknown>
217
+ }
218
+ if (typeof obj.gp === 'string') {
219
+ return JSON.parse(Buffer.from(obj.gp, 'base64').toString('utf8')) as Record<string, unknown>
220
+ }
221
+ return obj
222
+ }
223
+
224
+ export function extractChatId(link: string | undefined): string | null {
225
+ if (!link) return null
226
+ const match = link.match(/conversations\/([^/]+)/)
227
+ return match ? decodeURIComponent(match[1]) : null
228
+ }
@@ -8,6 +8,7 @@ import {
8
8
  TeamsFileSchema,
9
9
  TeamsMessageSchema,
10
10
  TeamsReactionSchema,
11
+ TeamsSearchResultSchema,
11
12
  TeamsTeamSchema,
12
13
  TeamsUserSchema,
13
14
  } from './types'
@@ -87,6 +88,22 @@ it('TeamsMessageSchema rejects missing required fields', () => {
87
88
  expect(result.success).toBe(false)
88
89
  })
89
90
 
91
+ it('TeamsSearchResultSchema validates search result shape', () => {
92
+ const result = TeamsSearchResultSchema.safeParse({
93
+ id: 'msg-123',
94
+ content: 'Deploy complete',
95
+ author: { id: 'user-123', displayName: 'Alice' },
96
+ channel_id: 'channel-123',
97
+ thread_id: 'thread-123',
98
+ team_name: 'Team One',
99
+ channel_name: 'General',
100
+ timestamp: '2024-01-01T00:00:00.000Z',
101
+ permalink: 'https://teams.microsoft.com/l/message/msg-123',
102
+ })
103
+
104
+ expect(result.success).toBe(true)
105
+ })
106
+
90
107
  // TeamsUserSchema tests
91
108
  it('TeamsUserSchema validates correct user', () => {
92
109
  const result = TeamsUserSchema.safeParse({
@@ -26,6 +26,24 @@ export interface TeamsMessage {
26
26
  }
27
27
  content: string
28
28
  timestamp: string
29
+ root_message_id?: string
30
+ parent_message_id?: string
31
+ is_thread_reply?: boolean
32
+ }
33
+
34
+ export interface TeamsSearchResult {
35
+ id: string
36
+ content: string
37
+ author: {
38
+ id: string
39
+ displayName: string
40
+ }
41
+ channel_id: string
42
+ thread_id?: string
43
+ team_name?: string
44
+ channel_name?: string
45
+ timestamp: string
46
+ permalink?: string
29
47
  }
30
48
 
31
49
  export interface TeamsUser {
@@ -56,6 +74,8 @@ export interface TeamsFile {
56
74
  size: number
57
75
  url: string
58
76
  contentType?: string
77
+ sharepoint_url?: string
78
+ object_url?: string
59
79
  }
60
80
 
61
81
  export interface TeamsCredentials {
@@ -67,6 +87,8 @@ export type TeamsAccountType = 'work' | 'personal'
67
87
 
68
88
  export type TeamsRegion = 'amer' | 'emea' | 'apac'
69
89
 
90
+ export type TeamsAuthMethod = 'device-code' | 'browser' | 'extracted' | 'manual'
91
+
70
92
  export interface TeamsAccount {
71
93
  token: string
72
94
  token_expires_at?: string
@@ -81,6 +103,10 @@ export interface TeamsAccount {
81
103
  team_name: string
82
104
  }
83
105
  >
106
+ auth_method?: TeamsAuthMethod
107
+ aad_refresh_token?: string
108
+ aad_client_id?: string
109
+ aad_tenant_id?: string
84
110
  }
85
111
 
86
112
  export interface TeamsConfig {
@@ -125,6 +151,24 @@ export const TeamsMessageSchema = z.object({
125
151
  }),
126
152
  content: z.string(),
127
153
  timestamp: z.string(),
154
+ root_message_id: z.string().optional(),
155
+ parent_message_id: z.string().optional(),
156
+ is_thread_reply: z.boolean().optional(),
157
+ })
158
+
159
+ export const TeamsSearchResultSchema = z.object({
160
+ id: z.string(),
161
+ content: z.string(),
162
+ author: z.object({
163
+ id: z.string(),
164
+ displayName: z.string(),
165
+ }),
166
+ channel_id: z.string(),
167
+ thread_id: z.string().optional(),
168
+ team_name: z.string().optional(),
169
+ channel_name: z.string().optional(),
170
+ timestamp: z.string(),
171
+ permalink: z.string().optional(),
128
172
  })
129
173
 
130
174
  export const TeamsUserSchema = z.object({
@@ -155,6 +199,8 @@ export const TeamsFileSchema = z.object({
155
199
  size: z.number(),
156
200
  url: z.string(),
157
201
  contentType: z.string().optional(),
202
+ sharepoint_url: z.string().optional(),
203
+ object_url: z.string().optional(),
158
204
  })
159
205
 
160
206
  export const TeamsCredentialsSchema = z.object({
@@ -166,6 +212,8 @@ export const TeamsAccountTypeSchema = z.enum(['work', 'personal'])
166
212
 
167
213
  export const TeamsRegionSchema = z.enum(['amer', 'emea', 'apac'])
168
214
 
215
+ export const TeamsAuthMethodSchema = z.enum(['device-code', 'browser', 'extracted', 'manual'])
216
+
169
217
  export const TeamsAccountSchema = z.object({
170
218
  token: z.string(),
171
219
  token_expires_at: z.string().optional(),
@@ -180,6 +228,10 @@ export const TeamsAccountSchema = z.object({
180
228
  team_name: z.string(),
181
229
  }),
182
230
  ),
231
+ auth_method: TeamsAuthMethodSchema.optional(),
232
+ aad_refresh_token: z.string().optional(),
233
+ aad_client_id: z.string().optional(),
234
+ aad_tenant_id: z.string().optional(),
183
235
  })
184
236
 
185
237
  export const TeamsConfigSchema = z.object({
@@ -200,6 +252,31 @@ export const TeamsConfigLegacySchema = z.object({
200
252
  ),
201
253
  })
202
254
 
255
+ export interface TeamsRealtimeMessage {
256
+ id: string
257
+ chatId: string
258
+ content: string
259
+ author: {
260
+ id: string
261
+ displayName: string
262
+ }
263
+ messageType: string
264
+ timestamp: string
265
+ }
266
+
267
+ export interface TeamsTrouterGenericEvent {
268
+ resourceType: string
269
+ [key: string]: unknown
270
+ }
271
+
272
+ export interface TeamsListenerEventMap {
273
+ message: [message: TeamsRealtimeMessage]
274
+ teams_event: [event: TeamsTrouterGenericEvent]
275
+ connected: [info: { endpointId: string }]
276
+ disconnected: []
277
+ error: [error: Error]
278
+ }
279
+
203
280
  export class TeamsError extends Error {
204
281
  code: string
205
282
 
@@ -209,3 +286,12 @@ export class TeamsError extends Error {
209
286
  this.code = code
210
287
  }
211
288
  }
289
+
290
+ export class TeamsAuthCapabilityError extends Error {
291
+ constructor() {
292
+ super(
293
+ 'Requires `agent-teams auth login` — cookie-based auth (`auth extract`) can only provide a Skype token, not the Microsoft token needed for search or SharePoint/OneDrive file downloads.',
294
+ )
295
+ this.name = 'TeamsAuthCapabilityError'
296
+ }
297
+ }
File without changes
@@ -0,0 +1,131 @@
1
+ import { ImsgClient } from '@/platforms/imessage/client'
2
+ import { IMessageCredentialManager } from '@/platforms/imessage/credential-manager'
3
+
4
+ import type { AuthHint, AuthIO, PlatformAdapter, UnifiedChannel, UnifiedMessage, Workspace } from './types'
5
+
6
+ export class IMessageAdapter implements PlatformAdapter {
7
+ readonly name = 'iMessage'
8
+
9
+ private client: ImsgClient | null = null
10
+ private credManager = new IMessageCredentialManager()
11
+ private currentAccount: Workspace | null = null
12
+ private stopWatch: (() => Promise<void>) | null = null
13
+
14
+ async login(): Promise<void> {
15
+ const resolved = await this.credManager.resolveAccount()
16
+ if (!resolved) {
17
+ throw new Error('iMessage is not configured. Run "agent-imessage setup" or set AGENT_IMESSAGE_BIN.')
18
+ }
19
+ const client = new ImsgClient()
20
+ await client.login({ binaryPath: resolved.binary_path, region: resolved.region })
21
+ await client.connect()
22
+ this.client = client
23
+
24
+ const config = await this.credManager.loadConfig()
25
+ if (config.current && config.accounts[config.current]) {
26
+ const acct = config.accounts[config.current]
27
+ this.currentAccount = { id: acct.account_id, name: acct.label ?? acct.account_id }
28
+ }
29
+ }
30
+
31
+ async getChannels(): Promise<UnifiedChannel[]> {
32
+ const client = this.ensureClient()
33
+ const chats = await client.listChats(50)
34
+ return chats.map((chat) => ({ id: String(chat.id), name: chat.name }))
35
+ }
36
+
37
+ async getMessages(channelId: string, limit = 50): Promise<UnifiedMessage[]> {
38
+ const client = this.ensureClient()
39
+ const messages = await client.getMessages(Number.parseInt(channelId, 10), limit)
40
+ return messages.map((msg) => ({
41
+ id: msg.guid,
42
+ channelId,
43
+ author: msg.is_outgoing ? 'you' : (msg.from_name ?? msg.from ?? 'unknown'),
44
+ content: msg.text ?? '',
45
+ timestamp: msg.timestamp,
46
+ }))
47
+ }
48
+
49
+ async sendMessage(channelId: string, text: string): Promise<void> {
50
+ const client = this.ensureClient()
51
+ await client.sendMessage({ chatId: Number.parseInt(channelId, 10) }, text)
52
+ }
53
+
54
+ async startListening(onMessage: (msg: UnifiedMessage) => void): Promise<void> {
55
+ const client = this.ensureClient()
56
+ this.stopWatch = await client.watch(
57
+ (msg) =>
58
+ onMessage({
59
+ id: msg.guid,
60
+ channelId: String(msg.chat_id),
61
+ author: msg.is_outgoing ? 'you' : (msg.from_name ?? msg.from ?? 'unknown'),
62
+ content: msg.text ?? '',
63
+ timestamp: msg.timestamp,
64
+ }),
65
+ { onError: () => undefined },
66
+ )
67
+ }
68
+
69
+ stopListening(): void {
70
+ void this.stopWatch?.()
71
+ this.stopWatch = null
72
+ }
73
+
74
+ async getWorkspaces(): Promise<Workspace[]> {
75
+ const accounts = await this.credManager.listAccounts()
76
+ return accounts.map((acct) => ({ id: acct.account_id, name: acct.label ?? acct.account_id }))
77
+ }
78
+
79
+ async switchWorkspace(accountId: string): Promise<void> {
80
+ if (this.client) {
81
+ await this.client.close().catch(() => {})
82
+ this.client = null
83
+ }
84
+ const account = await this.credManager.getAccount(accountId)
85
+ if (!account) throw new Error(`Account ${accountId} not found`)
86
+
87
+ const client = new ImsgClient()
88
+ await client.login({ binaryPath: account.binary_path, region: account.region })
89
+ await client.connect()
90
+ this.client = client
91
+ this.currentAccount = { id: account.account_id, name: account.label ?? account.account_id }
92
+ }
93
+
94
+ getCurrentWorkspace(): Workspace | null {
95
+ return this.currentAccount
96
+ }
97
+
98
+ getAuthHint(): AuthHint {
99
+ return {
100
+ command: 'agent-imessage setup',
101
+ description: 'iMessage runs on this Mac via the imsg tool. Run the guided setup below.',
102
+ }
103
+ }
104
+
105
+ async authenticate(io: AuthIO): Promise<void> {
106
+ const bin = (await io.prompt('Path to imsg binary (blank for "imsg" on PATH)')).trim() || undefined
107
+ const client = new ImsgClient()
108
+ await client.login({ binaryPath: bin })
109
+ await client.connect()
110
+
111
+ const { createAccountId } = await import('@/platforms/imessage/types')
112
+ const now = new Date().toISOString()
113
+ const accountId = createAccountId(bin ?? 'default')
114
+ await this.credManager.setAccount({
115
+ account_id: accountId,
116
+ provider: 'imsg',
117
+ binary_path: bin,
118
+ created_at: now,
119
+ updated_at: now,
120
+ })
121
+ await this.credManager.setCurrent(accountId)
122
+
123
+ this.client = client
124
+ this.currentAccount = { id: accountId, name: accountId }
125
+ }
126
+
127
+ private ensureClient(): ImsgClient {
128
+ if (!this.client) throw new Error('Not logged in. Call login() first.')
129
+ return this.client
130
+ }
131
+ }
@@ -107,8 +107,21 @@ export class InstagramAdapter implements PlatformAdapter {
107
107
  if (!code) throw new Error('Verification code is required')
108
108
  const challengeResult = await client.challengeSubmitCode(result.challengePath, code)
109
109
  userId = challengeResult.userId || userId
110
+ } else if (result.oneClickEmailAvailable) {
111
+ const { parseOneClickLoginLink } = await import('@/platforms/instagram/client')
112
+ io.print('Password login was rejected; this account can log in by email. Sending a login email...')
113
+ const { contactPoint } = await client.sendOneClickLoginEmail(username)
114
+ io.print(contactPoint ? `Login email sent to ${contactPoint}.` : 'Login email sent.')
115
+ const link = await io.prompt('Paste the "Login as ..." link from the email')
116
+ if (!link) throw new Error('Login link is required')
117
+ const parsed = parseOneClickLoginLink(link)
118
+ if (!parsed) throw new Error('Could not read uid and token from the pasted link')
119
+ const emailResult = await client.oneClickLogin(parsed.uid, parsed.token)
120
+ userId = emailResult.userId || userId
110
121
  }
111
122
 
123
+ if (!userId) throw new Error('Login did not complete')
124
+
112
125
  const now = new Date().toISOString()
113
126
  await this.credManager.setAccount({
114
127
  account_id: accountId,