agent-messenger 2.10.2 → 2.11.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 (330) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.env.template +4 -1
  3. package/README.md +77 -27
  4. package/bun.lock +26 -0
  5. package/dist/package.json +14 -1
  6. package/dist/src/platforms/channeltalk/commands/auth.d.ts +2 -1
  7. package/dist/src/platforms/channeltalk/commands/auth.d.ts.map +1 -1
  8. package/dist/src/platforms/channeltalk/commands/auth.js +5 -3
  9. package/dist/src/platforms/channeltalk/commands/auth.js.map +1 -1
  10. package/dist/src/platforms/channeltalk/token-extractor.d.ts +2 -1
  11. package/dist/src/platforms/channeltalk/token-extractor.d.ts.map +1 -1
  12. package/dist/src/platforms/channeltalk/token-extractor.js +22 -6
  13. package/dist/src/platforms/channeltalk/token-extractor.js.map +1 -1
  14. package/dist/src/platforms/channeltalkbot/cli.d.ts.map +1 -1
  15. package/dist/src/platforms/channeltalkbot/cli.js +11 -1
  16. package/dist/src/platforms/channeltalkbot/cli.js.map +1 -1
  17. package/dist/src/platforms/channeltalkbot/commands/auth.d.ts.map +1 -1
  18. package/dist/src/platforms/channeltalkbot/commands/auth.js +1 -5
  19. package/dist/src/platforms/channeltalkbot/commands/auth.js.map +1 -1
  20. package/dist/src/platforms/channeltalkbot/commands/bot.d.ts.map +1 -1
  21. package/dist/src/platforms/channeltalkbot/commands/bot.js +1 -6
  22. package/dist/src/platforms/channeltalkbot/commands/bot.js.map +1 -1
  23. package/dist/src/platforms/channeltalkbot/commands/chat.d.ts.map +1 -1
  24. package/dist/src/platforms/channeltalkbot/commands/chat.js +1 -6
  25. package/dist/src/platforms/channeltalkbot/commands/chat.js.map +1 -1
  26. package/dist/src/platforms/channeltalkbot/commands/group.d.ts.map +1 -1
  27. package/dist/src/platforms/channeltalkbot/commands/group.js +1 -6
  28. package/dist/src/platforms/channeltalkbot/commands/group.js.map +1 -1
  29. package/dist/src/platforms/channeltalkbot/commands/manager.d.ts.map +1 -1
  30. package/dist/src/platforms/channeltalkbot/commands/manager.js +1 -6
  31. package/dist/src/platforms/channeltalkbot/commands/manager.js.map +1 -1
  32. package/dist/src/platforms/channeltalkbot/commands/message.d.ts.map +1 -1
  33. package/dist/src/platforms/channeltalkbot/commands/message.js +1 -6
  34. package/dist/src/platforms/channeltalkbot/commands/message.js.map +1 -1
  35. package/dist/src/platforms/channeltalkbot/commands/whoami.d.ts.map +1 -1
  36. package/dist/src/platforms/channeltalkbot/commands/whoami.js +1 -6
  37. package/dist/src/platforms/channeltalkbot/commands/whoami.js.map +1 -1
  38. package/dist/src/platforms/channeltalkbot/credential-manager.d.ts +5 -0
  39. package/dist/src/platforms/channeltalkbot/credential-manager.d.ts.map +1 -1
  40. package/dist/src/platforms/channeltalkbot/credential-manager.js +34 -4
  41. package/dist/src/platforms/channeltalkbot/credential-manager.js.map +1 -1
  42. package/dist/src/platforms/discord/commands/auth.d.ts +1 -0
  43. package/dist/src/platforms/discord/commands/auth.d.ts.map +1 -1
  44. package/dist/src/platforms/discord/commands/auth.js +3 -1
  45. package/dist/src/platforms/discord/commands/auth.js.map +1 -1
  46. package/dist/src/platforms/discord/listener.d.ts +2 -0
  47. package/dist/src/platforms/discord/listener.d.ts.map +1 -1
  48. package/dist/src/platforms/discord/listener.js +51 -21
  49. package/dist/src/platforms/discord/listener.js.map +1 -1
  50. package/dist/src/platforms/discord/token-extractor.d.ts +2 -1
  51. package/dist/src/platforms/discord/token-extractor.d.ts.map +1 -1
  52. package/dist/src/platforms/discord/token-extractor.js +21 -6
  53. package/dist/src/platforms/discord/token-extractor.js.map +1 -1
  54. package/dist/src/platforms/discordbot/cli.d.ts.map +1 -1
  55. package/dist/src/platforms/discordbot/cli.js +12 -1
  56. package/dist/src/platforms/discordbot/cli.js.map +1 -1
  57. package/dist/src/platforms/discordbot/client.d.ts +3 -0
  58. package/dist/src/platforms/discordbot/client.d.ts.map +1 -1
  59. package/dist/src/platforms/discordbot/client.js +3 -0
  60. package/dist/src/platforms/discordbot/client.js.map +1 -1
  61. package/dist/src/platforms/discordbot/commands/auth.d.ts.map +1 -1
  62. package/dist/src/platforms/discordbot/commands/auth.js +1 -5
  63. package/dist/src/platforms/discordbot/commands/auth.js.map +1 -1
  64. package/dist/src/platforms/discordbot/commands/message.d.ts.map +1 -1
  65. package/dist/src/platforms/discordbot/commands/message.js +1 -6
  66. package/dist/src/platforms/discordbot/commands/message.js.map +1 -1
  67. package/dist/src/platforms/discordbot/commands/server.d.ts.map +1 -1
  68. package/dist/src/platforms/discordbot/commands/server.js +1 -4
  69. package/dist/src/platforms/discordbot/commands/server.js.map +1 -1
  70. package/dist/src/platforms/discordbot/commands/whoami.d.ts.map +1 -1
  71. package/dist/src/platforms/discordbot/commands/whoami.js +1 -6
  72. package/dist/src/platforms/discordbot/commands/whoami.js.map +1 -1
  73. package/dist/src/platforms/discordbot/index.d.ts +3 -1
  74. package/dist/src/platforms/discordbot/index.d.ts.map +1 -1
  75. package/dist/src/platforms/discordbot/index.js +2 -1
  76. package/dist/src/platforms/discordbot/index.js.map +1 -1
  77. package/dist/src/platforms/discordbot/listener.d.ts +43 -0
  78. package/dist/src/platforms/discordbot/listener.d.ts.map +1 -0
  79. package/dist/src/platforms/discordbot/listener.js +292 -0
  80. package/dist/src/platforms/discordbot/listener.js.map +1 -0
  81. package/dist/src/platforms/discordbot/types.d.ts +161 -0
  82. package/dist/src/platforms/discordbot/types.d.ts.map +1 -1
  83. package/dist/src/platforms/discordbot/types.js +34 -0
  84. package/dist/src/platforms/discordbot/types.js.map +1 -1
  85. package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
  86. package/dist/src/platforms/instagram/commands/auth.js +3 -1
  87. package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
  88. package/dist/src/platforms/instagram/token-extractor.d.ts +2 -1
  89. package/dist/src/platforms/instagram/token-extractor.d.ts.map +1 -1
  90. package/dist/src/platforms/instagram/token-extractor.js +11 -2
  91. package/dist/src/platforms/instagram/token-extractor.js.map +1 -1
  92. package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
  93. package/dist/src/platforms/slack/commands/auth.js +4 -2
  94. package/dist/src/platforms/slack/commands/auth.js.map +1 -1
  95. package/dist/src/platforms/slack/token-extractor.d.ts +4 -1
  96. package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
  97. package/dist/src/platforms/slack/token-extractor.js +64 -15
  98. package/dist/src/platforms/slack/token-extractor.js.map +1 -1
  99. package/dist/src/platforms/slackbot/cli.d.ts.map +1 -1
  100. package/dist/src/platforms/slackbot/cli.js +15 -3
  101. package/dist/src/platforms/slackbot/cli.js.map +1 -1
  102. package/dist/src/platforms/slackbot/client.d.ts +22 -1
  103. package/dist/src/platforms/slackbot/client.d.ts.map +1 -1
  104. package/dist/src/platforms/slackbot/client.js +104 -1
  105. package/dist/src/platforms/slackbot/client.js.map +1 -1
  106. package/dist/src/platforms/slackbot/commands/auth.d.ts.map +1 -1
  107. package/dist/src/platforms/slackbot/commands/auth.js +1 -5
  108. package/dist/src/platforms/slackbot/commands/auth.js.map +1 -1
  109. package/dist/src/platforms/slackbot/commands/file.d.ts +3 -0
  110. package/dist/src/platforms/slackbot/commands/file.d.ts.map +1 -0
  111. package/dist/src/platforms/slackbot/commands/file.js +164 -0
  112. package/dist/src/platforms/slackbot/commands/file.js.map +1 -0
  113. package/dist/src/platforms/slackbot/commands/index.d.ts +1 -0
  114. package/dist/src/platforms/slackbot/commands/index.d.ts.map +1 -1
  115. package/dist/src/platforms/slackbot/commands/index.js +1 -0
  116. package/dist/src/platforms/slackbot/commands/index.js.map +1 -1
  117. package/dist/src/platforms/slackbot/commands/message.d.ts.map +1 -1
  118. package/dist/src/platforms/slackbot/commands/message.js +19 -0
  119. package/dist/src/platforms/slackbot/commands/message.js.map +1 -1
  120. package/dist/src/platforms/slackbot/commands/whoami.d.ts.map +1 -1
  121. package/dist/src/platforms/slackbot/commands/whoami.js +1 -6
  122. package/dist/src/platforms/slackbot/commands/whoami.js.map +1 -1
  123. package/dist/src/platforms/slackbot/credential-manager.d.ts +1 -0
  124. package/dist/src/platforms/slackbot/credential-manager.d.ts.map +1 -1
  125. package/dist/src/platforms/slackbot/credential-manager.js +30 -2
  126. package/dist/src/platforms/slackbot/credential-manager.js.map +1 -1
  127. package/dist/src/platforms/slackbot/index.d.ts +4 -1
  128. package/dist/src/platforms/slackbot/index.d.ts.map +1 -1
  129. package/dist/src/platforms/slackbot/index.js +1 -0
  130. package/dist/src/platforms/slackbot/index.js.map +1 -1
  131. package/dist/src/platforms/slackbot/listener.d.ts +44 -0
  132. package/dist/src/platforms/slackbot/listener.d.ts.map +1 -0
  133. package/dist/src/platforms/slackbot/listener.js +313 -0
  134. package/dist/src/platforms/slackbot/listener.js.map +1 -0
  135. package/dist/src/platforms/slackbot/types.d.ts +196 -1
  136. package/dist/src/platforms/slackbot/types.d.ts.map +1 -1
  137. package/dist/src/platforms/slackbot/types.js +4 -1
  138. package/dist/src/platforms/slackbot/types.js.map +1 -1
  139. package/dist/src/platforms/teams/commands/auth.d.ts +1 -0
  140. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
  141. package/dist/src/platforms/teams/commands/auth.js +37 -6
  142. package/dist/src/platforms/teams/commands/auth.js.map +1 -1
  143. package/dist/src/platforms/teams/ensure-auth.js +31 -9
  144. package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
  145. package/dist/src/platforms/teams/token-extractor.d.ts +4 -1
  146. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  147. package/dist/src/platforms/teams/token-extractor.js +71 -29
  148. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  149. package/dist/src/platforms/webex/commands/auth.d.ts +1 -0
  150. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
  151. package/dist/src/platforms/webex/commands/auth.js +3 -1
  152. package/dist/src/platforms/webex/commands/auth.js.map +1 -1
  153. package/dist/src/platforms/webex/token-extractor.d.ts +3 -1
  154. package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -1
  155. package/dist/src/platforms/webex/token-extractor.js +16 -2
  156. package/dist/src/platforms/webex/token-extractor.js.map +1 -1
  157. package/dist/src/platforms/wechatbot/cli.d.ts.map +1 -1
  158. package/dist/src/platforms/wechatbot/cli.js +11 -1
  159. package/dist/src/platforms/wechatbot/cli.js.map +1 -1
  160. package/dist/src/platforms/wechatbot/commands/auth.d.ts.map +1 -1
  161. package/dist/src/platforms/wechatbot/commands/auth.js +1 -5
  162. package/dist/src/platforms/wechatbot/commands/auth.js.map +1 -1
  163. package/dist/src/platforms/wechatbot/commands/message.d.ts.map +1 -1
  164. package/dist/src/platforms/wechatbot/commands/message.js +1 -6
  165. package/dist/src/platforms/wechatbot/commands/message.js.map +1 -1
  166. package/dist/src/platforms/wechatbot/commands/template.d.ts.map +1 -1
  167. package/dist/src/platforms/wechatbot/commands/template.js +1 -6
  168. package/dist/src/platforms/wechatbot/commands/template.js.map +1 -1
  169. package/dist/src/platforms/wechatbot/commands/user.d.ts.map +1 -1
  170. package/dist/src/platforms/wechatbot/commands/user.js +1 -6
  171. package/dist/src/platforms/wechatbot/commands/user.js.map +1 -1
  172. package/dist/src/platforms/wechatbot/commands/whoami.d.ts.map +1 -1
  173. package/dist/src/platforms/wechatbot/commands/whoami.js +1 -6
  174. package/dist/src/platforms/wechatbot/commands/whoami.js.map +1 -1
  175. package/dist/src/platforms/whatsappbot/cli.d.ts.map +1 -1
  176. package/dist/src/platforms/whatsappbot/cli.js +11 -1
  177. package/dist/src/platforms/whatsappbot/cli.js.map +1 -1
  178. package/dist/src/platforms/whatsappbot/commands/auth.d.ts.map +1 -1
  179. package/dist/src/platforms/whatsappbot/commands/auth.js +1 -5
  180. package/dist/src/platforms/whatsappbot/commands/auth.js.map +1 -1
  181. package/dist/src/platforms/whatsappbot/commands/message.d.ts.map +1 -1
  182. package/dist/src/platforms/whatsappbot/commands/message.js +1 -6
  183. package/dist/src/platforms/whatsappbot/commands/message.js.map +1 -1
  184. package/dist/src/platforms/whatsappbot/commands/template.d.ts.map +1 -1
  185. package/dist/src/platforms/whatsappbot/commands/template.js +1 -6
  186. package/dist/src/platforms/whatsappbot/commands/template.js.map +1 -1
  187. package/dist/src/platforms/whatsappbot/commands/whoami.d.ts.map +1 -1
  188. package/dist/src/platforms/whatsappbot/commands/whoami.js +1 -6
  189. package/dist/src/platforms/whatsappbot/commands/whoami.js.map +1 -1
  190. package/dist/src/shared/chromium/browsers.d.ts +8 -0
  191. package/dist/src/shared/chromium/browsers.d.ts.map +1 -1
  192. package/dist/src/shared/chromium/browsers.js +58 -3
  193. package/dist/src/shared/chromium/browsers.js.map +1 -1
  194. package/dist/src/shared/chromium/cli-options.d.ts +5 -0
  195. package/dist/src/shared/chromium/cli-options.d.ts.map +1 -0
  196. package/dist/src/shared/chromium/cli-options.js +8 -0
  197. package/dist/src/shared/chromium/cli-options.js.map +1 -0
  198. package/dist/src/shared/chromium/index.d.ts +3 -1
  199. package/dist/src/shared/chromium/index.d.ts.map +1 -1
  200. package/dist/src/shared/chromium/index.js +2 -1
  201. package/dist/src/shared/chromium/index.js.map +1 -1
  202. package/dist/src/shared/utils/cli-output.d.ts +7 -0
  203. package/dist/src/shared/utils/cli-output.d.ts.map +1 -0
  204. package/dist/src/shared/utils/cli-output.js +7 -0
  205. package/dist/src/shared/utils/cli-output.js.map +1 -0
  206. package/dist/src/tui/app.d.ts.map +1 -1
  207. package/dist/src/tui/app.js +73 -20
  208. package/dist/src/tui/app.js.map +1 -1
  209. package/docs/content/docs/cli/channeltalk.mdx +4 -0
  210. package/docs/content/docs/cli/discord.mdx +5 -0
  211. package/docs/content/docs/cli/instagram.mdx +3 -0
  212. package/docs/content/docs/cli/slack.mdx +5 -0
  213. package/docs/content/docs/cli/slackbot.mdx +60 -22
  214. package/docs/content/docs/cli/teams.mdx +5 -0
  215. package/docs/content/docs/cli/webex.mdx +3 -0
  216. package/docs/content/docs/sdk/channeltalkbot.mdx +38 -1
  217. package/docs/content/docs/sdk/discordbot.mdx +501 -0
  218. package/docs/content/docs/sdk/meta.json +2 -0
  219. package/docs/content/docs/sdk/slackbot.mdx +576 -0
  220. package/e2e/README.md +1 -1
  221. package/e2e/config.ts +9 -4
  222. package/examples/discordbot-listen.ts +65 -0
  223. package/examples/slackbot-listen.ts +65 -0
  224. package/package.json +14 -1
  225. package/skills/agent-channeltalk/SKILL.md +5 -1
  226. package/skills/agent-channeltalk/references/authentication.md +5 -1
  227. package/skills/agent-channeltalkbot/SKILL.md +17 -3
  228. package/skills/agent-channeltalkbot/references/authentication.md +7 -5
  229. package/skills/agent-discord/SKILL.md +5 -1
  230. package/skills/agent-discord/references/authentication.md +7 -1
  231. package/skills/agent-discordbot/SKILL.md +13 -2
  232. package/skills/agent-discordbot/references/common-patterns.md +1 -1
  233. package/skills/agent-instagram/SKILL.md +7 -1
  234. package/skills/agent-instagram/references/authentication.md +6 -0
  235. package/skills/agent-kakaotalk/SKILL.md +1 -1
  236. package/skills/agent-line/SKILL.md +1 -1
  237. package/skills/agent-slack/SKILL.md +5 -1
  238. package/skills/agent-slack/references/authentication.md +7 -1
  239. package/skills/agent-slackbot/SKILL.md +56 -4
  240. package/skills/agent-slackbot/references/authentication.md +4 -0
  241. package/skills/agent-teams/SKILL.md +5 -1
  242. package/skills/agent-teams/references/authentication.md +7 -1
  243. package/skills/agent-telegram/SKILL.md +1 -1
  244. package/skills/agent-webex/SKILL.md +7 -1
  245. package/skills/agent-webex/references/authentication.md +6 -0
  246. package/skills/agent-wechatbot/SKILL.md +16 -1
  247. package/skills/agent-wechatbot/references/authentication.md +219 -0
  248. package/skills/agent-wechatbot/references/common-patterns.md +358 -0
  249. package/skills/agent-wechatbot/templates/account-summary.sh +122 -0
  250. package/skills/agent-wechatbot/templates/post-message.sh +122 -0
  251. package/skills/agent-wechatbot/templates/send-template.sh +152 -0
  252. package/skills/agent-whatsapp/SKILL.md +1 -1
  253. package/skills/agent-whatsappbot/SKILL.md +30 -1
  254. package/src/platforms/channeltalk/commands/auth.test.ts +15 -3
  255. package/src/platforms/channeltalk/commands/auth.ts +15 -5
  256. package/src/platforms/channeltalk/token-extractor.ts +24 -5
  257. package/src/platforms/channeltalkbot/cli.ts +9 -0
  258. package/src/platforms/channeltalkbot/commands/auth.ts +1 -5
  259. package/src/platforms/channeltalkbot/commands/bot.ts +1 -6
  260. package/src/platforms/channeltalkbot/commands/chat.ts +1 -6
  261. package/src/platforms/channeltalkbot/commands/group.ts +1 -6
  262. package/src/platforms/channeltalkbot/commands/manager.ts +1 -6
  263. package/src/platforms/channeltalkbot/commands/message.ts +1 -6
  264. package/src/platforms/channeltalkbot/commands/whoami.test.ts +2 -0
  265. package/src/platforms/channeltalkbot/commands/whoami.ts +1 -6
  266. package/src/platforms/channeltalkbot/credential-manager.test.ts +96 -2
  267. package/src/platforms/channeltalkbot/credential-manager.ts +37 -4
  268. package/src/platforms/discord/commands/auth.ts +13 -2
  269. package/src/platforms/discord/listener.test.ts +59 -1
  270. package/src/platforms/discord/listener.ts +43 -19
  271. package/src/platforms/discord/token-extractor.ts +30 -6
  272. package/src/platforms/discordbot/cli.ts +10 -0
  273. package/src/platforms/discordbot/client.ts +4 -0
  274. package/src/platforms/discordbot/commands/auth.ts +1 -5
  275. package/src/platforms/discordbot/commands/message.ts +1 -6
  276. package/src/platforms/discordbot/commands/server.ts +1 -5
  277. package/src/platforms/discordbot/commands/whoami.ts +1 -6
  278. package/src/platforms/discordbot/index.test.ts +82 -0
  279. package/src/platforms/discordbot/index.ts +27 -9
  280. package/src/platforms/discordbot/listener.test.ts +1002 -0
  281. package/src/platforms/discordbot/listener.ts +321 -0
  282. package/src/platforms/discordbot/types.ts +163 -0
  283. package/src/platforms/instagram/commands/auth.ts +9 -1
  284. package/src/platforms/instagram/token-extractor.ts +13 -1
  285. package/src/platforms/slack/commands/auth.ts +11 -2
  286. package/src/platforms/slack/token-extractor.test.ts +96 -0
  287. package/src/platforms/slack/token-extractor.ts +76 -13
  288. package/src/platforms/slackbot/cli.ts +13 -1
  289. package/src/platforms/slackbot/client.test.ts +274 -0
  290. package/src/platforms/slackbot/client.ts +130 -2
  291. package/src/platforms/slackbot/commands/auth.ts +1 -5
  292. package/src/platforms/slackbot/commands/file.test.ts +201 -0
  293. package/src/platforms/slackbot/commands/file.ts +212 -0
  294. package/src/platforms/slackbot/commands/index.ts +1 -0
  295. package/src/platforms/slackbot/commands/message.ts +22 -0
  296. package/src/platforms/slackbot/commands/whoami.ts +1 -6
  297. package/src/platforms/slackbot/credential-manager.test.ts +62 -2
  298. package/src/platforms/slackbot/credential-manager.ts +32 -2
  299. package/src/platforms/slackbot/index.test.ts +59 -0
  300. package/src/platforms/slackbot/index.ts +31 -7
  301. package/src/platforms/slackbot/listener.test.ts +1012 -0
  302. package/src/platforms/slackbot/listener.ts +362 -0
  303. package/src/platforms/slackbot/types.ts +224 -1
  304. package/src/platforms/teams/commands/auth.test.ts +1 -1
  305. package/src/platforms/teams/commands/auth.ts +66 -7
  306. package/src/platforms/teams/ensure-auth.test.ts +56 -5
  307. package/src/platforms/teams/ensure-auth.ts +39 -11
  308. package/src/platforms/teams/token-extractor.test.ts +146 -24
  309. package/src/platforms/teams/token-extractor.ts +87 -29
  310. package/src/platforms/webex/commands/auth.ts +13 -2
  311. package/src/platforms/webex/token-extractor.ts +25 -3
  312. package/src/platforms/wechatbot/cli.ts +9 -0
  313. package/src/platforms/wechatbot/commands/auth.ts +1 -5
  314. package/src/platforms/wechatbot/commands/message.ts +1 -6
  315. package/src/platforms/wechatbot/commands/template.ts +1 -6
  316. package/src/platforms/wechatbot/commands/user.ts +1 -6
  317. package/src/platforms/wechatbot/commands/whoami.ts +1 -6
  318. package/src/platforms/whatsappbot/cli.ts +9 -0
  319. package/src/platforms/whatsappbot/commands/auth.ts +1 -5
  320. package/src/platforms/whatsappbot/commands/message.ts +1 -6
  321. package/src/platforms/whatsappbot/commands/template.ts +1 -6
  322. package/src/platforms/whatsappbot/commands/whoami.ts +1 -6
  323. package/src/shared/chromium/browsers.test.ts +80 -0
  324. package/src/shared/chromium/browsers.ts +72 -3
  325. package/src/shared/chromium/cli-options.test.ts +22 -0
  326. package/src/shared/chromium/cli-options.ts +12 -0
  327. package/src/shared/chromium/index.ts +3 -0
  328. package/src/shared/utils/cli-output.test.ts +57 -0
  329. package/src/shared/utils/cli-output.ts +8 -0
  330. package/src/tui/app.ts +129 -20
@@ -1,5 +1,6 @@
1
1
  import { Command } from 'commander'
2
2
 
3
+ import { collectBrowserProfileOption } from '@/shared/chromium'
3
4
  import { handleError } from '@/shared/utils/error-handler'
4
5
  import { formatOutput } from '@/shared/utils/output'
5
6
  import { debug } from '@/shared/utils/stderr'
@@ -9,7 +10,46 @@ import { TeamsCredentialManager } from '../credential-manager'
9
10
  import { TeamsTokenExtractor } from '../token-extractor'
10
11
  import type { TeamsAccount, TeamsAccountType, TeamsConfig } from '../types'
11
12
 
12
- export async function extractAction(options: { pretty?: boolean; debug?: boolean; token?: string }): Promise<void> {
13
+ interface ValidatedTeamsToken {
14
+ client: TeamsClient
15
+ accountType: TeamsAccountType
16
+ authInfo: { id: string; displayName: string }
17
+ }
18
+
19
+ async function validateTokenWithProbe(
20
+ token: string,
21
+ accountType: TeamsAccountType,
22
+ accountTypeKnown: boolean,
23
+ debugLog?: (msg: string) => void,
24
+ ): Promise<ValidatedTeamsToken> {
25
+ const primaryTypes: TeamsAccountType[] = [accountType]
26
+ if (!accountTypeKnown) {
27
+ primaryTypes.push(accountType === 'work' ? 'personal' : 'work')
28
+ }
29
+
30
+ let lastError: Error | null = null
31
+ for (const candidateType of primaryTypes) {
32
+ try {
33
+ const client = await new TeamsClient().login({ token, accountType: candidateType })
34
+ const authInfo = await client.testAuth()
35
+ if (candidateType !== accountType) {
36
+ debugLog?.(`[debug] Reclassified ${accountType} → ${candidateType} via API probe`)
37
+ }
38
+ return { client, accountType: candidateType, authInfo }
39
+ } catch (error) {
40
+ lastError = error as Error
41
+ debugLog?.(`[debug] Probe ${candidateType} failed: ${lastError.message}`)
42
+ }
43
+ }
44
+ throw lastError ?? new Error('Token validation failed')
45
+ }
46
+
47
+ export async function extractAction(options: {
48
+ pretty?: boolean
49
+ debug?: boolean
50
+ token?: string
51
+ browserProfile?: string[]
52
+ }): Promise<void> {
13
53
  try {
14
54
  if (options.token) {
15
55
  await extractManualToken(options.token, options)
@@ -17,7 +57,7 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
17
57
  }
18
58
 
19
59
  const debugLog = options.debug ? (msg: string) => debug(`[debug] ${msg}`) : undefined
20
- const extractor = new TeamsTokenExtractor(undefined, undefined, debugLog)
60
+ const extractor = new TeamsTokenExtractor(undefined, undefined, debugLog, options.browserProfile)
21
61
 
22
62
  if (process.platform === 'darwin') {
23
63
  console.log('')
@@ -67,20 +107,33 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
67
107
  teams: string[]
68
108
  }> = []
69
109
 
70
- for (const { token, accountType } of extracted) {
110
+ for (const { token, accountType: extractedType, accountTypeKnown } of extracted) {
71
111
  if (options.debug) {
72
- debug(`[debug] Validating ${accountType} account token...`)
112
+ const label = accountTypeKnown ? extractedType : `${extractedType} (probing)`
113
+ debug(`[debug] Validating ${label} account token...`)
73
114
  }
74
115
 
75
116
  try {
76
- const client = await new TeamsClient().login({ token, accountType })
77
- const authInfo = await client.testAuth()
117
+ const debugLog = options.debug ? (msg: string) => debug(msg) : undefined
118
+ const { client, accountType, authInfo } = await validateTokenWithProbe(
119
+ token,
120
+ extractedType,
121
+ accountTypeKnown,
122
+ debugLog,
123
+ )
78
124
  const teams = await client.listTeams()
79
125
 
80
126
  if (options.debug) {
81
127
  debug(`[debug] ✓ ${accountType}: ${authInfo.displayName} (${teams.length} team(s))`)
82
128
  }
83
129
 
130
+ if (config.accounts[accountType]) {
131
+ if (options.debug) {
132
+ debug(`[debug] Skipping duplicate ${accountType} account`)
133
+ }
134
+ continue
135
+ }
136
+
84
137
  const teamMap: Record<string, { team_id: string; team_name: string }> = {}
85
138
  for (const team of teams) {
86
139
  teamMap[team.id] = { team_id: team.id, team_name: team.name }
@@ -110,7 +163,7 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
110
163
  const errorMessage = (error as Error).message
111
164
  const is401 = errorMessage.includes('401') || errorMessage.includes('Unauthorized')
112
165
  if (options.debug) {
113
- debug(`[debug] ✗ ${accountType}: ${errorMessage}`)
166
+ debug(`[debug] ✗ ${extractedType}: ${errorMessage}`)
114
167
  }
115
168
  if (extracted.length === 1) {
116
169
  console.log(
@@ -380,6 +433,12 @@ export const authCommand = new Command('auth')
380
433
  .option('--pretty', 'Pretty print JSON output')
381
434
  .option('--debug', 'Show debug output for troubleshooting')
382
435
  .option('--token <token>', 'Manually provide a token (bypasses auto-extraction)')
436
+ .option(
437
+ '--browser-profile <path>',
438
+ 'Additional Chromium profile/user-data directory to scan (repeatable, comma-separated supported)',
439
+ collectBrowserProfileOption,
440
+ [],
441
+ )
383
442
  .action(extractAction),
384
443
  )
385
444
  .addCommand(
@@ -16,7 +16,7 @@ beforeEach(() => {
16
16
  loadConfigSpy = spyOn(TeamsCredentialManager.prototype, 'loadConfig').mockResolvedValue(null)
17
17
 
18
18
  extractSpy = spyOn(TeamsTokenExtractor.prototype, 'extract').mockResolvedValue([
19
- { token: 'test-teams-token', accountType: 'work' },
19
+ { token: 'test-teams-token', accountType: 'work', accountTypeKnown: true },
20
20
  ])
21
21
 
22
22
  testAuthSpy = spyOn(TeamsClient.prototype, 'testAuth').mockResolvedValue({
@@ -199,8 +199,8 @@ describe('ensureTeamsAuth', () => {
199
199
  it('extracts and saves multiple accounts', async () => {
200
200
  // given
201
201
  extractSpy.mockResolvedValue([
202
- { token: 'work-token', accountType: 'work' },
203
- { token: 'personal-token', accountType: 'personal' },
202
+ { token: 'work-token', accountType: 'work', accountTypeKnown: true },
203
+ { token: 'personal-token', accountType: 'personal', accountTypeKnown: true },
204
204
  ])
205
205
 
206
206
  testAuthSpy
@@ -229,8 +229,8 @@ describe('ensureTeamsAuth', () => {
229
229
  it('skips failed account but saves successful ones', async () => {
230
230
  // given
231
231
  extractSpy.mockResolvedValue([
232
- { token: 'work-token', accountType: 'work' },
233
- { token: 'bad-token', accountType: 'personal' },
232
+ { token: 'work-token', accountType: 'work', accountTypeKnown: true },
233
+ { token: 'bad-token', accountType: 'personal', accountTypeKnown: true },
234
234
  ])
235
235
 
236
236
  testAuthSpy
@@ -248,6 +248,57 @@ describe('ensureTeamsAuth', () => {
248
248
  expect(savedConfig.accounts.personal).toBeUndefined()
249
249
  })
250
250
 
251
+ // Regression for #163: browser-sourced tokens arrive with accountTypeKnown=false and
252
+ // a guessed accountType of 'work'. If the work endpoint rejects them, we must probe
253
+ // the personal endpoint before giving up — otherwise personal accounts logged in via
254
+ // browser would always fail validation.
255
+ it('reclassifies browser-sourced token from work to personal when work probe fails', async () => {
256
+ // given: extractor returns a token guessed as work but with unknown flag
257
+ extractSpy.mockResolvedValue([{ token: 'browser-token', accountType: 'work', accountTypeKnown: false }])
258
+
259
+ // first testAuth call (work) fails, second (personal) succeeds
260
+ testAuthSpy
261
+ .mockRejectedValueOnce(new Error('HTTP 403'))
262
+ .mockResolvedValueOnce({ id: 'user-p', displayName: 'Personal User' })
263
+
264
+ listTeamsSpy.mockResolvedValueOnce([{ id: 'team-p', name: 'Personal Chat' }])
265
+
266
+ // when
267
+ await ensureTeamsAuth()
268
+
269
+ // then: saved under 'personal', not 'work'
270
+ const savedConfig = saveConfigSpy.mock.calls[0][0]
271
+ expect(savedConfig.accounts.personal).toBeDefined()
272
+ expect(savedConfig.accounts.personal.account_type).toBe('personal')
273
+ expect(savedConfig.accounts.personal.token).toBe('browser-token')
274
+ expect(savedConfig.accounts.work).toBeUndefined()
275
+ })
276
+
277
+ // Regression for #163: when a known-type desktop token and an unknown-type browser
278
+ // token are both extracted, the loop must not overwrite the desktop account's data.
279
+ it('does not overwrite existing account when a later token reclassifies to same type', async () => {
280
+ // given: desktop personal token and browser token that probes to personal
281
+ extractSpy.mockResolvedValue([
282
+ { token: 'desktop-personal-token', accountType: 'personal', accountTypeKnown: true },
283
+ { token: 'browser-token', accountType: 'work', accountTypeKnown: false },
284
+ ])
285
+
286
+ testAuthSpy
287
+ .mockResolvedValueOnce({ id: 'user-p', displayName: 'Desktop Personal' })
288
+ .mockRejectedValueOnce(new Error('HTTP 403'))
289
+ .mockResolvedValueOnce({ id: 'user-p', displayName: 'Browser Personal' })
290
+
291
+ listTeamsSpy.mockResolvedValueOnce([{ id: 'team-p', name: 'Personal' }])
292
+
293
+ // when
294
+ await ensureTeamsAuth()
295
+
296
+ // then: desktop token wins; browser token is skipped because personal already exists
297
+ const savedConfig = saveConfigSpy.mock.calls[0][0]
298
+ expect(savedConfig.accounts.personal.token).toBe('desktop-personal-token')
299
+ expect(savedConfig.accounts.work).toBeUndefined()
300
+ })
301
+
251
302
  it('re-extracts when token is empty string', async () => {
252
303
  // given
253
304
  loadConfigSpy.mockResolvedValue({
@@ -3,7 +3,7 @@ import { warn } from '@/shared/utils/stderr'
3
3
  import { TeamsClient } from './client'
4
4
  import { TeamsCredentialManager } from './credential-manager'
5
5
  import { TeamsTokenExtractor } from './token-extractor'
6
- import type { TeamsAccount, TeamsConfig } from './types'
6
+ import type { TeamsAccount, TeamsAccountType, TeamsConfig } from './types'
7
7
 
8
8
  export async function ensureTeamsAuth(): Promise<void> {
9
9
  try {
@@ -20,15 +20,14 @@ export async function ensureTeamsAuth(): Promise<void> {
20
20
  current_account: config?.current_account ?? null,
21
21
  accounts: { ...config?.accounts },
22
22
  }
23
+ const addedTypes = new Set<TeamsAccountType>()
23
24
 
24
- for (const { token, accountType } of extracted) {
25
+ for (const { token, accountType: extractedType, accountTypeKnown } of extracted) {
25
26
  try {
26
- const client = await new TeamsClient().login({
27
- token,
28
- accountType,
29
- region: config?.accounts[accountType]?.region,
30
- })
31
- await client.testAuth()
27
+ const resolved = await resolveAccountType(token, extractedType, accountTypeKnown, config)
28
+ const { client, accountType } = resolved
29
+
30
+ if (addedTypes.has(accountType)) continue
32
31
 
33
32
  const teams = await client.listTeams()
34
33
  if (accountType !== 'personal' && teams.length === 0) continue
@@ -38,23 +37,24 @@ export async function ensureTeamsAuth(): Promise<void> {
38
37
  teamMap[team.id] = { team_id: team.id, team_name: team.name }
39
38
  }
40
39
 
41
- const existing = newConfig.accounts[accountType]
40
+ const existing: TeamsAccount | undefined = newConfig.accounts[accountType]
42
41
  const account: TeamsAccount = {
43
42
  token,
44
43
  token_expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
45
44
  region: client.getRegion(),
46
45
  account_type: accountType,
47
46
  user_name: existing?.user_name,
48
- current_team: existing?.current_team ?? teams[0].id,
47
+ current_team: existing?.current_team ?? teams[0]?.id ?? null,
49
48
  teams: teamMap,
50
49
  }
51
50
 
52
51
  newConfig.accounts[accountType] = account
52
+ addedTypes.add(accountType)
53
53
  if (!newConfig.current_account) {
54
54
  newConfig.current_account = accountType
55
55
  }
56
56
  } catch (error) {
57
- warn(`[agent-teams] Skipping ${accountType} account: ${(error as Error).message}`)
57
+ warn(`[agent-teams] Skipping ${extractedType} account: ${(error as Error).message}`)
58
58
  }
59
59
  }
60
60
 
@@ -64,6 +64,34 @@ export async function ensureTeamsAuth(): Promise<void> {
64
64
  } catch {}
65
65
  }
66
66
 
67
+ async function resolveAccountType(
68
+ token: string,
69
+ extractedType: TeamsAccountType,
70
+ accountTypeKnown: boolean,
71
+ config: TeamsConfig | null,
72
+ ): Promise<{ client: TeamsClient; accountType: TeamsAccountType }> {
73
+ const candidates: TeamsAccountType[] = [extractedType]
74
+ if (!accountTypeKnown) {
75
+ candidates.push(extractedType === 'work' ? 'personal' : 'work')
76
+ }
77
+
78
+ let lastError: Error | null = null
79
+ for (const candidate of candidates) {
80
+ try {
81
+ const client = await new TeamsClient().login({
82
+ token,
83
+ accountType: candidate,
84
+ region: config?.accounts[candidate]?.region,
85
+ })
86
+ await client.testAuth()
87
+ return { client, accountType: candidate }
88
+ } catch (error) {
89
+ lastError = error as Error
90
+ }
91
+ }
92
+ throw lastError ?? new Error('Token validation failed')
93
+ }
94
+
67
95
  function hasValidToken(config: TeamsConfig): boolean {
68
96
  const key = TeamsCredentialManager.accountOverride ?? config.current_account
69
97
  if (!key) return false
@@ -30,15 +30,28 @@ describe('TeamsTokenExtractor', () => {
30
30
  'EBWebView',
31
31
  )
32
32
  expect(paths).toEqual([
33
- { path: join(darwinEbWebView, 'WV2Profile_tfw', 'Cookies'), accountType: 'work' },
34
- { path: join(darwinEbWebView, 'WV2Profile_tfw', 'Network', 'Cookies'), accountType: 'work' },
35
- { path: join(darwinEbWebView, 'WV2Profile_tfl', 'Cookies'), accountType: 'personal' },
36
- { path: join(darwinEbWebView, 'WV2Profile_tfl', 'Network', 'Cookies'), accountType: 'personal' },
37
- { path: join(darwinEbWebView, 'Default', 'Cookies'), accountType: 'work' },
38
- { path: join(darwinEbWebView, 'Default', 'Network', 'Cookies'), accountType: 'work' },
33
+ { path: join(darwinEbWebView, 'WV2Profile_tfw', 'Cookies'), accountType: 'work', accountTypeKnown: true },
34
+ {
35
+ path: join(darwinEbWebView, 'WV2Profile_tfw', 'Network', 'Cookies'),
36
+ accountType: 'work',
37
+ accountTypeKnown: true,
38
+ },
39
+ {
40
+ path: join(darwinEbWebView, 'WV2Profile_tfl', 'Cookies'),
41
+ accountType: 'personal',
42
+ accountTypeKnown: true,
43
+ },
44
+ {
45
+ path: join(darwinEbWebView, 'WV2Profile_tfl', 'Network', 'Cookies'),
46
+ accountType: 'personal',
47
+ accountTypeKnown: true,
48
+ },
49
+ { path: join(darwinEbWebView, 'Default', 'Cookies'), accountType: 'work', accountTypeKnown: false },
50
+ { path: join(darwinEbWebView, 'Default', 'Network', 'Cookies'), accountType: 'work', accountTypeKnown: false },
39
51
  {
40
52
  path: join(homedir(), 'Library', 'Application Support', 'Microsoft', 'Teams', 'Cookies'),
41
53
  accountType: 'work',
54
+ accountTypeKnown: false,
42
55
  },
43
56
  ])
44
57
  })
@@ -51,6 +64,7 @@ describe('TeamsTokenExtractor', () => {
51
64
  {
52
65
  path: join(homedir(), '.config', 'Microsoft', 'Microsoft Teams', 'Cookies'),
53
66
  accountType: 'work',
67
+ accountTypeKnown: false,
54
68
  },
55
69
  ])
56
70
  })
@@ -71,13 +85,21 @@ describe('TeamsTokenExtractor', () => {
71
85
  'EBWebView',
72
86
  )
73
87
  expect(paths).toEqual([
74
- { path: join(winEbWebView, 'WV2Profile_tfw', 'Cookies'), accountType: 'work' },
75
- { path: join(winEbWebView, 'WV2Profile_tfw', 'Network', 'Cookies'), accountType: 'work' },
76
- { path: join(winEbWebView, 'WV2Profile_tfl', 'Cookies'), accountType: 'personal' },
77
- { path: join(winEbWebView, 'WV2Profile_tfl', 'Network', 'Cookies'), accountType: 'personal' },
78
- { path: join(winEbWebView, 'Default', 'Cookies'), accountType: 'work' },
79
- { path: join(winEbWebView, 'Default', 'Network', 'Cookies'), accountType: 'work' },
80
- { path: join(appdata, 'Microsoft', 'Teams', 'Cookies'), accountType: 'work' },
88
+ { path: join(winEbWebView, 'WV2Profile_tfw', 'Cookies'), accountType: 'work', accountTypeKnown: true },
89
+ {
90
+ path: join(winEbWebView, 'WV2Profile_tfw', 'Network', 'Cookies'),
91
+ accountType: 'work',
92
+ accountTypeKnown: true,
93
+ },
94
+ { path: join(winEbWebView, 'WV2Profile_tfl', 'Cookies'), accountType: 'personal', accountTypeKnown: true },
95
+ {
96
+ path: join(winEbWebView, 'WV2Profile_tfl', 'Network', 'Cookies'),
97
+ accountType: 'personal',
98
+ accountTypeKnown: true,
99
+ },
100
+ { path: join(winEbWebView, 'Default', 'Cookies'), accountType: 'work', accountTypeKnown: false },
101
+ { path: join(winEbWebView, 'Default', 'Network', 'Cookies'), accountType: 'work', accountTypeKnown: false },
102
+ { path: join(appdata, 'Microsoft', 'Teams', 'Cookies'), accountType: 'work', accountTypeKnown: false },
81
103
  ])
82
104
  })
83
105
 
@@ -96,10 +118,12 @@ describe('TeamsTokenExtractor', () => {
96
118
  expect(paths).toContainEqual({
97
119
  path: join(chromeBase, 'Default', 'Cookies'),
98
120
  accountType: 'work',
121
+ accountTypeKnown: false,
99
122
  })
100
123
  expect(paths).toContainEqual({
101
124
  path: join(chromeBase, 'Default', 'Network', 'Cookies'),
102
125
  accountType: 'work',
126
+ accountTypeKnown: false,
103
127
  })
104
128
  })
105
129
 
@@ -111,6 +135,7 @@ describe('TeamsTokenExtractor', () => {
111
135
  expect(paths).toContainEqual({
112
136
  path: join(chromeBase, 'Default', 'Cookies'),
113
137
  accountType: 'work',
138
+ accountTypeKnown: false,
114
139
  })
115
140
  })
116
141
 
@@ -123,6 +148,7 @@ describe('TeamsTokenExtractor', () => {
123
148
  expect(paths).toContainEqual({
124
149
  path: join(chromeBase, 'Default', 'Cookies'),
125
150
  accountType: 'work',
151
+ accountTypeKnown: false,
126
152
  })
127
153
  })
128
154
 
@@ -131,10 +157,14 @@ describe('TeamsTokenExtractor', () => {
131
157
  expect(unsupportedExtractor.getBrowserCookiesPaths()).toEqual([])
132
158
  })
133
159
 
134
- it('all browser paths have accountType work', () => {
160
+ // Regression for #163: browser paths must not assert accountType confidently because
161
+ // Chromium profile paths don't encode work vs personal. Desktop WV2Profile_tfw/_tfl
162
+ // paths are authoritative; browsers must be probed at validation time.
163
+ it('browser paths have accountTypeKnown=false so they get probed at validation', () => {
135
164
  const darwinExtractor = new TeamsTokenExtractor('darwin')
136
165
  const paths = darwinExtractor.getBrowserCookiesPaths()
137
- expect(paths.every((p) => p.accountType === 'work')).toBe(true)
166
+ expect(paths.length).toBeGreaterThan(0)
167
+ expect(paths.every((p) => p.accountTypeKnown === false)).toBe(true)
138
168
  })
139
169
  })
140
170
 
@@ -166,6 +196,7 @@ describe('TeamsTokenExtractor', () => {
166
196
  {
167
197
  path: join(homedir(), '.config', 'Microsoft', 'Microsoft Teams', 'Cookies'),
168
198
  accountType: 'work',
199
+ accountTypeKnown: false,
169
200
  },
170
201
  ])
171
202
  expect(paths.length).toBeGreaterThan(desktopPaths.length)
@@ -338,7 +369,7 @@ describe('TeamsTokenExtractor', () => {
338
369
 
339
370
  const linuxExtractor = new TeamsTokenExtractor('linux')
340
371
  const extractFromCookiesDBSpy = spyOn(linuxExtractor as any, 'extractFromCookiesDB').mockResolvedValue([
341
- { token: mockToken, accountType: 'work' },
372
+ { token: mockToken, accountType: 'work', accountTypeKnown: true },
342
373
  ])
343
374
 
344
375
  const result = await linuxExtractor.extract()
@@ -382,8 +413,8 @@ describe('TeamsTokenExtractor', () => {
382
413
 
383
414
  const winExtractor = new TeamsTokenExtractor('win32')
384
415
  const getPathsSpy = spyOn(winExtractor, 'getTeamsCookiesPaths').mockReturnValue([
385
- { path: cookiesPath, accountType: 'personal' },
386
- { path: networkCookiesPath, accountType: 'personal' },
416
+ { path: cookiesPath, accountType: 'personal', accountTypeKnown: true },
417
+ { path: networkCookiesPath, accountType: 'personal', accountTypeKnown: true },
387
418
  ])
388
419
  const tried: string[] = []
389
420
  const copyAndExtractSpy = spyOn(winExtractor as any, 'copyAndExtract').mockImplementation(async (...args) => {
@@ -398,7 +429,7 @@ describe('TeamsTokenExtractor', () => {
398
429
  // then: the Cookies path was skipped (never passed to copyAndExtract),
399
430
  // the Network/Cookies sibling was tried, and the token was returned.
400
431
  expect(tried).toEqual([networkCookiesPath])
401
- expect(results).toEqual([{ token: mockToken, accountType: 'personal' }])
432
+ expect(results).toEqual([{ token: mockToken, accountType: 'personal', accountTypeKnown: true }])
402
433
 
403
434
  getPathsSpy.mockRestore()
404
435
  copyAndExtractSpy.mockRestore()
@@ -419,8 +450,8 @@ describe('TeamsTokenExtractor', () => {
419
450
  const firstPath = join(workDir, 'WV2Profile_tfw', 'Cookies')
420
451
  const secondPath = join(workDir, 'Default', 'Network', 'Cookies')
421
452
  const getPathsSpy = spyOn(winExtractor, 'getTeamsCookiesPaths').mockReturnValue([
422
- { path: firstPath, accountType: 'work' },
423
- { path: secondPath, accountType: 'work' },
453
+ { path: firstPath, accountType: 'work', accountTypeKnown: true },
454
+ { path: secondPath, accountType: 'work', accountTypeKnown: true },
424
455
  ])
425
456
  mkdirSync(join(workDir, 'WV2Profile_tfw'), { recursive: true })
426
457
  mkdirSync(join(workDir, 'Default', 'Network'), { recursive: true })
@@ -436,7 +467,98 @@ describe('TeamsTokenExtractor', () => {
436
467
 
437
468
  // then: garbage was rejected, loop continued to the real token
438
469
  expect(copyAndExtractSpy).toHaveBeenCalledTimes(2)
439
- expect(results).toEqual([{ token: realToken, accountType: 'work' }])
470
+ expect(results).toEqual([{ token: realToken, accountType: 'work', accountTypeKnown: true }])
471
+
472
+ getPathsSpy.mockRestore()
473
+ copyAndExtractSpy.mockRestore()
474
+ cleanup()
475
+ })
476
+
477
+ // Regression for #163: browser-sourced tokens carry accountTypeKnown=false so that
478
+ // the auth command can probe both endpoints. The extraction loop must preserve the
479
+ // flag and must not dedupe an unknown-type browser token by its guessed accountType.
480
+ it('propagates accountTypeKnown=false for browser-sourced tokens', async () => {
481
+ // given: a browser Cookies path returning a valid token, guessed as work
482
+ const browserPath = join(workDir, 'Chrome', 'Default', 'Cookies')
483
+ mkdirSync(join(workDir, 'Chrome', 'Default'), { recursive: true })
484
+ writeFileSync(browserPath, '')
485
+
486
+ const winExtractor = new TeamsTokenExtractor('win32')
487
+ const getPathsSpy = spyOn(winExtractor, 'getTeamsCookiesPaths').mockReturnValue([
488
+ { path: browserPath, accountType: 'work', accountTypeKnown: false },
489
+ ])
490
+ const copyAndExtractSpy = spyOn(winExtractor as any, 'copyAndExtract').mockResolvedValue(mockToken)
491
+
492
+ // when
493
+ const results = await (winExtractor as any).extractFromCookiesDB()
494
+
495
+ // then: the flag is passed through so callers can probe
496
+ expect(results).toEqual([{ token: mockToken, accountType: 'work', accountTypeKnown: false }])
497
+
498
+ getPathsSpy.mockRestore()
499
+ copyAndExtractSpy.mockRestore()
500
+ cleanup()
501
+ })
502
+
503
+ // Regression for #163: when a desktop path (known=true) for 'work' already succeeded,
504
+ // a subsequent browser path (known=false) guessed as 'work' must still be explored —
505
+ // it might be a personal account misguessed as work. The dedup only kicks in for
506
+ // confidently labeled paths.
507
+ it('does not skip unknown-type path just because a known-type same-label succeeded', async () => {
508
+ const desktopPath = join(workDir, 'WV2Profile_tfw', 'Network', 'Cookies')
509
+ const browserPath = join(workDir, 'Chrome', 'Default', 'Cookies')
510
+ mkdirSync(join(workDir, 'WV2Profile_tfw', 'Network'), { recursive: true })
511
+ mkdirSync(join(workDir, 'Chrome', 'Default'), { recursive: true })
512
+ writeFileSync(desktopPath, '')
513
+ writeFileSync(browserPath, '')
514
+
515
+ const desktopToken = mockToken
516
+ const browserToken = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJicm93c2VyIn0.different_signature_here_abc'
517
+
518
+ const winExtractor = new TeamsTokenExtractor('win32')
519
+ const getPathsSpy = spyOn(winExtractor, 'getTeamsCookiesPaths').mockReturnValue([
520
+ { path: desktopPath, accountType: 'work', accountTypeKnown: true },
521
+ { path: browserPath, accountType: 'work', accountTypeKnown: false },
522
+ ])
523
+ const copyAndExtractSpy = spyOn(winExtractor as any, 'copyAndExtract')
524
+ .mockResolvedValueOnce(desktopToken)
525
+ .mockResolvedValueOnce(browserToken)
526
+
527
+ // when
528
+ const results = await (winExtractor as any).extractFromCookiesDB()
529
+
530
+ // then: both tokens returned; browser token keeps accountTypeKnown=false for probing
531
+ expect(results).toEqual([
532
+ { token: desktopToken, accountType: 'work', accountTypeKnown: true },
533
+ { token: browserToken, accountType: 'work', accountTypeKnown: false },
534
+ ])
535
+
536
+ getPathsSpy.mockRestore()
537
+ copyAndExtractSpy.mockRestore()
538
+ cleanup()
539
+ })
540
+
541
+ it('dedupes identical tokens extracted from multiple paths', async () => {
542
+ const path1 = join(workDir, 'Chrome', 'Default', 'Cookies')
543
+ const path2 = join(workDir, 'Edge', 'Default', 'Cookies')
544
+ mkdirSync(join(workDir, 'Chrome', 'Default'), { recursive: true })
545
+ mkdirSync(join(workDir, 'Edge', 'Default'), { recursive: true })
546
+ writeFileSync(path1, '')
547
+ writeFileSync(path2, '')
548
+
549
+ const winExtractor = new TeamsTokenExtractor('win32')
550
+ const getPathsSpy = spyOn(winExtractor, 'getTeamsCookiesPaths').mockReturnValue([
551
+ { path: path1, accountType: 'work', accountTypeKnown: false },
552
+ { path: path2, accountType: 'work', accountTypeKnown: false },
553
+ ])
554
+ const copyAndExtractSpy = spyOn(winExtractor as any, 'copyAndExtract').mockResolvedValue(mockToken)
555
+
556
+ // when
557
+ const results = await (winExtractor as any).extractFromCookiesDB()
558
+
559
+ // then: only one result despite two paths returning the same token
560
+ expect(results).toHaveLength(1)
561
+ expect(results[0].token).toBe(mockToken)
440
562
 
441
563
  getPathsSpy.mockRestore()
442
564
  copyAndExtractSpy.mockRestore()
@@ -454,8 +576,8 @@ describe('TeamsTokenExtractor', () => {
454
576
 
455
577
  const winExtractor = new TeamsTokenExtractor('win32')
456
578
  const getPathsSpy = spyOn(winExtractor, 'getTeamsCookiesPaths').mockReturnValue([
457
- { path: workCookies, accountType: 'work' },
458
- { path: workNetworkCookies, accountType: 'work' },
579
+ { path: workCookies, accountType: 'work', accountTypeKnown: true },
580
+ { path: workNetworkCookies, accountType: 'work', accountTypeKnown: true },
459
581
  ])
460
582
  const copyAndExtractSpy = spyOn(winExtractor as any, 'copyAndExtract').mockResolvedValue(mockToken)
461
583