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
@@ -1,10 +1,16 @@
1
1
  import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
2
- import { unlinkSync } from 'node:fs'
2
+ import { rmSync, unlinkSync } from 'node:fs'
3
+ import { join } from 'node:path'
3
4
 
4
5
  import { TeamsClient } from './client'
6
+ import { TeamsCredentialManager } from './credential-manager'
5
7
  import { TeamsError } from './types'
6
8
 
7
9
  const TEMP_FILES_TO_CLEANUP = ['/tmp/test-teams-upload.txt']
10
+ const TEMP_DIRS_TO_CLEANUP: string[] = []
11
+ const SEARCH_TENANT_ID = '11111111-1111-1111-1111-111111111111'
12
+ const SEARCH_USER_ID = '22222222-2222-2222-2222-222222222222'
13
+ const GRAPH_AUDIENCE = 'https://graph.microsoft.com'
8
14
 
9
15
  describe('TeamsClient', () => {
10
16
  const originalFetch = globalThis.fetch
@@ -36,8 +42,63 @@ describe('TeamsClient', () => {
36
42
  // File may not exist, ignore
37
43
  }
38
44
  }
45
+ for (const dir of TEMP_DIRS_TO_CLEANUP.splice(0)) {
46
+ rmSync(dir, { recursive: true, force: true })
47
+ }
39
48
  })
40
49
 
50
+ const setupCredentialManager = async (): Promise<TeamsCredentialManager> => {
51
+ const dir = join(import.meta.dir, `.test-teams-client-${Date.now()}-${Math.random().toString(36).slice(2)}`)
52
+ TEMP_DIRS_TO_CLEANUP.push(dir)
53
+ const manager = new TeamsCredentialManager(dir)
54
+ await manager.setDeviceCodeAccount({
55
+ accountType: 'work',
56
+ token: 'skype-token',
57
+ tokenExpiresAt: '2100-01-01T00:00:00Z',
58
+ aadRefreshToken: 'refresh-token',
59
+ aadClientId: 'client-id',
60
+ teams: {},
61
+ currentTeam: null,
62
+ })
63
+ return manager
64
+ }
65
+
66
+ const createSearchJwt = (): string => {
67
+ const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url')
68
+ const payload = Buffer.from(
69
+ JSON.stringify({
70
+ aud: 'https://substrate.office.com',
71
+ tid: SEARCH_TENANT_ID,
72
+ oid: SEARCH_USER_ID,
73
+ exp: Math.floor(Date.now() / 1000) + 3600,
74
+ }),
75
+ ).toString('base64url')
76
+ return `${header}.${payload}.signature`
77
+ }
78
+
79
+ const createGraphJwt = (): string => {
80
+ const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url')
81
+ const payload = Buffer.from(
82
+ JSON.stringify({
83
+ aud: GRAPH_AUDIENCE,
84
+ tid: SEARCH_TENANT_ID,
85
+ oid: SEARCH_USER_ID,
86
+ exp: Math.floor(Date.now() / 1000) + 3600,
87
+ }),
88
+ ).toString('base64url')
89
+ return `${header}.${payload}.signature`
90
+ }
91
+
92
+ const headerValue = (init: RequestInit | undefined, name: string): string | undefined => {
93
+ const headers = init?.headers
94
+ if (headers instanceof Headers) return headers.get(name) ?? undefined
95
+ if (Array.isArray(headers)) {
96
+ const pair = headers.find(([key]) => key.toLowerCase() === name.toLowerCase())
97
+ return pair?.[1]
98
+ }
99
+ return headers?.[name]
100
+ }
101
+
41
102
  const mockResponse = (body: unknown, status = 200, headers: Record<string, string> = {}) => {
42
103
  const defaultHeaders: Record<string, string> = {
43
104
  'Content-Type': 'application/json',
@@ -53,6 +114,10 @@ describe('TeamsClient', () => {
53
114
  )
54
115
  }
55
116
 
117
+ const mockBinaryResponse = (body: string, status = 200) => {
118
+ fetchResponses.push(new Response(body, { status }))
119
+ }
120
+
56
121
  describe('login', () => {
57
122
  it('requires token', async () => {
58
123
  await expect(new TeamsClient().login({ token: '', region: 'emea' })).rejects.toThrow(TeamsError)
@@ -325,6 +390,29 @@ describe('TeamsClient', () => {
325
390
  expect(fetchCalls[0].options?.method).toBe('POST')
326
391
  expect(fetchCalls[0].options?.body).toBe(JSON.stringify({ content: 'Hello world' }))
327
392
  })
393
+
394
+ it('sends a reply to a channel thread', async () => {
395
+ mockResponse({
396
+ id: 'reply1',
397
+ channel_id: 'ch1',
398
+ author: { id: '123', displayName: 'Test User' },
399
+ content: 'Thread reply',
400
+ timestamp: '2024-01-01T00:01:00.000Z',
401
+ })
402
+
403
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
404
+ const message = await client.sendMessage('111', 'ch1', 'Thread reply', 'root1')
405
+
406
+ expect(fetchCalls[0].url).toBe(
407
+ 'https://teams.microsoft.com/api/csa/emea/api/v2/teams/111/channels/ch1/messages/root1/replies',
408
+ )
409
+ expect(fetchCalls[0].options?.method).toBe('POST')
410
+ expect(fetchCalls[0].options?.body).toBe(JSON.stringify({ content: 'Thread reply', parentMessageId: 'root1' }))
411
+ // the reply the API echoes back omits thread ids, so the client normalizes them from the root
412
+ expect(message.root_message_id).toBe('root1')
413
+ expect(message.parent_message_id).toBe('root1')
414
+ expect(message.is_thread_reply).toBe(true)
415
+ })
328
416
  })
329
417
 
330
418
  describe('getMessages', () => {
@@ -359,6 +447,168 @@ describe('TeamsClient', () => {
359
447
  'https://teams.microsoft.com/api/csa/emea/api/v2/teams/111/channels/ch1/messages?limit=50',
360
448
  )
361
449
  })
450
+
451
+ it('preserves thread fields for replies without inventing them for top-level messages', async () => {
452
+ mockResponse([
453
+ {
454
+ id: 'reply1',
455
+ channel_id: 'ch1',
456
+ author: { id: '123', displayName: 'User 1' },
457
+ content: 'Reply',
458
+ timestamp: '2024-01-01T00:01:00.000Z',
459
+ rootMessageId: 'root1',
460
+ parentMessageId: 'root1',
461
+ },
462
+ {
463
+ id: 'root1',
464
+ channel_id: 'ch1',
465
+ author: { id: '123', displayName: 'User 1' },
466
+ content: 'Top level',
467
+ timestamp: '2024-01-01T00:00:00.000Z',
468
+ },
469
+ ])
470
+
471
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
472
+ const messages = await client.getMessages('111', 'ch1')
473
+
474
+ expect(messages[0].root_message_id).toBe('root1')
475
+ expect(messages[0].parent_message_id).toBe('root1')
476
+ expect(messages[0].is_thread_reply).toBe(true)
477
+ expect(messages[1].root_message_id).toBeUndefined()
478
+ expect(messages[1].is_thread_reply).toBeFalsy()
479
+ })
480
+ })
481
+
482
+ describe('getThreadReplies', () => {
483
+ it('returns replies for a channel thread with root metadata', async () => {
484
+ mockResponse([
485
+ {
486
+ id: 'reply1',
487
+ channel_id: 'ch1',
488
+ author: { id: '123', displayName: 'User 1' },
489
+ content: 'Reply 1',
490
+ timestamp: '2024-01-01T00:01:00.000Z',
491
+ },
492
+ ])
493
+
494
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
495
+ const replies = await client.getThreadReplies('111', 'ch1', 'root1', 10)
496
+
497
+ expect(replies).toHaveLength(1)
498
+ expect(replies[0].root_message_id).toBe('root1')
499
+ expect(replies[0].parent_message_id).toBe('root1')
500
+ expect(replies[0].is_thread_reply).toBe(true)
501
+ expect(fetchCalls[0].url).toBe(
502
+ 'https://teams.microsoft.com/api/csa/emea/api/v2/teams/111/channels/ch1/messages/root1/replies?limit=10',
503
+ )
504
+ })
505
+ })
506
+
507
+ describe('searchMessages', () => {
508
+ it('posts to Substrate search with bearer token, anchor mailbox, and parses nested results', async () => {
509
+ const manager = await setupCredentialManager()
510
+ const searchJwt = createSearchJwt()
511
+ mockResponse({ access_token: searchJwt, refresh_token: 'rotated-refresh', expires_in: 3600 })
512
+ mockResponse({
513
+ EntitySets: [
514
+ {
515
+ ResultSets: [
516
+ {
517
+ Results: [
518
+ {
519
+ Id: 'msg-1',
520
+ Content: '<p>Deploy complete</p>',
521
+ Author: { Id: 'author-1', DisplayName: 'Alice' },
522
+ ChannelId: 'channel-1',
523
+ ThreadId: 'thread-1',
524
+ TeamName: 'Team One',
525
+ ChannelName: 'General',
526
+ DateTimeSent: '2024-01-01T00:00:00.000Z',
527
+ WebUrl: 'https://teams.microsoft.com/l/message/msg-1',
528
+ },
529
+ ],
530
+ },
531
+ ],
532
+ },
533
+ ],
534
+ })
535
+
536
+ const client = await new TeamsClient(manager).login({ token: 'skype-token', region: 'emea' })
537
+ const results = await client.searchMessages('deploy', { limit: 10, from: 5 })
538
+
539
+ expect(fetchCalls[1].url).toBe('https://substrate.office.com/searchservice/api/v2/query')
540
+ expect(fetchCalls[1].options?.method).toBe('POST')
541
+ expect(headerValue(fetchCalls[1].options, 'Authorization')).toBe(`Bearer ${searchJwt}`)
542
+ expect(headerValue(fetchCalls[1].options, 'x-anchormailbox')).toBe(`Oid:${SEARCH_USER_ID}@${SEARCH_TENANT_ID}`)
543
+ const payload = JSON.parse(String(fetchCalls[1].options?.body)) as {
544
+ entityRequests: Array<{ entityType: string; contentSources: string[]; from: number; size: number }>
545
+ }
546
+ expect(payload.entityRequests[0]).toMatchObject({
547
+ entityType: 'Message',
548
+ contentSources: ['Teams'],
549
+ from: 5,
550
+ size: 10,
551
+ })
552
+ expect(results).toEqual([
553
+ {
554
+ id: 'msg-1',
555
+ content: 'Deploy complete',
556
+ author: { id: 'author-1', displayName: 'Alice' },
557
+ channel_id: 'channel-1',
558
+ thread_id: 'thread-1',
559
+ team_name: 'Team One',
560
+ channel_name: 'General',
561
+ timestamp: '2024-01-01T00:00:00.000Z',
562
+ permalink: 'https://teams.microsoft.com/l/message/msg-1',
563
+ },
564
+ ])
565
+ })
566
+
567
+ it('returns an empty array when Substrate has no nested results', async () => {
568
+ const manager = await setupCredentialManager()
569
+ mockResponse({ access_token: createSearchJwt(), refresh_token: 'rotated-refresh', expires_in: 3600 })
570
+ mockResponse({ EntitySets: [] })
571
+
572
+ const client = await new TeamsClient(manager).login({ token: 'skype-token', region: 'emea' })
573
+ const results = await client.searchMessages('zzimprobablequery_xyz')
574
+
575
+ expect(results).toEqual([])
576
+ })
577
+
578
+ it('uses the logged-in account refresh token when current account differs', async () => {
579
+ const manager = await setupCredentialManager()
580
+ await manager.setDeviceCodeAccount({
581
+ accountType: 'personal',
582
+ token: 'personal-skype-token',
583
+ tokenExpiresAt: '2100-01-01T00:00:00Z',
584
+ aadRefreshToken: 'personal-refresh-token',
585
+ aadClientId: 'personal-client-id',
586
+ teams: {},
587
+ currentTeam: null,
588
+ })
589
+ const searchJwt = createSearchJwt()
590
+ mockResponse({ access_token: searchJwt, refresh_token: 'work-rotated-refresh', expires_in: 3600 })
591
+ mockResponse({ EntitySets: [] })
592
+
593
+ const client = await new TeamsClient(manager).login({ token: 'skype-token', accountType: 'work', region: 'emea' })
594
+ await client.searchMessages('deploy')
595
+
596
+ const tokenRequestBody = new URLSearchParams(String(fetchCalls[0].options?.body))
597
+ expect(tokenRequestBody.get('refresh_token')).toBe('refresh-token')
598
+ expect(tokenRequestBody.get('client_id')).toBe('client-id')
599
+ })
600
+
601
+ it('rejects invalid pagination options', async () => {
602
+ const manager = await setupCredentialManager()
603
+ const client = await new TeamsClient(manager).login({ token: 'skype-token', region: 'emea' })
604
+
605
+ await expect(client.searchMessages('deploy', { limit: Number.NaN })).rejects.toThrow('positive integer')
606
+ await expect(client.searchMessages('deploy', { limit: 0 })).rejects.toThrow('positive integer')
607
+ await expect(client.searchMessages('deploy', { limit: -1 })).rejects.toThrow('positive integer')
608
+ await expect(client.searchMessages('deploy', { from: -1 })).rejects.toThrow('non-negative integer')
609
+ await expect(client.searchMessages('deploy', { from: 1.5 })).rejects.toThrow('non-negative integer')
610
+ expect(fetchCalls).toHaveLength(0)
611
+ })
362
612
  })
363
613
 
364
614
  describe('getMessage', () => {
@@ -490,6 +740,90 @@ describe('TeamsClient', () => {
490
740
  })
491
741
  })
492
742
 
743
+ describe('downloadFile', () => {
744
+ it('downloads SharePoint files through Graph shares with base64url share id', async () => {
745
+ const manager = await setupCredentialManager()
746
+ const shareUrl = 'https://contoso.sharepoint.com/sites/team/Shared%20Documents/report.docx'
747
+ mockResponse([
748
+ { id: 'file1', name: 'report.docx', size: 11, url: shareUrl, contentType: 'application/vnd.ms-word' },
749
+ ])
750
+ mockResponse({ access_token: createGraphJwt(), refresh_token: 'rotated-refresh', expires_in: 3600 })
751
+ mockBinaryResponse('graph-bytes')
752
+
753
+ const client = await new TeamsClient(manager).login({ token: 'skype-token', region: 'emea' })
754
+ const result = await client.downloadFile('111', 'ch1', 'file1')
755
+
756
+ const shareId = `u!${Buffer.from(shareUrl).toString('base64url').replace(/=+$/, '')}`
757
+ expect(fetchCalls[2].url).toBe(`https://graph.microsoft.com/v1.0/shares/${shareId}/driveItem/content`)
758
+ expect(headerValue(fetchCalls[2].options, 'Authorization')).toBe(`Bearer ${createGraphJwt()}`)
759
+ expect(Buffer.from(result.buffer).toString()).toBe('graph-bytes')
760
+ expect(result.file.id).toBe('file1')
761
+ })
762
+
763
+ it('downloads inline object URLs with the Skype token', async () => {
764
+ mockResponse([
765
+ {
766
+ id: 'file2',
767
+ name: 'image.png',
768
+ size: 10,
769
+ url: 'https://teams.microsoft.com/files/image.png',
770
+ object_url: 'https://us-api.asm.skype.com/v1/objects/0-weu-d1/image.png',
771
+ contentType: 'image/png',
772
+ },
773
+ ])
774
+ mockBinaryResponse('image-bytes')
775
+
776
+ const client = await new TeamsClient().login({ token: 'skype-token', region: 'emea' })
777
+ const result = await client.downloadFile('111', 'ch1', 'file2')
778
+
779
+ expect(fetchCalls[1].url).toBe('https://us-api.asm.skype.com/v1/objects/0-weu-d1/image.png')
780
+ expect(headerValue(fetchCalls[1].options, 'Authorization')).toBe('Bearer skype-token')
781
+ expect(headerValue(fetchCalls[1].options, 'X-Skypetoken')).toBe('skype-token')
782
+ expect(Buffer.from(result.buffer).toString()).toBe('image-bytes')
783
+ })
784
+
785
+ it('throws TeamsAuthCapabilityError for SharePoint files with cookie-only credentials', async () => {
786
+ const dir = join(
787
+ import.meta.dir,
788
+ `.test-teams-client-cookie-${Date.now()}-${Math.random().toString(36).slice(2)}`,
789
+ )
790
+ TEMP_DIRS_TO_CLEANUP.push(dir)
791
+ const manager = new TeamsCredentialManager(dir)
792
+ await manager.setToken('skype-token', 'work', '2100-01-01T00:00:00Z')
793
+ mockResponse([
794
+ {
795
+ id: 'file3',
796
+ name: 'deck.pptx',
797
+ size: 10,
798
+ url: 'https://contoso.sharepoint.com/sites/team/Shared%20Documents/deck.pptx',
799
+ },
800
+ ])
801
+
802
+ const client = await new TeamsClient(manager).login({ token: 'skype-token', region: 'emea' })
803
+
804
+ await expect(client.downloadFile('111', 'ch1', 'file3')).rejects.toThrow('Requires `agent-teams auth login`')
805
+ expect(fetchCalls).toHaveLength(1)
806
+ })
807
+
808
+ it('refuses to send the Skype token to an untrusted host', async () => {
809
+ mockResponse([
810
+ {
811
+ id: 'file4',
812
+ name: 'evil.bin',
813
+ size: 10,
814
+ url: 'https://evil.example.com/steal',
815
+ object_url: 'https://evil.example.com/steal',
816
+ },
817
+ ])
818
+
819
+ const client = await new TeamsClient().login({ token: 'skype-token', region: 'emea' })
820
+
821
+ await expect(client.downloadFile('111', 'ch1', 'file4')).rejects.toThrow('untrusted host')
822
+ // only the listFiles call happened — no credentialed download fetch to the untrusted host
823
+ expect(fetchCalls).toHaveLength(1)
824
+ })
825
+ })
826
+
493
827
  describe('rate limiting', () => {
494
828
  it('waits when bucket is exhausted before making request', async () => {
495
829
  mockResponse({ userDetails: JSON.stringify({ name: 'User 1' }), locale: 'en-us' }, 200, {