agent-messenger 1.0.0 → 1.1.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/commands/release.md +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.github/workflows/ci.yml +1 -1
  4. package/.github/workflows/e2e.yml.disabled +69 -0
  5. package/README.md +16 -14
  6. package/biome.json +33 -1
  7. package/bun.lock +63 -0
  8. package/dist/package.json +8 -4
  9. package/dist/src/cli.d.ts.map +1 -1
  10. package/dist/src/cli.js +4 -1
  11. package/dist/src/cli.js.map +1 -1
  12. package/dist/src/platforms/discord/cli.js +1 -1
  13. package/dist/src/platforms/discord/client.d.ts.map +1 -1
  14. package/dist/src/platforms/discord/client.js +3 -3
  15. package/dist/src/platforms/discord/client.js.map +1 -1
  16. package/dist/src/platforms/discord/commands/user.d.ts.map +1 -1
  17. package/dist/src/platforms/discord/commands/user.js +10 -1
  18. package/dist/src/platforms/discord/commands/user.js.map +1 -1
  19. package/dist/src/platforms/discord/credential-manager.d.ts.map +1 -1
  20. package/dist/src/platforms/discord/credential-manager.js +18 -12
  21. package/dist/src/platforms/discord/credential-manager.js.map +1 -1
  22. package/dist/src/platforms/slack/cli.js +1 -1
  23. package/dist/src/platforms/slack/credential-manager.d.ts.map +1 -1
  24. package/dist/src/platforms/slack/credential-manager.js +20 -6
  25. package/dist/src/platforms/slack/credential-manager.js.map +1 -1
  26. package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
  27. package/dist/src/platforms/slack/token-extractor.js +34 -9
  28. package/dist/src/platforms/slack/token-extractor.js.map +1 -1
  29. package/dist/src/platforms/teams/cli.d.ts.map +1 -0
  30. package/dist/{cli.js → src/platforms/teams/cli.js} +11 -10
  31. package/dist/src/platforms/teams/cli.js.map +1 -0
  32. package/dist/src/platforms/teams/client.d.ts +32 -0
  33. package/dist/src/platforms/teams/client.d.ts.map +1 -0
  34. package/dist/src/platforms/teams/client.js +202 -0
  35. package/dist/src/platforms/teams/client.js.map +1 -0
  36. package/dist/src/platforms/teams/commands/auth.d.ts +14 -0
  37. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -0
  38. package/dist/src/platforms/teams/commands/auth.js +176 -0
  39. package/dist/src/platforms/teams/commands/auth.js.map +1 -0
  40. package/dist/src/platforms/teams/commands/channel.d.ts +13 -0
  41. package/dist/src/platforms/teams/commands/channel.d.ts.map +1 -0
  42. package/dist/src/platforms/teams/commands/channel.js +97 -0
  43. package/dist/src/platforms/teams/commands/channel.js.map +1 -0
  44. package/dist/src/platforms/teams/commands/file.d.ts +12 -0
  45. package/dist/src/platforms/teams/commands/file.d.ts.map +1 -0
  46. package/dist/src/platforms/teams/commands/file.js +104 -0
  47. package/dist/src/platforms/teams/commands/file.js.map +1 -0
  48. package/dist/{commands → src/platforms/teams/commands}/index.d.ts +5 -2
  49. package/dist/src/platforms/teams/commands/index.d.ts.map +1 -0
  50. package/dist/{commands → src/platforms/teams/commands}/index.js +5 -2
  51. package/dist/src/platforms/teams/commands/index.js.map +1 -0
  52. package/dist/src/platforms/teams/commands/message.d.ts +17 -0
  53. package/dist/src/platforms/teams/commands/message.d.ts.map +1 -0
  54. package/dist/src/platforms/teams/commands/message.js +133 -0
  55. package/dist/src/platforms/teams/commands/message.js.map +1 -0
  56. package/dist/src/platforms/teams/commands/reaction.d.ts +9 -0
  57. package/dist/src/platforms/teams/commands/reaction.d.ts.map +1 -0
  58. package/dist/src/platforms/teams/commands/reaction.js +68 -0
  59. package/dist/src/platforms/teams/commands/reaction.js.map +1 -0
  60. package/dist/src/platforms/teams/commands/snapshot.d.ts +10 -0
  61. package/dist/src/platforms/teams/commands/snapshot.d.ts.map +1 -0
  62. package/dist/src/platforms/teams/commands/snapshot.js +85 -0
  63. package/dist/src/platforms/teams/commands/snapshot.js.map +1 -0
  64. package/dist/src/platforms/teams/commands/team.d.ts +18 -0
  65. package/dist/src/platforms/teams/commands/team.d.ts.map +1 -0
  66. package/dist/src/platforms/teams/commands/team.js +130 -0
  67. package/dist/src/platforms/teams/commands/team.js.map +1 -0
  68. package/dist/src/platforms/teams/commands/user.d.ts.map +1 -0
  69. package/dist/src/platforms/teams/commands/user.js +88 -0
  70. package/dist/src/platforms/teams/commands/user.js.map +1 -0
  71. package/dist/src/platforms/teams/credential-manager.d.ts +18 -0
  72. package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -0
  73. package/dist/src/platforms/teams/credential-manager.js +81 -0
  74. package/dist/src/platforms/teams/credential-manager.js.map +1 -0
  75. package/dist/src/platforms/teams/index.d.ts +4 -0
  76. package/dist/src/platforms/teams/index.d.ts.map +1 -0
  77. package/dist/src/platforms/teams/index.js +6 -0
  78. package/dist/src/platforms/teams/index.js.map +1 -0
  79. package/dist/src/platforms/teams/token-extractor.d.ts +36 -0
  80. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -0
  81. package/dist/src/platforms/teams/token-extractor.js +335 -0
  82. package/dist/src/platforms/teams/token-extractor.js.map +1 -0
  83. package/dist/src/platforms/teams/types.d.ts +209 -0
  84. package/dist/src/platforms/teams/types.d.ts.map +1 -0
  85. package/dist/src/platforms/teams/types.js +65 -0
  86. package/dist/src/platforms/teams/types.js.map +1 -0
  87. package/docs/teams.md +321 -0
  88. package/e2e/README.md +256 -0
  89. package/e2e/config.ts +45 -0
  90. package/e2e/discord.e2e.test.ts +252 -0
  91. package/e2e/helpers.ts +107 -0
  92. package/e2e/slack.e2e.test.ts +309 -0
  93. package/package.json +8 -4
  94. package/scripts/postbuild.ts +15 -0
  95. package/skills/agent-teams/SKILL.md +292 -0
  96. package/skills/agent-teams/references/authentication.md +375 -0
  97. package/skills/agent-teams/references/common-patterns.md +596 -0
  98. package/skills/agent-teams/templates/monitor-channel.sh +239 -0
  99. package/skills/agent-teams/templates/post-message.sh +224 -0
  100. package/skills/agent-teams/templates/team-summary.sh +210 -0
  101. package/src/cli.ts +4 -0
  102. package/src/platforms/discord/client.ts +3 -3
  103. package/src/platforms/discord/commands/auth.test.ts +48 -32
  104. package/src/platforms/discord/commands/channel.test.ts +54 -42
  105. package/src/platforms/discord/commands/file.test.ts +40 -53
  106. package/src/platforms/discord/commands/guild.test.ts +47 -27
  107. package/src/platforms/discord/commands/message.test.ts +54 -51
  108. package/src/platforms/discord/commands/reaction.test.ts +54 -42
  109. package/src/platforms/discord/commands/user.ts +12 -1
  110. package/src/platforms/discord/credential-manager.test.ts +137 -136
  111. package/src/platforms/discord/credential-manager.ts +20 -13
  112. package/src/platforms/discord/token-extractor.test.ts +133 -383
  113. package/{tests → src/platforms/slack}/cli.test.ts +3 -3
  114. package/{tests/slack-client.test.ts → src/platforms/slack/client.test.ts} +1 -1
  115. package/{tests → src/platforms/slack}/commands/auth.test.ts +25 -13
  116. package/{tests → src/platforms/slack}/commands/channel.test.ts +2 -2
  117. package/{tests → src/platforms/slack}/commands/file.test.ts +2 -2
  118. package/{tests → src/platforms/slack}/commands/message.test.ts +2 -2
  119. package/{tests → src/platforms/slack}/commands/reaction.test.ts +1 -1
  120. package/{tests → src/platforms/slack}/commands/snapshot.test.ts +117 -105
  121. package/{tests → src/platforms/slack}/commands/user.test.ts +3 -3
  122. package/{tests → src/platforms/slack}/commands/workspace.test.ts +44 -95
  123. package/{tests → src/platforms/slack}/credential-manager.test.ts +2 -2
  124. package/src/platforms/slack/credential-manager.ts +22 -7
  125. package/src/platforms/slack/token-extractor-node-test.ts +40 -0
  126. package/src/platforms/slack/token-extractor-node.test.ts +10 -0
  127. package/src/platforms/slack/token-extractor.ts +36 -10
  128. package/{tests → src/platforms/slack}/types.test.ts +1 -1
  129. package/src/platforms/teams/cli.ts +36 -0
  130. package/src/platforms/teams/client.test.ts +500 -0
  131. package/src/platforms/teams/client.ts +365 -0
  132. package/src/platforms/teams/commands/auth.test.ts +99 -0
  133. package/src/platforms/teams/commands/auth.ts +232 -0
  134. package/src/platforms/teams/commands/channel.test.ts +147 -0
  135. package/src/platforms/teams/commands/channel.ts +129 -0
  136. package/src/platforms/teams/commands/file.test.ts +88 -0
  137. package/src/platforms/teams/commands/file.ts +144 -0
  138. package/src/platforms/teams/commands/index.ts +12 -0
  139. package/src/platforms/teams/commands/message.test.ts +110 -0
  140. package/src/platforms/teams/commands/message.ts +188 -0
  141. package/src/platforms/teams/commands/reaction.test.ts +87 -0
  142. package/src/platforms/teams/commands/reaction.ts +104 -0
  143. package/src/platforms/teams/commands/snapshot.test.ts +35 -0
  144. package/src/platforms/teams/commands/snapshot.ts +115 -0
  145. package/src/platforms/teams/commands/team.test.ts +157 -0
  146. package/src/platforms/teams/commands/team.ts +164 -0
  147. package/src/platforms/teams/commands/user.test.ts +83 -0
  148. package/src/platforms/teams/commands/user.ts +112 -0
  149. package/src/platforms/teams/credential-manager.test.ts +178 -0
  150. package/src/platforms/teams/credential-manager.ts +92 -0
  151. package/src/platforms/teams/index.ts +5 -0
  152. package/src/platforms/teams/token-extractor.test.ts +429 -0
  153. package/src/platforms/teams/token-extractor.ts +462 -0
  154. package/src/platforms/teams/types.test.ts +226 -0
  155. package/src/platforms/teams/types.ts +140 -0
  156. package/tsconfig.json +1 -1
  157. package/dist/cli.d.ts.map +0 -1
  158. package/dist/cli.js.map +0 -1
  159. package/dist/commands/auth.d.ts +0 -3
  160. package/dist/commands/auth.d.ts.map +0 -1
  161. package/dist/commands/auth.js +0 -140
  162. package/dist/commands/auth.js.map +0 -1
  163. package/dist/commands/channel.d.ts +0 -3
  164. package/dist/commands/channel.d.ts.map +0 -1
  165. package/dist/commands/channel.js +0 -118
  166. package/dist/commands/channel.js.map +0 -1
  167. package/dist/commands/file.d.ts +0 -3
  168. package/dist/commands/file.d.ts.map +0 -1
  169. package/dist/commands/file.js +0 -113
  170. package/dist/commands/file.js.map +0 -1
  171. package/dist/commands/index.d.ts.map +0 -1
  172. package/dist/commands/index.js.map +0 -1
  173. package/dist/commands/message.d.ts +0 -3
  174. package/dist/commands/message.d.ts.map +0 -1
  175. package/dist/commands/message.js +0 -214
  176. package/dist/commands/message.js.map +0 -1
  177. package/dist/commands/reaction.d.ts +0 -3
  178. package/dist/commands/reaction.d.ts.map +0 -1
  179. package/dist/commands/reaction.js +0 -100
  180. package/dist/commands/reaction.js.map +0 -1
  181. package/dist/commands/snapshot.d.ts +0 -3
  182. package/dist/commands/snapshot.d.ts.map +0 -1
  183. package/dist/commands/snapshot.js +0 -88
  184. package/dist/commands/snapshot.js.map +0 -1
  185. package/dist/commands/user.d.ts.map +0 -1
  186. package/dist/commands/user.js +0 -96
  187. package/dist/commands/user.js.map +0 -1
  188. package/dist/commands/workspace.d.ts +0 -3
  189. package/dist/commands/workspace.d.ts.map +0 -1
  190. package/dist/commands/workspace.js +0 -89
  191. package/dist/commands/workspace.js.map +0 -1
  192. package/dist/lib/credential-manager.d.ts +0 -13
  193. package/dist/lib/credential-manager.d.ts.map +0 -1
  194. package/dist/lib/credential-manager.js +0 -58
  195. package/dist/lib/credential-manager.js.map +0 -1
  196. package/dist/lib/index.d.ts +0 -3
  197. package/dist/lib/index.d.ts.map +0 -1
  198. package/dist/lib/index.js +0 -3
  199. package/dist/lib/index.js.map +0 -1
  200. package/dist/lib/ref-manager.d.ts +0 -26
  201. package/dist/lib/ref-manager.d.ts.map +0 -1
  202. package/dist/lib/ref-manager.js +0 -92
  203. package/dist/lib/ref-manager.js.map +0 -1
  204. package/dist/lib/slack-client.d.ts +0 -37
  205. package/dist/lib/slack-client.d.ts.map +0 -1
  206. package/dist/lib/slack-client.js +0 -379
  207. package/dist/lib/slack-client.js.map +0 -1
  208. package/dist/lib/token-extractor.d.ts +0 -28
  209. package/dist/lib/token-extractor.d.ts.map +0 -1
  210. package/dist/lib/token-extractor.js +0 -401
  211. package/dist/lib/token-extractor.js.map +0 -1
  212. package/dist/src/platforms/discord/client.test.d.ts +0 -2
  213. package/dist/src/platforms/discord/client.test.d.ts.map +0 -1
  214. package/dist/src/platforms/discord/client.test.js +0 -367
  215. package/dist/src/platforms/discord/client.test.js.map +0 -1
  216. package/dist/src/platforms/discord/commands/auth.test.d.ts +0 -2
  217. package/dist/src/platforms/discord/commands/auth.test.d.ts.map +0 -1
  218. package/dist/src/platforms/discord/commands/auth.test.js +0 -65
  219. package/dist/src/platforms/discord/commands/auth.test.js.map +0 -1
  220. package/dist/src/platforms/discord/commands/channel.test.d.ts +0 -2
  221. package/dist/src/platforms/discord/commands/channel.test.d.ts.map +0 -1
  222. package/dist/src/platforms/discord/commands/channel.test.js +0 -136
  223. package/dist/src/platforms/discord/commands/channel.test.js.map +0 -1
  224. package/dist/src/platforms/discord/commands/file.test.d.ts +0 -2
  225. package/dist/src/platforms/discord/commands/file.test.d.ts.map +0 -1
  226. package/dist/src/platforms/discord/commands/file.test.js +0 -83
  227. package/dist/src/platforms/discord/commands/file.test.js.map +0 -1
  228. package/dist/src/platforms/discord/commands/guild.test.d.ts +0 -2
  229. package/dist/src/platforms/discord/commands/guild.test.d.ts.map +0 -1
  230. package/dist/src/platforms/discord/commands/guild.test.js +0 -100
  231. package/dist/src/platforms/discord/commands/guild.test.js.map +0 -1
  232. package/dist/src/platforms/discord/commands/message.test.d.ts +0 -2
  233. package/dist/src/platforms/discord/commands/message.test.d.ts.map +0 -1
  234. package/dist/src/platforms/discord/commands/message.test.js +0 -91
  235. package/dist/src/platforms/discord/commands/message.test.js.map +0 -1
  236. package/dist/src/platforms/discord/commands/reaction.test.d.ts +0 -2
  237. package/dist/src/platforms/discord/commands/reaction.test.d.ts.map +0 -1
  238. package/dist/src/platforms/discord/commands/reaction.test.js +0 -115
  239. package/dist/src/platforms/discord/commands/reaction.test.js.map +0 -1
  240. package/dist/src/platforms/discord/commands/snapshot.test.d.ts +0 -2
  241. package/dist/src/platforms/discord/commands/snapshot.test.d.ts.map +0 -1
  242. package/dist/src/platforms/discord/commands/snapshot.test.js +0 -25
  243. package/dist/src/platforms/discord/commands/snapshot.test.js.map +0 -1
  244. package/dist/src/platforms/discord/commands/user.test.d.ts +0 -2
  245. package/dist/src/platforms/discord/commands/user.test.d.ts.map +0 -1
  246. package/dist/src/platforms/discord/commands/user.test.js +0 -103
  247. package/dist/src/platforms/discord/commands/user.test.js.map +0 -1
  248. package/dist/src/platforms/discord/credential-manager.test.d.ts +0 -2
  249. package/dist/src/platforms/discord/credential-manager.test.d.ts.map +0 -1
  250. package/dist/src/platforms/discord/credential-manager.test.js +0 -136
  251. package/dist/src/platforms/discord/credential-manager.test.js.map +0 -1
  252. package/dist/src/platforms/discord/token-extractor.test.d.ts +0 -2
  253. package/dist/src/platforms/discord/token-extractor.test.d.ts.map +0 -1
  254. package/dist/src/platforms/discord/token-extractor.test.js +0 -789
  255. package/dist/src/platforms/discord/token-extractor.test.js.map +0 -1
  256. package/dist/src/platforms/discord/types.test.d.ts +0 -2
  257. package/dist/src/platforms/discord/types.test.d.ts.map +0 -1
  258. package/dist/src/platforms/discord/types.test.js +0 -211
  259. package/dist/src/platforms/discord/types.test.js.map +0 -1
  260. package/dist/src/shared/utils/concurrency.test.d.ts +0 -2
  261. package/dist/src/shared/utils/concurrency.test.d.ts.map +0 -1
  262. package/dist/src/shared/utils/concurrency.test.js +0 -39
  263. package/dist/src/shared/utils/concurrency.test.js.map +0 -1
  264. package/dist/tests/cli.test.d.ts +0 -2
  265. package/dist/tests/cli.test.d.ts.map +0 -1
  266. package/dist/tests/cli.test.js +0 -83
  267. package/dist/tests/cli.test.js.map +0 -1
  268. package/dist/tests/commands/.test-slack-data/Local Storage/leveldb/CURRENT +0 -1
  269. package/dist/tests/commands/.test-slack-data/Local Storage/leveldb/LOCK +0 -0
  270. package/dist/tests/commands/.test-slack-data/Local Storage/leveldb/LOG +0 -3
  271. package/dist/tests/commands/.test-slack-data/Local Storage/leveldb/LOG.old +0 -1
  272. package/dist/tests/commands/.test-slack-data/Local Storage/leveldb/MANIFEST-000004 +0 -0
  273. package/dist/tests/commands/auth.test.d.ts +0 -2
  274. package/dist/tests/commands/auth.test.d.ts.map +0 -1
  275. package/dist/tests/commands/auth.test.js +0 -304
  276. package/dist/tests/commands/auth.test.js.map +0 -1
  277. package/dist/tests/commands/channel.test.d.ts +0 -2
  278. package/dist/tests/commands/channel.test.d.ts.map +0 -1
  279. package/dist/tests/commands/channel.test.js +0 -166
  280. package/dist/tests/commands/channel.test.js.map +0 -1
  281. package/dist/tests/commands/file.test.d.ts +0 -2
  282. package/dist/tests/commands/file.test.d.ts.map +0 -1
  283. package/dist/tests/commands/file.test.js +0 -175
  284. package/dist/tests/commands/file.test.js.map +0 -1
  285. package/dist/tests/commands/message.test.d.ts +0 -2
  286. package/dist/tests/commands/message.test.d.ts.map +0 -1
  287. package/dist/tests/commands/message.test.js +0 -293
  288. package/dist/tests/commands/message.test.js.map +0 -1
  289. package/dist/tests/commands/reaction.test.d.ts +0 -2
  290. package/dist/tests/commands/reaction.test.d.ts.map +0 -1
  291. package/dist/tests/commands/reaction.test.js +0 -84
  292. package/dist/tests/commands/reaction.test.js.map +0 -1
  293. package/dist/tests/commands/snapshot.test.d.ts +0 -2
  294. package/dist/tests/commands/snapshot.test.d.ts.map +0 -1
  295. package/dist/tests/commands/snapshot.test.js +0 -280
  296. package/dist/tests/commands/snapshot.test.js.map +0 -1
  297. package/dist/tests/commands/user.test.d.ts +0 -2
  298. package/dist/tests/commands/user.test.d.ts.map +0 -1
  299. package/dist/tests/commands/user.test.js +0 -117
  300. package/dist/tests/commands/user.test.js.map +0 -1
  301. package/dist/tests/commands/workspace.test.d.ts +0 -2
  302. package/dist/tests/commands/workspace.test.d.ts.map +0 -1
  303. package/dist/tests/commands/workspace.test.js +0 -453
  304. package/dist/tests/commands/workspace.test.js.map +0 -1
  305. package/dist/tests/credential-manager.test.d.ts +0 -2
  306. package/dist/tests/credential-manager.test.d.ts.map +0 -1
  307. package/dist/tests/credential-manager.test.js +0 -199
  308. package/dist/tests/credential-manager.test.js.map +0 -1
  309. package/dist/tests/slack-client.test.d.ts +0 -2
  310. package/dist/tests/slack-client.test.d.ts.map +0 -1
  311. package/dist/tests/slack-client.test.js +0 -741
  312. package/dist/tests/slack-client.test.js.map +0 -1
  313. package/dist/tests/types.test.d.ts +0 -2
  314. package/dist/tests/types.test.d.ts.map +0 -1
  315. package/dist/tests/types.test.js +0 -215
  316. package/dist/tests/types.test.js.map +0 -1
  317. package/dist/types/index.d.ts +0 -369
  318. package/dist/types/index.d.ts.map +0 -1
  319. package/dist/types/index.js +0 -92
  320. package/dist/types/index.js.map +0 -1
  321. package/dist/utils/error-handler.d.ts +0 -2
  322. package/dist/utils/error-handler.d.ts.map +0 -1
  323. package/dist/utils/error-handler.js +0 -5
  324. package/dist/utils/error-handler.js.map +0 -1
  325. package/dist/utils/output.d.ts +0 -2
  326. package/dist/utils/output.d.ts.map +0 -1
  327. package/dist/utils/output.js +0 -4
  328. package/dist/utils/output.js.map +0 -1
  329. /package/dist/{cli.d.ts → src/platforms/teams/cli.d.ts} +0 -0
  330. /package/dist/{commands → src/platforms/teams/commands}/user.d.ts +0 -0
@@ -0,0 +1,365 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { basename } from 'node:path'
3
+ import type { TeamsChannel, TeamsFile, TeamsMessage, TeamsTeam, TeamsUser } from './types'
4
+ import { TeamsError } from './types'
5
+
6
+ interface RateLimitBucket {
7
+ remaining: number
8
+ resetAt: number
9
+ }
10
+
11
+ const MSG_API_BASE = 'https://emea.ng.msg.teams.microsoft.com/v1'
12
+ const CSA_API_BASE = 'https://teams.microsoft.com/api'
13
+ const MAX_RETRIES = 3
14
+ const BASE_BACKOFF_MS = 100
15
+
16
+ export class TeamsClient {
17
+ private token: string
18
+ private tokenExpiresAt?: Date
19
+ private buckets: Map<string, RateLimitBucket> = new Map()
20
+ private globalRateLimitUntil: number = 0
21
+
22
+ constructor(token: string, tokenExpiresAt?: string) {
23
+ if (!token) {
24
+ throw new TeamsError('Token is required', 'missing_token')
25
+ }
26
+ this.token = token
27
+ if (tokenExpiresAt) {
28
+ this.tokenExpiresAt = new Date(tokenExpiresAt)
29
+ }
30
+ }
31
+
32
+ private isTokenExpired(): boolean {
33
+ if (!this.tokenExpiresAt) {
34
+ return false
35
+ }
36
+ return this.tokenExpiresAt.getTime() < Date.now()
37
+ }
38
+
39
+ private getBucketKey(method: string, path: string): string {
40
+ const normalized = path
41
+ .replace(/\/teams\/[^/]+/, '/teams/{team_id}')
42
+ .replace(/\/channels\/[^/]+/, '/channels/{channel_id}')
43
+ .replace(/\/messages\/[^/]+/, '/messages/{message_id}')
44
+ .replace(/\/users\/[^/]+/, '/users/{user_id}')
45
+ .replace(/\/members\/[^/]+/, '/members/{member_id}')
46
+ return `${method}:${normalized}`
47
+ }
48
+
49
+ private async waitForRateLimit(bucketKey: string): Promise<void> {
50
+ const now = Date.now()
51
+
52
+ if (this.globalRateLimitUntil > now) {
53
+ await this.sleep(this.globalRateLimitUntil - now)
54
+ }
55
+
56
+ const bucket = this.buckets.get(bucketKey)
57
+ if (bucket && bucket.remaining === 0 && bucket.resetAt * 1000 > now) {
58
+ await this.sleep(bucket.resetAt * 1000 - now)
59
+ }
60
+ }
61
+
62
+ private updateBucket(bucketKey: string, response: Response): void {
63
+ const remaining = response.headers.get('X-RateLimit-Remaining')
64
+ const reset = response.headers.get('X-RateLimit-Reset')
65
+
66
+ if (remaining !== null && reset !== null) {
67
+ this.buckets.set(bucketKey, {
68
+ remaining: parseInt(remaining, 10),
69
+ resetAt: parseFloat(reset),
70
+ })
71
+ }
72
+ }
73
+
74
+ private async handleRateLimitResponse(response: Response): Promise<number> {
75
+ const retryAfter = response.headers.get('Retry-After')
76
+ const waitMs = parseFloat(retryAfter || '1') * 1000
77
+
78
+ this.globalRateLimitUntil = Date.now() + waitMs
79
+ await this.sleep(waitMs)
80
+ return waitMs
81
+ }
82
+
83
+ private sleep(ms: number): Promise<void> {
84
+ return new Promise((resolve) => setTimeout(resolve, ms))
85
+ }
86
+
87
+ private async request<T>(
88
+ method: string,
89
+ path: string,
90
+ body?: unknown,
91
+ baseUrl: string = MSG_API_BASE
92
+ ): Promise<T> {
93
+ if (this.isTokenExpired()) {
94
+ throw new TeamsError('Token has expired', 'token_expired')
95
+ }
96
+
97
+ const url = `${baseUrl}${path}`
98
+ const bucketKey = this.getBucketKey(method, path)
99
+
100
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
101
+ await this.waitForRateLimit(bucketKey)
102
+
103
+ const headers: Record<string, string> = {
104
+ 'X-Skypetoken': this.token,
105
+ 'Content-Type': 'application/json',
106
+ }
107
+
108
+ const options: RequestInit = {
109
+ method,
110
+ headers,
111
+ }
112
+
113
+ if (body !== undefined) {
114
+ options.body = JSON.stringify(body)
115
+ }
116
+
117
+ const response = await fetch(url, options)
118
+ this.updateBucket(bucketKey, response)
119
+
120
+ if (response.status === 429) {
121
+ if (attempt < MAX_RETRIES) {
122
+ await this.handleRateLimitResponse(response)
123
+ continue
124
+ }
125
+ const errorBody = (await response.json().catch(() => null)) as {
126
+ message?: string
127
+ } | null
128
+ throw new TeamsError(errorBody?.message ?? 'Rate limited', 'rate_limited')
129
+ }
130
+
131
+ if (response.status >= 500 && attempt < MAX_RETRIES) {
132
+ await this.sleep(BASE_BACKOFF_MS * 2 ** attempt)
133
+ continue
134
+ }
135
+
136
+ if (!response.ok) {
137
+ const errorBody = (await response.json().catch(() => null)) as {
138
+ message?: string
139
+ code?: string | number
140
+ } | null
141
+ throw new TeamsError(
142
+ errorBody?.message ?? `HTTP ${response.status}`,
143
+ errorBody?.code?.toString() ?? `http_${response.status}`
144
+ )
145
+ }
146
+
147
+ if (response.status === 204) {
148
+ return undefined as T
149
+ }
150
+
151
+ return response.json() as Promise<T>
152
+ }
153
+
154
+ throw new TeamsError('Request failed after retries', 'max_retries')
155
+ }
156
+
157
+ private async requestFormData<T>(
158
+ path: string,
159
+ formData: FormData,
160
+ baseUrl: string = MSG_API_BASE
161
+ ): Promise<T> {
162
+ if (this.isTokenExpired()) {
163
+ throw new TeamsError('Token has expired', 'token_expired')
164
+ }
165
+
166
+ const url = `${baseUrl}${path}`
167
+ const bucketKey = this.getBucketKey('POST', path)
168
+
169
+ await this.waitForRateLimit(bucketKey)
170
+
171
+ const response = await fetch(url, {
172
+ method: 'POST',
173
+ headers: {
174
+ 'X-Skypetoken': this.token,
175
+ },
176
+ body: formData,
177
+ })
178
+
179
+ this.updateBucket(bucketKey, response)
180
+
181
+ if (!response.ok) {
182
+ const errorBody = (await response.json().catch(() => null)) as {
183
+ message?: string
184
+ code?: string | number
185
+ } | null
186
+ throw new TeamsError(
187
+ errorBody?.message ?? `HTTP ${response.status}`,
188
+ errorBody?.code?.toString() ?? `http_${response.status}`
189
+ )
190
+ }
191
+
192
+ return response.json() as Promise<T>
193
+ }
194
+
195
+ async testAuth(): Promise<TeamsUser> {
196
+ interface UserProperties {
197
+ userDetails?: string
198
+ locale?: string
199
+ }
200
+ const props = await this.request<UserProperties>('GET', '/users/ME/properties')
201
+ const userDetails = props.userDetails ? JSON.parse(props.userDetails) : {}
202
+ return {
203
+ id: 'ME',
204
+ displayName: userDetails.name || 'Teams User',
205
+ }
206
+ }
207
+
208
+ async listTeams(): Promise<TeamsTeam[]> {
209
+ interface Conversation {
210
+ id: string
211
+ threadProperties?: {
212
+ groupId?: string
213
+ spaceThreadTopic?: string
214
+ productThreadType?: string
215
+ threadType?: string
216
+ }
217
+ }
218
+ interface ConversationsResponse {
219
+ conversations: Conversation[]
220
+ }
221
+ const data = await this.request<ConversationsResponse>('GET', '/users/ME/conversations')
222
+
223
+ const teamsMap = new Map<string, TeamsTeam>()
224
+ for (const conv of data.conversations) {
225
+ const tp = conv.threadProperties
226
+ if (!tp?.groupId) continue
227
+ if (!tp.productThreadType?.includes('Teams') && tp.threadType !== 'space') continue
228
+
229
+ if (!teamsMap.has(tp.groupId)) {
230
+ teamsMap.set(tp.groupId, {
231
+ id: tp.groupId,
232
+ name: tp.spaceThreadTopic || 'Unknown Team',
233
+ })
234
+ }
235
+ }
236
+
237
+ return Array.from(teamsMap.values())
238
+ }
239
+
240
+ async getTeam(teamId: string): Promise<TeamsTeam> {
241
+ return this.request<TeamsTeam>('GET', `/csa/api/v1/teams/${teamId}`, undefined, CSA_API_BASE)
242
+ }
243
+
244
+ async listChannels(teamId: string): Promise<TeamsChannel[]> {
245
+ return this.request<TeamsChannel[]>(
246
+ 'GET',
247
+ `/csa/api/v1/teams/${teamId}/channels`,
248
+ undefined,
249
+ CSA_API_BASE
250
+ )
251
+ }
252
+
253
+ async getChannel(teamId: string, channelId: string): Promise<TeamsChannel> {
254
+ return this.request<TeamsChannel>(
255
+ 'GET',
256
+ `/csa/api/v1/teams/${teamId}/channels/${channelId}`,
257
+ undefined,
258
+ CSA_API_BASE
259
+ )
260
+ }
261
+
262
+ async sendMessage(teamId: string, channelId: string, content: string): Promise<TeamsMessage> {
263
+ return this.request<TeamsMessage>(
264
+ 'POST',
265
+ `/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/messages`,
266
+ { content },
267
+ CSA_API_BASE
268
+ )
269
+ }
270
+
271
+ async getMessages(
272
+ teamId: string,
273
+ channelId: string,
274
+ limit: number = 50
275
+ ): Promise<TeamsMessage[]> {
276
+ return this.request<TeamsMessage[]>(
277
+ 'GET',
278
+ `/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/messages?limit=${limit}`,
279
+ undefined,
280
+ CSA_API_BASE
281
+ )
282
+ }
283
+
284
+ async getMessage(teamId: string, channelId: string, messageId: string): Promise<TeamsMessage> {
285
+ return this.request<TeamsMessage>(
286
+ 'GET',
287
+ `/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/messages/${messageId}`,
288
+ undefined,
289
+ CSA_API_BASE
290
+ )
291
+ }
292
+
293
+ async deleteMessage(teamId: string, channelId: string, messageId: string): Promise<void> {
294
+ return this.request<void>(
295
+ 'DELETE',
296
+ `/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/messages/${messageId}`,
297
+ undefined,
298
+ CSA_API_BASE
299
+ )
300
+ }
301
+
302
+ async addReaction(
303
+ teamId: string,
304
+ channelId: string,
305
+ messageId: string,
306
+ emoji: string
307
+ ): Promise<void> {
308
+ return this.request<void>(
309
+ 'POST',
310
+ `/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/messages/${messageId}/reactions`,
311
+ { emoji },
312
+ CSA_API_BASE
313
+ )
314
+ }
315
+
316
+ async removeReaction(
317
+ teamId: string,
318
+ channelId: string,
319
+ messageId: string,
320
+ emoji: string
321
+ ): Promise<void> {
322
+ return this.request<void>(
323
+ 'DELETE',
324
+ `/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/messages/${messageId}/reactions/${emoji}`,
325
+ undefined,
326
+ CSA_API_BASE
327
+ )
328
+ }
329
+
330
+ async listUsers(teamId: string): Promise<TeamsUser[]> {
331
+ return this.request<TeamsUser[]>(
332
+ 'GET',
333
+ `/csa/api/v1/teams/${teamId}/members`,
334
+ undefined,
335
+ CSA_API_BASE
336
+ )
337
+ }
338
+
339
+ async getUser(userId: string): Promise<TeamsUser> {
340
+ return this.request<TeamsUser>('GET', `/csa/api/v1/users/${userId}`, undefined, CSA_API_BASE)
341
+ }
342
+
343
+ async uploadFile(teamId: string, channelId: string, filePath: string): Promise<TeamsFile> {
344
+ const fileBuffer = await readFile(filePath)
345
+ const filename = basename(filePath) || 'file'
346
+
347
+ const formData = new FormData()
348
+ formData.append('file', new Blob([fileBuffer]), filename)
349
+
350
+ return this.requestFormData<TeamsFile>(
351
+ `/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/files`,
352
+ formData,
353
+ CSA_API_BASE
354
+ )
355
+ }
356
+
357
+ async listFiles(teamId: string, channelId: string): Promise<TeamsFile[]> {
358
+ return this.request<TeamsFile[]>(
359
+ 'GET',
360
+ `/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/files`,
361
+ undefined,
362
+ CSA_API_BASE
363
+ )
364
+ }
365
+ }
@@ -0,0 +1,99 @@
1
+ import { afterEach, beforeEach, expect, spyOn, test } from 'bun:test'
2
+ import { TeamsClient } from '../client'
3
+ import { TeamsCredentialManager } from '../credential-manager'
4
+ import { TeamsTokenExtractor } from '../token-extractor'
5
+
6
+ let extractorExtractSpy: ReturnType<typeof spyOn>
7
+ let clientTestAuthSpy: ReturnType<typeof spyOn>
8
+ let clientListTeamsSpy: ReturnType<typeof spyOn>
9
+ let credManagerLoadConfigSpy: ReturnType<typeof spyOn>
10
+ let credManagerSaveConfigSpy: ReturnType<typeof spyOn>
11
+ let credManagerClearCredentialsSpy: ReturnType<typeof spyOn>
12
+ let credManagerIsTokenExpiredSpy: ReturnType<typeof spyOn>
13
+
14
+ beforeEach(() => {
15
+ extractorExtractSpy = spyOn(TeamsTokenExtractor.prototype, 'extract').mockResolvedValue({
16
+ token: 'test-skype-token-123',
17
+ })
18
+
19
+ clientTestAuthSpy = spyOn(TeamsClient.prototype, 'testAuth').mockResolvedValue({
20
+ id: 'user-123',
21
+ displayName: 'Test User',
22
+ email: 'test@example.com',
23
+ })
24
+
25
+ clientListTeamsSpy = spyOn(TeamsClient.prototype, 'listTeams').mockResolvedValue([
26
+ { id: 'team-1', name: 'Team One' },
27
+ { id: 'team-2', name: 'Team Two' },
28
+ ])
29
+
30
+ credManagerLoadConfigSpy = spyOn(
31
+ TeamsCredentialManager.prototype,
32
+ 'loadConfig'
33
+ ).mockResolvedValue(null)
34
+
35
+ credManagerSaveConfigSpy = spyOn(
36
+ TeamsCredentialManager.prototype,
37
+ 'saveConfig'
38
+ ).mockResolvedValue(undefined)
39
+
40
+ credManagerClearCredentialsSpy = spyOn(
41
+ TeamsCredentialManager.prototype,
42
+ 'clearCredentials'
43
+ ).mockResolvedValue(undefined)
44
+
45
+ credManagerIsTokenExpiredSpy = spyOn(
46
+ TeamsCredentialManager.prototype,
47
+ 'isTokenExpired'
48
+ ).mockResolvedValue(false)
49
+ })
50
+
51
+ afterEach(() => {
52
+ extractorExtractSpy?.mockRestore()
53
+ clientTestAuthSpy?.mockRestore()
54
+ clientListTeamsSpy?.mockRestore()
55
+ credManagerLoadConfigSpy?.mockRestore()
56
+ credManagerSaveConfigSpy?.mockRestore()
57
+ credManagerClearCredentialsSpy?.mockRestore()
58
+ credManagerIsTokenExpiredSpy?.mockRestore()
59
+ })
60
+
61
+ test('extract: calls TeamsTokenExtractor', async () => {
62
+ const extractor = new TeamsTokenExtractor()
63
+ const result = await extractor.extract()
64
+ expect(result).toBeDefined()
65
+ expect(result?.token).toBe('test-skype-token-123')
66
+ })
67
+
68
+ test('extract: validates token with TeamsClient', async () => {
69
+ const client = new TeamsClient('test-skype-token-123')
70
+ const authInfo = await client.testAuth()
71
+ expect(authInfo).toBeDefined()
72
+ expect(authInfo.id).toBe('user-123')
73
+ expect(authInfo.displayName).toBe('Test User')
74
+ })
75
+
76
+ test('extract: discovers teams', async () => {
77
+ const client = new TeamsClient('test-skype-token-123')
78
+ const teams = await client.listTeams()
79
+ expect(teams).toHaveLength(2)
80
+ expect(teams[0].id).toBe('team-1')
81
+ })
82
+
83
+ test('logout: clears credentials', async () => {
84
+ const credManager = new TeamsCredentialManager()
85
+ await credManager.clearCredentials()
86
+ expect(credManager.clearCredentials).toHaveBeenCalled()
87
+ })
88
+
89
+ test('status: returns auth state when not authenticated', async () => {
90
+ const credManager = new TeamsCredentialManager()
91
+ const config = await credManager.loadConfig()
92
+ expect(config).toBeNull()
93
+ })
94
+
95
+ test('status: checks token expiry', async () => {
96
+ const credManager = new TeamsCredentialManager()
97
+ const isExpired = await credManager.isTokenExpired()
98
+ expect(isExpired).toBe(false)
99
+ })
@@ -0,0 +1,232 @@
1
+ import { Command } from 'commander'
2
+ import { handleError } from '../../../shared/utils/error-handler'
3
+ import { formatOutput } from '../../../shared/utils/output'
4
+ import { TeamsClient } from '../client'
5
+ import { TeamsCredentialManager } from '../credential-manager'
6
+ import { TeamsTokenExtractor } from '../token-extractor'
7
+
8
+ export async function extractAction(options: {
9
+ pretty?: boolean
10
+ debug?: boolean
11
+ token?: string
12
+ }): Promise<void> {
13
+ try {
14
+ let token: string
15
+
16
+ if (options.token) {
17
+ token = options.token
18
+ if (options.debug) {
19
+ console.error(`[debug] Using provided token: ${token.substring(0, 20)}...`)
20
+ }
21
+ } else {
22
+ const extractor = new TeamsTokenExtractor()
23
+
24
+ if (process.platform === 'darwin') {
25
+ console.log('')
26
+ console.log(' Extracting your Microsoft Teams credentials...')
27
+ console.log('')
28
+ console.log(' Your Mac may ask for your password to access Keychain.')
29
+ console.log(' This is required because Teams encrypts your login token')
30
+ console.log(' using macOS Keychain for security.')
31
+ console.log('')
32
+ console.log(' What happens:')
33
+ console.log(" 1. We read the encrypted token from Teams' cookies")
34
+ console.log(' 2. macOS Keychain decrypts it (requires your password)')
35
+ console.log(' 3. The token is stored locally in ~/.config/agent-messenger/')
36
+ console.log('')
37
+ console.log(' Your password is never stored or transmitted anywhere.')
38
+ console.log('')
39
+ }
40
+
41
+ if (options.debug) {
42
+ console.error(`[debug] Extracting Teams token...`)
43
+ }
44
+
45
+ const extracted = await extractor.extract()
46
+
47
+ if (!extracted) {
48
+ console.log(
49
+ formatOutput(
50
+ {
51
+ error:
52
+ 'No Teams token found. Make sure Microsoft Teams desktop app is installed and logged in.',
53
+ hint: 'Run with --token <token> to manually provide a token, or --debug for more info.',
54
+ },
55
+ options.pretty
56
+ )
57
+ )
58
+ process.exit(1)
59
+ }
60
+ token = extracted.token
61
+ }
62
+
63
+ if (options.debug) {
64
+ console.error(`[debug] Token extracted: ${token.substring(0, 20)}...`)
65
+ }
66
+
67
+ try {
68
+ const client = new TeamsClient(token)
69
+
70
+ if (options.debug) {
71
+ console.error(`[debug] Testing token validity...`)
72
+ }
73
+
74
+ const authInfo = await client.testAuth()
75
+
76
+ if (options.debug) {
77
+ console.error(`[debug] ✓ Token valid for user: ${authInfo.displayName}`)
78
+ console.error(`[debug] Discovering teams...`)
79
+ }
80
+
81
+ const teams = await client.listTeams()
82
+
83
+ if (options.debug) {
84
+ console.error(`[debug] ✓ Found ${teams.length} team(s)`)
85
+ }
86
+
87
+ if (teams.length === 0) {
88
+ console.log(
89
+ formatOutput(
90
+ {
91
+ error:
92
+ 'No teams found. Make sure you are a member of at least one Microsoft Teams team.',
93
+ },
94
+ options.pretty
95
+ )
96
+ )
97
+ process.exit(1)
98
+ }
99
+
100
+ const credManager = new TeamsCredentialManager()
101
+ const teamMap: Record<string, { team_id: string; team_name: string }> = {}
102
+
103
+ for (const team of teams) {
104
+ teamMap[team.id] = {
105
+ team_id: team.id,
106
+ team_name: team.name,
107
+ }
108
+ }
109
+
110
+ const config = {
111
+ token: token,
112
+ current_team: teams[0].id,
113
+ teams: teamMap,
114
+ token_expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
115
+ }
116
+
117
+ await credManager.saveConfig(config)
118
+
119
+ if (options.debug) {
120
+ console.error(`[debug] ✓ Credentials saved`)
121
+ }
122
+
123
+ const output = {
124
+ teams: teams.map((t) => `${t.id}/${t.name}`),
125
+ current: teams[0].id,
126
+ }
127
+
128
+ console.log(formatOutput(output, options.pretty))
129
+ } catch (error) {
130
+ const errorMessage = (error as Error).message
131
+ const is401 = errorMessage.includes('401') || errorMessage.includes('Unauthorized')
132
+ console.log(
133
+ formatOutput(
134
+ {
135
+ error: `Token validation failed: ${errorMessage}`,
136
+ hint: is401
137
+ ? 'Token expired. Open Microsoft Teams, send a message to refresh your session, then run "auth extract" again.'
138
+ : 'Make sure Microsoft Teams desktop app is running and you are logged in.',
139
+ },
140
+ options.pretty
141
+ )
142
+ )
143
+ process.exit(1)
144
+ }
145
+ } catch (error) {
146
+ handleError(error as Error)
147
+ }
148
+ }
149
+
150
+ export async function logoutAction(options: { pretty?: boolean }): Promise<void> {
151
+ try {
152
+ const credManager = new TeamsCredentialManager()
153
+ const config = await credManager.loadConfig()
154
+
155
+ if (!config?.token) {
156
+ console.log(
157
+ formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty)
158
+ )
159
+ process.exit(1)
160
+ }
161
+
162
+ await credManager.clearCredentials()
163
+
164
+ console.log(formatOutput({ removed: 'teams', success: true }, options.pretty))
165
+ } catch (error) {
166
+ handleError(error as Error)
167
+ }
168
+ }
169
+
170
+ export async function statusAction(options: { pretty?: boolean }): Promise<void> {
171
+ try {
172
+ const credManager = new TeamsCredentialManager()
173
+ const config = await credManager.loadConfig()
174
+
175
+ if (!config?.token) {
176
+ console.log(
177
+ formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty)
178
+ )
179
+ process.exit(1)
180
+ }
181
+
182
+ let authInfo: { id: string; displayName: string } | null = null
183
+ let valid = false
184
+ const isExpired = await credManager.isTokenExpired()
185
+
186
+ if (!isExpired) {
187
+ try {
188
+ const client = new TeamsClient(config.token, config.token_expires_at)
189
+ authInfo = await client.testAuth()
190
+ valid = true
191
+ } catch {
192
+ valid = false
193
+ }
194
+ }
195
+
196
+ const output = {
197
+ authenticated: valid,
198
+ user: authInfo?.displayName,
199
+ current_team: config.current_team,
200
+ teams_count: Object.keys(config.teams).length,
201
+ token_expires_at: config.token_expires_at ?? null,
202
+ token_expired: isExpired,
203
+ }
204
+
205
+ console.log(formatOutput(output, options.pretty))
206
+ } catch (error) {
207
+ handleError(error as Error)
208
+ }
209
+ }
210
+
211
+ export const authCommand = new Command('auth')
212
+ .description('Authentication commands')
213
+ .addCommand(
214
+ new Command('extract')
215
+ .description('Extract token from Microsoft Teams desktop app')
216
+ .option('--pretty', 'Pretty print JSON output')
217
+ .option('--debug', 'Show debug output for troubleshooting')
218
+ .option('--token <token>', 'Manually provide a token (bypasses auto-extraction)')
219
+ .action(extractAction)
220
+ )
221
+ .addCommand(
222
+ new Command('logout')
223
+ .description('Logout from Microsoft Teams')
224
+ .option('--pretty', 'Pretty print JSON output')
225
+ .action(logoutAction)
226
+ )
227
+ .addCommand(
228
+ new Command('status')
229
+ .description('Show authentication status')
230
+ .option('--pretty', 'Pretty print JSON output')
231
+ .action(statusAction)
232
+ )