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,7 +1,10 @@
1
+ import { randomUUID } from 'node:crypto'
1
2
  import { readFile } from 'node:fs/promises'
2
3
  import { basename } from 'node:path'
3
4
 
5
+ import { SUBSTRATE_SEARCH_URL } from './app-config'
4
6
  import { TeamsCredentialManager } from './credential-manager'
7
+ import { TeamsTokenProvider } from './token-provider'
5
8
  import type {
6
9
  TeamsAccountType,
7
10
  TeamsChannel,
@@ -10,6 +13,7 @@ import type {
10
13
  TeamsFile,
11
14
  TeamsMessage,
12
15
  TeamsRegion,
16
+ TeamsSearchResult,
13
17
  TeamsTeam,
14
18
  TeamsUser,
15
19
  } from './types'
@@ -20,12 +24,35 @@ interface RateLimitBucket {
20
24
  resetAt: number
21
25
  }
22
26
 
27
+ interface RawTeamsMessage extends TeamsMessage {
28
+ rootMessageId?: string
29
+ parentMessageId?: string
30
+ }
31
+
32
+ type JsonRecord = Record<string, unknown>
33
+
23
34
  const PERSONAL_MSG_API_BASE = 'https://msgapi.teams.live.com/v1'
24
35
  const CSA_API_BASE = 'https://teams.microsoft.com/api'
25
36
  const MAX_RETRIES = 3
26
37
  const BASE_BACKOFF_MS = 100
27
38
  const DEFAULT_REGION: TeamsRegion = 'amer'
28
39
  const REGIONS: TeamsRegion[] = ['amer', 'emea', 'apac']
40
+ const GRAPH_API_BASE = 'https://graph.microsoft.com/v1.0'
41
+
42
+ // Personal (Teams for Life) skypetokens carry a consumer `skypeid` (e.g.
43
+ // "live:..." or "8:live:..."); work/school tokens carry an org identity. Used
44
+ // only to guess the account type when a caller logs in with a bare token.
45
+ function isPersonalToken(token: string): boolean {
46
+ try {
47
+ const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8')) as {
48
+ skypeid?: string
49
+ }
50
+ const skypeId = payload.skypeid ?? ''
51
+ return skypeId.includes('live:') || skypeId.startsWith('8:live:')
52
+ } catch {
53
+ return false
54
+ }
55
+ }
29
56
 
30
57
  function stripHtml(content: string | undefined): string | undefined {
31
58
  if (content === undefined) return undefined
@@ -40,6 +67,169 @@ function stripHtml(content: string | undefined): string | undefined {
40
67
  .trim()
41
68
  }
42
69
 
70
+ function isRecord(value: unknown): value is JsonRecord {
71
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
72
+ }
73
+
74
+ function stringFrom(record: JsonRecord, keys: string[]): string | undefined {
75
+ for (const key of keys) {
76
+ const value = record[key]
77
+ if (typeof value === 'string' && value.length > 0) return value
78
+ }
79
+ return undefined
80
+ }
81
+
82
+ function recordFrom(record: JsonRecord, keys: string[]): JsonRecord | undefined {
83
+ for (const key of keys) {
84
+ const value = record[key]
85
+ if (isRecord(value)) return value
86
+ }
87
+ return undefined
88
+ }
89
+
90
+ function arrayFrom(record: JsonRecord, keys: string[]): unknown[] {
91
+ for (const key of keys) {
92
+ const value = record[key]
93
+ if (Array.isArray(value)) return value
94
+ }
95
+ return []
96
+ }
97
+
98
+ function propertyValue(record: JsonRecord, names: string[]): string | undefined {
99
+ const properties = arrayFrom(record, ['Properties', 'properties'])
100
+ const normalized = names.map((name) => name.toLowerCase())
101
+ for (const property of properties) {
102
+ if (!isRecord(property)) continue
103
+ const name = stringFrom(property, ['Name', 'name', 'Key', 'key'])
104
+ if (!name || !normalized.includes(name.toLowerCase())) continue
105
+ const value = property.Value ?? property.value
106
+ if (typeof value === 'string' && value.length > 0) return value
107
+ }
108
+ return undefined
109
+ }
110
+
111
+ function resultString(record: JsonRecord, keys: string[]): string | undefined {
112
+ return stringFrom(record, keys) ?? propertyValue(record, keys)
113
+ }
114
+
115
+ function parseSubstrateResult(value: unknown): TeamsSearchResult | null {
116
+ if (!isRecord(value)) return null
117
+ const author = recordFrom(value, ['Author', 'author', 'From', 'from'])
118
+ const id = resultString(value, ['id', 'Id', 'ReferenceId', 'MessageId'])
119
+ const channelId = resultString(value, ['channel_id', 'ChannelId', 'ConversationId', 'ThreadId'])
120
+ if (!id || !channelId) return null
121
+
122
+ return {
123
+ id,
124
+ content:
125
+ stripHtml(resultString(value, ['content', 'Content', 'HitHighlightedSummary', 'Summary', 'Preview'])) ?? '',
126
+ author: {
127
+ id: author ? (stringFrom(author, ['id', 'Id', 'ObjectId']) ?? '') : (propertyValue(value, ['AuthorId']) ?? ''),
128
+ displayName: author
129
+ ? (stringFrom(author, ['displayName', 'DisplayName', 'Name']) ?? 'Unknown')
130
+ : (propertyValue(value, ['AuthorDisplayName', 'Author']) ?? 'Unknown'),
131
+ },
132
+ channel_id: channelId,
133
+ thread_id: resultString(value, ['thread_id', 'ThreadId']),
134
+ team_name: resultString(value, ['team_name', 'TeamName']),
135
+ channel_name: resultString(value, ['channel_name', 'ChannelName']),
136
+ timestamp: resultString(value, ['timestamp', 'Timestamp', 'DateTimeSent', 'LastModifiedTime']) ?? '',
137
+ permalink: resultString(value, ['permalink', 'Permalink', 'WebUrl', 'Url']),
138
+ }
139
+ }
140
+
141
+ function parseSubstrateResults(data: unknown): TeamsSearchResult[] {
142
+ if (!isRecord(data)) return []
143
+ const results: TeamsSearchResult[] = []
144
+ for (const entitySet of arrayFrom(data, ['EntitySets', 'entitySets'])) {
145
+ if (!isRecord(entitySet)) continue
146
+ for (const resultSet of arrayFrom(entitySet, ['ResultSets', 'resultSets'])) {
147
+ if (!isRecord(resultSet)) continue
148
+ for (const rawResult of arrayFrom(resultSet, ['Results', 'results'])) {
149
+ const result = parseSubstrateResult(rawResult)
150
+ if (result) results.push(result)
151
+ }
152
+ }
153
+ }
154
+ return results
155
+ }
156
+
157
+ function validateSearchLimit(value: number | undefined): number {
158
+ if (value === undefined) return 20
159
+ if (!Number.isInteger(value) || value < 1) {
160
+ throw new TeamsError('Search limit must be a positive integer.', 'invalid_pagination')
161
+ }
162
+ return value
163
+ }
164
+
165
+ function validateSearchFrom(value: number | undefined): number {
166
+ if (value === undefined) return 0
167
+ if (!Number.isInteger(value) || value < 0) {
168
+ throw new TeamsError('Search from offset must be a non-negative integer.', 'invalid_pagination')
169
+ }
170
+ return value
171
+ }
172
+
173
+ function isSharePointOrOneDriveUrl(url: string): boolean {
174
+ try {
175
+ const { hostname } = new URL(url)
176
+ const normalizedHost = hostname.toLowerCase()
177
+ return (
178
+ normalizedHost.includes('sharepoint.com') ||
179
+ normalizedHost.includes('-my.sharepoint') ||
180
+ normalizedHost === '1drv.ms' ||
181
+ normalizedHost.endsWith('.1drv.ms') ||
182
+ normalizedHost === 'onedrive.live.com' ||
183
+ normalizedHost.endsWith('.onedrive.live.com')
184
+ )
185
+ } catch {
186
+ return false
187
+ }
188
+ }
189
+
190
+ // Only these hosts receive the Skype token on a raw download fetch. Teams file
191
+ // metadata can carry arbitrary URLs, so we never attach credentials to a host
192
+ // outside this allowlist — that would leak the token to a third party.
193
+ function isTrustedSkypeDownloadHost(url: string): boolean {
194
+ try {
195
+ const host = new URL(url).hostname.toLowerCase()
196
+ return (
197
+ host === 'teams.microsoft.com' ||
198
+ host.endsWith('.teams.microsoft.com') ||
199
+ host.endsWith('.asm.skype.com') ||
200
+ host.endsWith('.asyncgw.teams.microsoft.com') ||
201
+ host === 'substrate.office.com' ||
202
+ host.endsWith('.substrate.office.com')
203
+ )
204
+ } catch {
205
+ return false
206
+ }
207
+ }
208
+
209
+ function getFileDownloadSource(file: TeamsFile): { route: 'graph' | 'skype'; url: string } {
210
+ const shareUrl = [file.sharepoint_url, file.url, file.object_url].find((candidate): candidate is string =>
211
+ Boolean(candidate && isSharePointOrOneDriveUrl(candidate)),
212
+ )
213
+ if (shareUrl) return { route: 'graph', url: shareUrl }
214
+
215
+ const directUrl = file.object_url ?? file.url
216
+ if (!directUrl) {
217
+ throw new TeamsError(`File has no downloadable URL: ${file.id}`, 'file_url_missing')
218
+ }
219
+ if (!isTrustedSkypeDownloadHost(directUrl)) {
220
+ throw new TeamsError(`Refusing to download ${file.id} from an untrusted host: ${directUrl}`, 'file_url_untrusted')
221
+ }
222
+ return { route: 'skype', url: directUrl }
223
+ }
224
+
225
+ async function readDownloadResponse(response: Response, codePrefix: string): Promise<Buffer> {
226
+ if (!response.ok) {
227
+ throw new TeamsError(`File download failed with HTTP ${response.status}`, `${codePrefix}_${response.status}`)
228
+ }
229
+
230
+ return Buffer.from(await response.arrayBuffer())
231
+ }
232
+
43
233
  function escapeHtml(value: string): string {
44
234
  return value
45
235
  .replaceAll('&', '&amp;')
@@ -49,6 +239,23 @@ function escapeHtml(value: string): string {
49
239
  .replaceAll("'", '&#39;')
50
240
  }
51
241
 
242
+ function withThreadMetadata(message: RawTeamsMessage, rootMessageId?: string): TeamsMessage {
243
+ const { rootMessageId: messageRootMessageId, parentMessageId, ...teamsMessage } = message
244
+ const rawRootMessageId = rootMessageId ?? messageRootMessageId
245
+ const isThreadReply = Boolean(
246
+ rootMessageId ||
247
+ (messageRootMessageId !== undefined && messageRootMessageId !== message.id) ||
248
+ (parentMessageId !== undefined && parentMessageId !== message.id),
249
+ )
250
+
251
+ return {
252
+ ...teamsMessage,
253
+ root_message_id: isThreadReply ? rawRootMessageId : undefined,
254
+ parent_message_id: isThreadReply ? (parentMessageId ?? rawRootMessageId) : undefined,
255
+ is_thread_reply: isThreadReply ? true : undefined,
256
+ }
257
+ }
258
+
52
259
  // groupId => Teams/channel thread (handled by listTeams). "48:notes"/
53
260
  // streamofnotes => the user's self ("to me") chat. Anything else without a
54
261
  // non-chat threadType is a normal 1:1 (no topic) or group (has topic) chat.
@@ -68,9 +275,12 @@ export class TeamsClient {
68
275
  private isPersonalAccount: boolean = false
69
276
  private region: TeamsRegion = DEFAULT_REGION
70
277
  private regionDiscovered: boolean = false
278
+ private tokenProvider?: TeamsTokenProvider
71
279
  private buckets: Map<string, RateLimitBucket> = new Map()
72
280
  private globalRateLimitUntil: number = 0
73
281
 
282
+ constructor(private credManager: TeamsCredentialManager = new TeamsCredentialManager()) {}
283
+
74
284
  async login(credentials?: {
75
285
  token: string
76
286
  tokenExpiresAt?: string
@@ -85,18 +295,22 @@ export class TeamsClient {
85
295
  if (credentials.tokenExpiresAt) {
86
296
  this.tokenExpiresAt = new Date(credentials.tokenExpiresAt)
87
297
  }
88
- this.isPersonalAccount = credentials.accountType === 'personal'
298
+ this.isPersonalAccount = credentials.accountType
299
+ ? credentials.accountType === 'personal'
300
+ : isPersonalToken(credentials.token)
89
301
  if (credentials.region) {
90
302
  this.region = credentials.region
91
303
  this.regionDiscovered = true
92
304
  }
305
+ if (credentials.accountType) {
306
+ this.getTokenProvider().bindAccount(credentials.accountType)
307
+ }
93
308
  return this
94
309
  }
95
310
 
96
311
  const { ensureTeamsAuth } = await import('./ensure-auth')
97
312
  await ensureTeamsAuth()
98
- const credManager = new TeamsCredentialManager()
99
- const creds = await credManager.getTokenWithExpiry()
313
+ const creds = await this.credManager.getTokenWithExpiry()
100
314
  if (!creds) {
101
315
  throw new TeamsError(
102
316
  'No Teams credentials found. Make sure Microsoft Teams is logged in via the desktop app or a supported Chromium browser.',
@@ -115,6 +329,20 @@ export class TeamsClient {
115
329
  return this.region
116
330
  }
117
331
 
332
+ getToken(): string {
333
+ return this.ensureAuth()
334
+ }
335
+
336
+ getAccountType(): TeamsAccountType {
337
+ return this.isPersonalAccount ? 'personal' : 'work'
338
+ }
339
+
340
+ async getIdToken(): Promise<string | null> {
341
+ const { TeamsTokenExtractor } = await import('./token-extractor')
342
+ const extractor = new TeamsTokenExtractor()
343
+ return extractor.extractIdToken(this.getAccountType())
344
+ }
345
+
118
346
  private ensureAuth(): string {
119
347
  if (this.token === null) {
120
348
  throw new TeamsError('Not authenticated. Call .login() first.', 'not_authenticated')
@@ -210,7 +438,7 @@ export class TeamsClient {
210
438
 
211
439
  private async request<T>(method: string, path: string, body?: unknown, baseUrl?: string): Promise<T> {
212
440
  if (this.isTokenExpired()) {
213
- throw new TeamsError('Token has expired. Run "auth extract" to refresh.', 'token_expired')
441
+ throw new TeamsError('Token has expired. Run "auth login" or "auth extract" to refresh.', 'token_expired')
214
442
  }
215
443
 
216
444
  if (baseUrl === undefined && !this.regionDiscovered) {
@@ -279,7 +507,7 @@ export class TeamsClient {
279
507
 
280
508
  private async requestFormData<T>(path: string, formData: FormData, baseUrl?: string): Promise<T> {
281
509
  if (this.isTokenExpired()) {
282
- throw new TeamsError('Token has expired. Run "auth extract" to refresh.', 'token_expired')
510
+ throw new TeamsError('Token has expired. Run "auth login" or "auth extract" to refresh.', 'token_expired')
283
511
  }
284
512
 
285
513
  if (baseUrl === undefined && !this.regionDiscovered) {
@@ -474,7 +702,17 @@ export class TeamsClient {
474
702
  )
475
703
  }
476
704
 
477
- async sendMessage(teamId: string, channelId: string, content: string): Promise<TeamsMessage> {
705
+ async sendMessage(teamId: string, channelId: string, content: string, rootMessageId?: string): Promise<TeamsMessage> {
706
+ if (rootMessageId) {
707
+ const response = await this.request<RawTeamsMessage>(
708
+ 'POST',
709
+ `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages/${rootMessageId}/replies`,
710
+ { content, parentMessageId: rootMessageId },
711
+ CSA_API_BASE,
712
+ )
713
+ return withThreadMetadata(response, rootMessageId)
714
+ }
715
+
478
716
  return this.request<TeamsMessage>(
479
717
  'POST',
480
718
  `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages`,
@@ -484,12 +722,75 @@ export class TeamsClient {
484
722
  }
485
723
 
486
724
  async getMessages(teamId: string, channelId: string, limit: number = 50): Promise<TeamsMessage[]> {
487
- return this.request<TeamsMessage[]>(
725
+ const messages = await this.request<RawTeamsMessage[]>(
488
726
  'GET',
489
727
  `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages?limit=${limit}`,
490
728
  undefined,
491
729
  CSA_API_BASE,
492
730
  )
731
+ return messages.map((message) => withThreadMetadata(message))
732
+ }
733
+
734
+ async getThreadReplies(
735
+ teamId: string,
736
+ channelId: string,
737
+ rootMessageId: string,
738
+ limit: number = 50,
739
+ ): Promise<TeamsMessage[]> {
740
+ const replies = await this.request<RawTeamsMessage[]>(
741
+ 'GET',
742
+ `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages/${rootMessageId}/replies?limit=${limit}`,
743
+ undefined,
744
+ CSA_API_BASE,
745
+ )
746
+ return replies.map((reply) => withThreadMetadata(reply, rootMessageId))
747
+ }
748
+
749
+ async searchMessages(query: string, opts: { limit?: number; from?: number } = {}): Promise<TeamsSearchResult[]> {
750
+ const size = validateSearchLimit(opts.limit)
751
+ const from = validateSearchFrom(opts.from)
752
+ const tokenProvider = this.getTokenProvider()
753
+ const substrateToken = await tokenProvider.getSubstrateToken()
754
+ const tenantId = await tokenProvider.getTenantId()
755
+ const userId = await tokenProvider.getUserId()
756
+
757
+ const response = await fetch(SUBSTRATE_SEARCH_URL, {
758
+ method: 'POST',
759
+ headers: {
760
+ Authorization: `Bearer ${substrateToken}`,
761
+ 'Content-Type': 'application/json',
762
+ 'x-anchormailbox': `Oid:${userId}@${tenantId}`,
763
+ },
764
+ body: JSON.stringify({
765
+ cvid: randomUUID(),
766
+ logicalId: randomUUID(),
767
+ query: { queryString: query },
768
+ entityRequests: [
769
+ {
770
+ entityType: 'Message',
771
+ contentSources: ['Teams'],
772
+ from,
773
+ size,
774
+ query: { queryString: query },
775
+ },
776
+ ],
777
+ }),
778
+ })
779
+
780
+ const data = (await response.json().catch(() => ({}))) as unknown
781
+ if (!response.ok) {
782
+ const message = isRecord(data)
783
+ ? stringFrom(data, ['message', 'Message', 'error_description', 'error'])
784
+ : undefined
785
+ throw new TeamsError(message ?? `HTTP ${response.status}`, `substrate_${response.status}`)
786
+ }
787
+
788
+ return parseSubstrateResults(data)
789
+ }
790
+
791
+ private getTokenProvider(): TeamsTokenProvider {
792
+ this.tokenProvider ??= new TeamsTokenProvider(this.credManager)
793
+ return this.tokenProvider
493
794
  }
494
795
 
495
796
  async getMessage(teamId: string, channelId: string, messageId: string): Promise<TeamsMessage> {
@@ -558,4 +859,38 @@ export class TeamsClient {
558
859
  CSA_API_BASE,
559
860
  )
560
861
  }
862
+
863
+ async downloadFile(teamId: string, channelId: string, fileId: string): Promise<{ buffer: Buffer; file: TeamsFile }> {
864
+ const files = await this.listFiles(teamId, channelId)
865
+ const file = files.find((candidate) => candidate.id === fileId)
866
+ if (!file) {
867
+ throw new TeamsError(`File not found: ${fileId}`, 'file_not_found')
868
+ }
869
+
870
+ const source = getFileDownloadSource(file)
871
+ if (source.route === 'graph') {
872
+ const graphToken = await new TeamsTokenProvider(this.credManager).getGraphToken()
873
+ const shareId = `u!${Buffer.from(source.url).toString('base64url').replace(/=+$/, '')}`
874
+ const response = await fetch(`${GRAPH_API_BASE}/shares/${shareId}/driveItem/content`, {
875
+ headers: {
876
+ Authorization: `Bearer ${graphToken}`,
877
+ },
878
+ redirect: 'follow',
879
+ })
880
+ return { buffer: await readDownloadResponse(response, 'graph_download'), file }
881
+ }
882
+
883
+ if (this.isTokenExpired()) {
884
+ throw new TeamsError('Token has expired. Run "auth login" or "auth extract" to refresh.', 'token_expired')
885
+ }
886
+ const skypeToken = this.getToken()
887
+ const response = await fetch(source.url, {
888
+ headers: {
889
+ Authorization: `Bearer ${skypeToken}`,
890
+ 'X-Skypetoken': skypeToken,
891
+ },
892
+ redirect: 'follow',
893
+ })
894
+ return { buffer: await readDownloadResponse(response, 'skype_download'), file }
895
+ }
561
896
  }
@@ -1,9 +1,13 @@
1
1
  import { afterEach, beforeEach, expect, spyOn, it } from 'bun:test'
2
2
 
3
+ import * as interactive from '@/shared/utils/interactive'
4
+
3
5
  import { TeamsClient } from '../client'
4
6
  import { TeamsCredentialManager } from '../credential-manager'
7
+ import * as deviceLogin from '../device-login'
8
+ import * as realmDiscovery from '../realm-discovery'
5
9
  import { TeamsTokenExtractor } from '../token-extractor'
6
- import { getNoTeamsTokenFoundMessage } from './auth'
10
+ import { getNoTeamsTokenFoundMessage, loginAction } from './auth'
7
11
 
8
12
  let extractorExtractSpy: ReturnType<typeof spyOn>
9
13
  let clientTestAuthSpy: ReturnType<typeof spyOn>
@@ -13,6 +17,7 @@ let credManagerSaveConfigSpy: ReturnType<typeof spyOn>
13
17
  let credManagerClearCredentialsSpy: ReturnType<typeof spyOn>
14
18
  let credManagerIsTokenExpiredSpy: ReturnType<typeof spyOn>
15
19
  let clientGetRegionSpy: ReturnType<typeof spyOn>
20
+ const originalConsoleLog = console.log
16
21
 
17
22
  beforeEach(() => {
18
23
  extractorExtractSpy = spyOn(TeamsTokenExtractor.prototype, 'extract').mockResolvedValue([
@@ -52,6 +57,7 @@ afterEach(() => {
52
57
  credManagerClearCredentialsSpy?.mockRestore()
53
58
  credManagerIsTokenExpiredSpy?.mockRestore()
54
59
  clientGetRegionSpy?.mockRestore()
60
+ console.log = originalConsoleLog
55
61
  })
56
62
 
57
63
  it('extract: calls TeamsTokenExtractor', async () => {
@@ -98,3 +104,89 @@ it('status: checks token expiry', async () => {
98
104
  it('no-token message mentions desktop app and browser fallback', () => {
99
105
  expect(getNoTeamsTokenFoundMessage()).toContain('desktop app or a supported Chromium browser')
100
106
  })
107
+
108
+ it('login pending hint preserves explicit account type', async () => {
109
+ const completeSpy = spyOn(deviceLogin, 'completeDeviceCode').mockRejectedValue(new deviceLogin.PendingApprovalError())
110
+ const consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
111
+ const exitSpy = spyOn(process, 'exit').mockImplementation((code?: string | number | null) => {
112
+ throw new Error(`exit:${code}`)
113
+ })
114
+
115
+ await expect(loginAction({ deviceCode: 'device-code', accountType: 'personal', pretty: false })).rejects.toThrow(
116
+ 'exit:1',
117
+ )
118
+
119
+ expect(completeSpy).toHaveBeenCalled()
120
+ expect(exitSpy.mock.calls[0][0]).toBe(0)
121
+ const output = consoleSpy.mock.calls[0][0]
122
+ expect(output).toContain('agent-teams auth login --device-code <device_code> --account-type personal')
123
+ completeSpy.mockRestore()
124
+ consoleSpy.mockRestore()
125
+ exitSpy.mockRestore()
126
+ })
127
+
128
+ it('routes to the personal flow when the email probe reports a personal account', async () => {
129
+ const interactiveSpy = spyOn(interactive, 'isInteractive').mockReturnValue(true)
130
+ const probeSpy = spyOn(realmDiscovery, 'probeAccountType').mockResolvedValue('personal')
131
+ const consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
132
+ const loginSpy = spyOn(deviceLogin, 'loginWithDeviceCode').mockResolvedValue({
133
+ accountType: 'personal',
134
+ userName: 'Personal User',
135
+ teams: [],
136
+ current: null,
137
+ })
138
+
139
+ await loginAction({ email: 'user@outlook.com', pretty: false })
140
+
141
+ expect(probeSpy).toHaveBeenCalledWith('user@outlook.com')
142
+ expect(loginSpy.mock.calls[0][0].accountType).toBe('personal')
143
+
144
+ interactiveSpy.mockRestore()
145
+ probeSpy.mockRestore()
146
+ consoleSpy.mockRestore()
147
+ loginSpy.mockRestore()
148
+ })
149
+
150
+ it('skips the email probe when an account type is forced explicitly', async () => {
151
+ const interactiveSpy = spyOn(interactive, 'isInteractive').mockReturnValue(true)
152
+ const probeSpy = spyOn(realmDiscovery, 'probeAccountType').mockResolvedValue('personal')
153
+ const consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
154
+ const loginSpy = spyOn(deviceLogin, 'loginWithDeviceCode').mockResolvedValue({
155
+ accountType: 'work',
156
+ userName: 'Work User',
157
+ teams: [],
158
+ current: null,
159
+ })
160
+
161
+ await loginAction({ email: 'user@outlook.com', accountType: 'work', pretty: false })
162
+
163
+ expect(probeSpy).not.toHaveBeenCalled()
164
+ expect(loginSpy.mock.calls[0][0].accountType).toBe('work')
165
+
166
+ interactiveSpy.mockRestore()
167
+ probeSpy.mockRestore()
168
+ consoleSpy.mockRestore()
169
+ loginSpy.mockRestore()
170
+ })
171
+
172
+ it('falls back to the default flow when the email probe is inconclusive', async () => {
173
+ const interactiveSpy = spyOn(interactive, 'isInteractive').mockReturnValue(true)
174
+ const probeSpy = spyOn(realmDiscovery, 'probeAccountType').mockResolvedValue(undefined)
175
+ const consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
176
+ const loginSpy = spyOn(deviceLogin, 'loginWithDeviceCode').mockResolvedValue({
177
+ accountType: 'work',
178
+ userName: 'Work User',
179
+ teams: [],
180
+ current: null,
181
+ })
182
+
183
+ await loginAction({ email: 'user@unknown.test', pretty: false })
184
+
185
+ expect(probeSpy).toHaveBeenCalledWith('user@unknown.test')
186
+ expect(loginSpy.mock.calls[0][0].accountType).toBe('work')
187
+
188
+ interactiveSpy.mockRestore()
189
+ probeSpy.mockRestore()
190
+ consoleSpy.mockRestore()
191
+ loginSpy.mockRestore()
192
+ })