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
@@ -2,14 +2,178 @@ import { Command } from 'commander'
2
2
 
3
3
  import { collectBrowserProfileOption } from '@/shared/chromium'
4
4
  import { handleError } from '@/shared/utils/error-handler'
5
+ import { isInteractive } from '@/shared/utils/interactive'
5
6
  import { formatOutput } from '@/shared/utils/output'
6
- import { debug } from '@/shared/utils/stderr'
7
+ import { debug, info } from '@/shared/utils/stderr'
7
8
 
8
9
  import { TeamsClient } from '../client'
9
10
  import { TeamsCredentialManager } from '../credential-manager'
11
+ import { completeDeviceCode, loginWithDeviceCode, PendingApprovalError, startDeviceCode } from '../device-login'
12
+ import { probeAccountType } from '../realm-discovery'
10
13
  import { TeamsTokenExtractor } from '../token-extractor'
11
14
  import type { TeamsAccount, TeamsAccountType, TeamsConfig } from '../types'
12
15
 
16
+ interface LoginOptions {
17
+ pretty?: boolean
18
+ debug?: boolean
19
+ deviceCode?: string
20
+ clientId?: string
21
+ accountType?: string
22
+ email?: string
23
+ }
24
+
25
+ export async function loginAction(options: LoginOptions): Promise<void> {
26
+ try {
27
+ const explicitAccountType = resolveLoginAccountType(options.accountType)
28
+ if (options.deviceCode) {
29
+ await finishDeviceCode(options, explicitAccountType)
30
+ return
31
+ }
32
+ if (!isInteractive()) {
33
+ const accountType = options.email
34
+ ? await resolveAccountTypeFromEmail(options, explicitAccountType)
35
+ : explicitAccountType
36
+ await startNonInteractiveLogin(options.clientId, accountType)
37
+ return
38
+ }
39
+
40
+ const debugLog = options.debug ? (msg: string) => debug(`[debug] ${msg}`) : undefined
41
+ const accountType = await resolveAccountTypeFromEmail(options, explicitAccountType)
42
+ const result = await loginWithDeviceCode({
43
+ clientId: options.clientId,
44
+ accountType,
45
+ onCode: async ({ verificationUri, userCode }) => {
46
+ info(`\nTo sign in, open ${verificationUri} and enter code ${userCode}\n`)
47
+ info(`Waiting for approval of code ${userCode}...`)
48
+ },
49
+ onPending: () => info('Approved. Finishing sign-in...'),
50
+ debug: debugLog,
51
+ })
52
+
53
+ console.log(
54
+ formatOutput(
55
+ {
56
+ authenticated: true,
57
+ account_type: result.accountType,
58
+ user: result.userName,
59
+ teams: result.teams.map((t) => `${t.id}/${t.name}`),
60
+ current: result.current,
61
+ },
62
+ options.pretty,
63
+ ),
64
+ )
65
+ } catch (error) {
66
+ handleError(error as Error)
67
+ }
68
+ }
69
+
70
+ function resolveLoginAccountType(value: string | undefined): TeamsAccountType {
71
+ if (value === undefined) return 'work'
72
+ if (value === 'work' || value === 'personal') return value
73
+ throw new Error('Invalid account type. Use "work" or "personal".')
74
+ }
75
+
76
+ // When the user did not force --account-type, look the account up by email so a
77
+ // personal account is routed to the personal flow up front instead of failing
78
+ // after approval. Any misstep falls back to the caller's default account type.
79
+ async function resolveAccountTypeFromEmail(
80
+ options: LoginOptions,
81
+ fallback: TeamsAccountType,
82
+ ): Promise<TeamsAccountType> {
83
+ if (options.accountType !== undefined) return fallback
84
+
85
+ const email = options.email ?? (isInteractive() ? await promptEmail() : undefined)
86
+ if (!email) return fallback
87
+
88
+ const probed = await probeAccountType(email)
89
+ if (!probed) return fallback
90
+ if (probed === 'personal') {
91
+ info('Detected a personal Microsoft account. Using personal sign-in.')
92
+ }
93
+ return probed
94
+ }
95
+
96
+ async function promptEmail(): Promise<string | undefined> {
97
+ const { createInterface } = await import('node:readline/promises')
98
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true })
99
+ try {
100
+ const answer = await rl.question('Microsoft email (press Enter to skip): ')
101
+ return answer.trim() || undefined
102
+ } finally {
103
+ rl.close()
104
+ }
105
+ }
106
+
107
+ async function startNonInteractiveLogin(
108
+ clientIdOverride: string | undefined,
109
+ accountType: TeamsAccountType,
110
+ ): Promise<void> {
111
+ const { info: device, clientId } = await startDeviceCode(clientIdOverride, accountType)
112
+ const clientIdFlag = clientIdOverride ? ` --client-id ${clientIdOverride}` : ''
113
+ const accountTypeFlag = accountType === 'personal' ? ' --account-type personal' : ''
114
+ console.log(
115
+ formatOutput({
116
+ next_action: 'authorize',
117
+ account_type: accountType,
118
+ verification_uri: device.verificationUri,
119
+ verification_uri_complete: device.verificationUriComplete,
120
+ user_code: device.userCode,
121
+ device_code: device.deviceCode,
122
+ client_id: clientId,
123
+ expires_at: Date.now() + device.expiresIn * 1000,
124
+ message: `Show the user \`verification_uri\` and \`user_code\` (or \`verification_uri_complete\`) and ask them to approve in a browser. After they approve, run \`agent-teams auth login --device-code <device_code>${clientIdFlag}${accountTypeFlag}\` to finish.`,
125
+ }),
126
+ )
127
+ process.exit(0)
128
+ }
129
+
130
+ async function finishDeviceCode(options: LoginOptions, accountType: TeamsAccountType): Promise<void> {
131
+ const { getTeamsAppClientId } = await import('../app-config')
132
+ const clientId = options.clientId ?? getTeamsAppClientId(accountType).clientId
133
+ try {
134
+ const result = await completeDeviceCode(options.deviceCode!, clientId, undefined, accountType)
135
+ console.log(
136
+ formatOutput(
137
+ {
138
+ authenticated: true,
139
+ account_type: result.accountType,
140
+ user: result.userName,
141
+ teams: result.teams.map((t) => `${t.id}/${t.name}`),
142
+ current: result.current,
143
+ },
144
+ options.pretty,
145
+ ),
146
+ )
147
+ } catch (error) {
148
+ if (error instanceof PendingApprovalError) {
149
+ const clientIdFlag = options.clientId ? ` --client-id ${options.clientId}` : ''
150
+ const accountTypeFlag = options.accountType ? ` --account-type ${accountType}` : ''
151
+ console.log(
152
+ formatOutput(
153
+ {
154
+ next_action: 'still_pending',
155
+ device_code: options.deviceCode,
156
+ message: `User has not approved yet. Confirm they completed authorization in the browser, then run \`agent-teams auth login --device-code <device_code>${clientIdFlag}${accountTypeFlag}\` again.`,
157
+ },
158
+ options.pretty,
159
+ ),
160
+ )
161
+ process.exit(0)
162
+ }
163
+ console.log(
164
+ formatOutput(
165
+ {
166
+ next_action: 'restart',
167
+ error: (error as Error).message,
168
+ message: 'This device code is no longer valid. Run `agent-teams auth login` to start over.',
169
+ },
170
+ options.pretty,
171
+ ),
172
+ )
173
+ process.exit(1)
174
+ }
175
+ }
176
+
13
177
  interface ValidatedTeamsToken {
14
178
  client: TeamsClient
15
179
  accountType: TeamsAccountType
@@ -308,7 +472,9 @@ export async function logoutAction(options: { pretty?: boolean }): Promise<void>
308
472
  const config = await credManager.loadConfig()
309
473
 
310
474
  if (!config || Object.keys(config.accounts).length === 0) {
311
- console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
475
+ console.log(
476
+ formatOutput({ error: 'Not authenticated. Run "auth login" or "auth extract" first.' }, options.pretty),
477
+ )
312
478
  process.exit(1)
313
479
  }
314
480
 
@@ -326,7 +492,9 @@ export async function statusAction(options: { pretty?: boolean }): Promise<void>
326
492
  const config = await credManager.loadConfig()
327
493
 
328
494
  if (!config || Object.keys(config.accounts).length === 0) {
329
- console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
495
+ console.log(
496
+ formatOutput({ error: 'Not authenticated. Run "auth login" or "auth extract" first.' }, options.pretty),
497
+ )
330
498
  process.exit(1)
331
499
  }
332
500
 
@@ -427,6 +595,17 @@ export async function switchAccountAction(accountType: string, options: { pretty
427
595
 
428
596
  export const authCommand = new Command('auth')
429
597
  .description('Authentication commands')
598
+ .addCommand(
599
+ new Command('login')
600
+ .description('Sign in via device code (no desktop app needed). Detects the account type from your email.')
601
+ .option('--pretty', 'Pretty print JSON output')
602
+ .option('--debug', 'Show debug output for troubleshooting')
603
+ .option('--device-code <code>', 'Finish a non-interactive login started earlier')
604
+ .option('--client-id <id>', 'Override the AAD client ID')
605
+ .option('--account-type <type>', 'Force account type: work or personal (skips email detection)')
606
+ .option('--email <email>', 'Microsoft email, used to auto-detect work vs personal')
607
+ .action(loginAction),
608
+ )
430
609
  .addCommand(
431
610
  new Command('extract')
432
611
  .description('Extract token from Microsoft Teams desktop app or a supported Chromium browser')
@@ -1,13 +1,22 @@
1
1
  import { afterEach, beforeEach, expect, mock, spyOn, it } from 'bun:test'
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
2
5
 
3
6
  import { TeamsClient } from '../client'
4
7
  import { TeamsCredentialManager } from '../credential-manager'
5
- import { infoAction, listAction, uploadAction } from './file'
8
+ import { TeamsAuthCapabilityError } from '../types'
9
+ import { downloadAction, infoAction, listAction, uploadAction } from './file'
6
10
 
7
11
  let clientUploadFileSpy: ReturnType<typeof spyOn>
8
12
  let clientListFilesSpy: ReturnType<typeof spyOn>
13
+ let clientDownloadFileSpy: ReturnType<typeof spyOn>
9
14
  let credManagerLoadConfigSpy: ReturnType<typeof spyOn>
15
+ let processExitSpy: ReturnType<typeof spyOn>
16
+ const tempDirs: string[] = []
10
17
  const originalConsoleLog = console.log
18
+ const originalConsoleError = console.error
19
+ const originalCwd = process.cwd()
11
20
 
12
21
  beforeEach(() => {
13
22
  clientUploadFileSpy = spyOn(TeamsClient.prototype, 'uploadFile').mockResolvedValue({
@@ -35,6 +44,17 @@ beforeEach(() => {
35
44
  },
36
45
  ])
37
46
 
47
+ clientDownloadFileSpy = spyOn(TeamsClient.prototype, 'downloadFile').mockResolvedValue({
48
+ buffer: Buffer.from('downloaded-content'),
49
+ file: {
50
+ id: 'file_123',
51
+ name: 'folder\\test.pdf',
52
+ size: 18,
53
+ url: 'https://teams.microsoft.com/files/123/test.pdf',
54
+ contentType: 'application/pdf',
55
+ },
56
+ })
57
+
38
58
  credManagerLoadConfigSpy = spyOn(TeamsCredentialManager.prototype, 'loadConfig').mockResolvedValue({
39
59
  current_account: 'work',
40
60
  accounts: {
@@ -46,13 +66,24 @@ beforeEach(() => {
46
66
  },
47
67
  },
48
68
  })
69
+
70
+ processExitSpy = spyOn(process, 'exit').mockImplementation((code?: string | number | null | undefined): never => {
71
+ throw new Error(`process.exit:${code ?? 0}`)
72
+ })
49
73
  })
50
74
 
51
75
  afterEach(() => {
52
76
  clientUploadFileSpy?.mockRestore()
53
77
  clientListFilesSpy?.mockRestore()
78
+ clientDownloadFileSpy?.mockRestore()
54
79
  credManagerLoadConfigSpy?.mockRestore()
80
+ processExitSpy?.mockRestore()
55
81
  console.log = originalConsoleLog
82
+ console.error = originalConsoleError
83
+ process.chdir(originalCwd)
84
+ for (const dir of tempDirs.splice(0)) {
85
+ rmSync(dir, { recursive: true, force: true })
86
+ }
56
87
  })
57
88
 
58
89
  it('upload: sends multipart request and returns file info', async () => {
@@ -90,3 +121,63 @@ it('info: returns single file details', async () => {
90
121
  expect(output).toContain('file_123')
91
122
  expect(output).toContain('test.pdf')
92
123
  })
124
+
125
+ it('download: writes to a directory using a safe basename', async () => {
126
+ const consoleSpy = mock((_msg: string) => {})
127
+ console.log = consoleSpy
128
+ const dir = mkdtempSync(join(tmpdir(), 'teams-download-dir-'))
129
+ tempDirs.push(dir)
130
+
131
+ await downloadAction('team_123', 'ch_456', 'file_123', dir, { pretty: false })
132
+
133
+ const downloadedPath = join(dir, 'test.pdf')
134
+ expect(readFileSync(downloadedPath, 'utf8')).toBe('downloaded-content')
135
+ expect(clientDownloadFileSpy).toHaveBeenCalledWith('team_123', 'ch_456', 'file_123')
136
+ const output = consoleSpy.mock.calls[0][0]
137
+ expect(output).toContain(downloadedPath)
138
+ })
139
+
140
+ it('download: writes to an explicit output file path', async () => {
141
+ const consoleSpy = mock((_msg: string) => {})
142
+ console.log = consoleSpy
143
+ const dir = mkdtempSync(join(tmpdir(), 'teams-download-file-'))
144
+ tempDirs.push(dir)
145
+ const outputPath = join(dir, 'renamed.pdf')
146
+
147
+ await downloadAction('team_123', 'ch_456', 'file_123', outputPath, { pretty: false })
148
+
149
+ expect(readFileSync(outputPath, 'utf8')).toBe('downloaded-content')
150
+ const output = consoleSpy.mock.calls[0][0]
151
+ expect(output).toContain(outputPath)
152
+ })
153
+
154
+ it('download: writes to cwd when output path is omitted', async () => {
155
+ const consoleSpy = mock((_msg: string) => {})
156
+ console.log = consoleSpy
157
+ const dir = mkdtempSync(join(tmpdir(), 'teams-download-cwd-'))
158
+ tempDirs.push(dir)
159
+ process.chdir(dir)
160
+
161
+ await downloadAction('team_123', 'ch_456', 'file_123', undefined, { pretty: false })
162
+
163
+ const downloadedPath = join(dir, 'test.pdf')
164
+ expect(readFileSync(downloadedPath, 'utf8')).toBe('downloaded-content')
165
+ const output = consoleSpy.mock.calls[0][0]
166
+ expect(output).toContain(downloadedPath)
167
+ })
168
+
169
+ it('download: exits on capability error without writing a partial file', async () => {
170
+ const consoleErrorSpy = mock((_msg: string) => {})
171
+ console.error = consoleErrorSpy
172
+ const dir = mkdtempSync(join(tmpdir(), 'teams-download-capability-'))
173
+ tempDirs.push(dir)
174
+ const outputPath = join(dir, 'sharepoint.docx')
175
+ clientDownloadFileSpy.mockRejectedValue(new TeamsAuthCapabilityError())
176
+
177
+ await expect(downloadAction('team_123', 'ch_456', 'file_123', outputPath, { pretty: false })).rejects.toThrow(
178
+ 'process.exit:1',
179
+ )
180
+
181
+ expect(existsSync(outputPath)).toBe(false)
182
+ expect(consoleErrorSpy.mock.calls[0][0]).toContain('Requires `agent-teams auth login`')
183
+ })
@@ -1,4 +1,5 @@
1
- import { resolve } from 'node:path'
1
+ import { statSync, writeFileSync } from 'node:fs'
2
+ import { basename, join, resolve } from 'node:path'
2
3
 
3
4
  import { Command } from 'commander'
4
5
 
@@ -8,6 +9,7 @@ import { formatOutput } from '@/shared/utils/output'
8
9
  import { TeamsClient } from '../client'
9
10
  import { TeamsCredentialManager } from '../credential-manager'
10
11
  import type { TeamsFile } from '../types'
12
+ import { TeamsAuthCapabilityError } from '../types'
11
13
 
12
14
  export async function uploadAction(
13
15
  teamId: string,
@@ -122,6 +124,69 @@ export async function infoAction(
122
124
  }
123
125
  }
124
126
 
127
+ export async function downloadAction(
128
+ teamId: string,
129
+ channelId: string,
130
+ fileId: string,
131
+ outputPath: string | undefined,
132
+ options: { pretty?: boolean },
133
+ ): Promise<void> {
134
+ try {
135
+ const credManager = new TeamsCredentialManager()
136
+ const cred = await credManager.getTokenWithExpiry()
137
+
138
+ if (!cred) {
139
+ console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
140
+ process.exit(1)
141
+ }
142
+
143
+ const client = await new TeamsClient().login({
144
+ token: cred.token,
145
+ tokenExpiresAt: cred.tokenExpiresAt,
146
+ accountType: cred.accountType,
147
+ region: cred.region,
148
+ })
149
+ const { buffer, file } = await client.downloadFile(teamId, channelId, fileId)
150
+
151
+ const safeName = basename(file.name.replace(/\\/g, '/'))
152
+ let destPath = outputPath ? resolve(outputPath) : resolve(safeName)
153
+ let isDirectory = false
154
+ try {
155
+ isDirectory = statSync(destPath).isDirectory()
156
+ } catch (error) {
157
+ const err = error as NodeJS.ErrnoException
158
+ if (err.code !== 'ENOENT') {
159
+ throw error
160
+ }
161
+ }
162
+
163
+ if (isDirectory) {
164
+ destPath = join(destPath, safeName)
165
+ }
166
+
167
+ writeFileSync(destPath, buffer)
168
+
169
+ console.log(
170
+ formatOutput(
171
+ {
172
+ id: file.id,
173
+ name: file.name,
174
+ size: file.size,
175
+ content_type: file.contentType || null,
176
+ path: destPath,
177
+ },
178
+ options.pretty,
179
+ ),
180
+ )
181
+ } catch (error) {
182
+ if (error instanceof TeamsAuthCapabilityError) {
183
+ console.error(error.message)
184
+ process.exit(1)
185
+ }
186
+ handleError(error as Error)
187
+ }
188
+ }
189
+
125
190
  export const fileCommand = new Command('file')
126
191
  .description('File commands')
127
192
  .addCommand(
@@ -150,3 +215,13 @@ export const fileCommand = new Command('file')
150
215
  .option('--pretty', 'Pretty print JSON output')
151
216
  .action(infoAction),
152
217
  )
218
+ .addCommand(
219
+ new Command('download')
220
+ .description('Download file from channel')
221
+ .argument('<team-id>', 'Team ID')
222
+ .argument('<channel-id>', 'Channel ID')
223
+ .argument('<file-id>', 'File ID')
224
+ .argument('[output-path]', 'Output file or directory path')
225
+ .option('--pretty', 'Pretty print JSON output')
226
+ .action(downloadAction),
227
+ )
@@ -2,14 +2,17 @@ import { afterEach, beforeEach, expect, mock, spyOn, it } from 'bun:test'
2
2
 
3
3
  import { TeamsClient } from '../client'
4
4
  import { TeamsCredentialManager } from '../credential-manager'
5
- import { deleteAction, getAction, listAction, sendAction } from './message'
5
+ import { deleteAction, getAction, listAction, repliesAction, searchAction, sendAction, strictParseInt } from './message'
6
6
 
7
7
  let clientSendMessageSpy: ReturnType<typeof spyOn>
8
8
  let clientGetMessagesSpy: ReturnType<typeof spyOn>
9
+ let clientGetThreadRepliesSpy: ReturnType<typeof spyOn>
9
10
  let clientGetMessageSpy: ReturnType<typeof spyOn>
10
11
  let clientDeleteMessageSpy: ReturnType<typeof spyOn>
12
+ let clientSearchMessagesSpy: ReturnType<typeof spyOn>
11
13
  let credManagerLoadSpy: ReturnType<typeof spyOn>
12
14
  const originalConsoleLog = console.log
15
+ const originalConsoleError = console.error
13
16
 
14
17
  beforeEach(() => {
15
18
  clientSendMessageSpy = spyOn(TeamsClient.prototype, 'sendMessage').mockResolvedValue({
@@ -37,6 +40,18 @@ beforeEach(() => {
37
40
  },
38
41
  ])
39
42
 
43
+ clientGetThreadRepliesSpy = spyOn(TeamsClient.prototype, 'getThreadReplies').mockResolvedValue([
44
+ {
45
+ id: 'reply_123',
46
+ channel_id: 'ch_456',
47
+ author: { id: 'user_789', displayName: 'Test User' },
48
+ content: 'Thread reply',
49
+ timestamp: '2025-01-29T10:02:00Z',
50
+ root_message_id: 'msg_123',
51
+ is_thread_reply: true,
52
+ },
53
+ ])
54
+
40
55
  clientGetMessageSpy = spyOn(TeamsClient.prototype, 'getMessage').mockResolvedValue({
41
56
  id: 'msg_123',
42
57
  channel_id: 'ch_456',
@@ -47,6 +62,20 @@ beforeEach(() => {
47
62
 
48
63
  clientDeleteMessageSpy = spyOn(TeamsClient.prototype, 'deleteMessage').mockResolvedValue(undefined)
49
64
 
65
+ clientSearchMessagesSpy = spyOn(TeamsClient.prototype, 'searchMessages').mockResolvedValue([
66
+ {
67
+ id: 'search_123',
68
+ content: 'Deploy complete',
69
+ author: { id: 'user_789', displayName: 'Test User' },
70
+ channel_id: 'ch_456',
71
+ thread_id: 'thread_123',
72
+ team_name: 'Test Team',
73
+ channel_name: 'General',
74
+ timestamp: '2025-01-29T10:03:00Z',
75
+ permalink: 'https://teams.microsoft.com/l/message/search_123',
76
+ },
77
+ ])
78
+
50
79
  credManagerLoadSpy = spyOn(TeamsCredentialManager.prototype, 'loadConfig').mockResolvedValue({
51
80
  current_account: 'work',
52
81
  accounts: {
@@ -63,10 +92,13 @@ beforeEach(() => {
63
92
  afterEach(() => {
64
93
  clientSendMessageSpy?.mockRestore()
65
94
  clientGetMessagesSpy?.mockRestore()
95
+ clientGetThreadRepliesSpy?.mockRestore()
66
96
  clientGetMessageSpy?.mockRestore()
67
97
  clientDeleteMessageSpy?.mockRestore()
98
+ clientSearchMessagesSpy?.mockRestore()
68
99
  credManagerLoadSpy?.mockRestore()
69
100
  console.log = originalConsoleLog
101
+ console.error = originalConsoleError
70
102
  })
71
103
 
72
104
  it('send: returns message with id', async () => {
@@ -92,6 +124,68 @@ it('list: returns array of messages', async () => {
92
124
  expect(output).toContain('msg_124')
93
125
  })
94
126
 
127
+ it('replies: returns array of thread replies', async () => {
128
+ const consoleSpy = mock((_msg: string) => {})
129
+ console.log = consoleSpy
130
+
131
+ await repliesAction('team_123', 'ch_456', 'msg_123', { limit: 10, pretty: false })
132
+
133
+ expect(clientGetThreadRepliesSpy).toHaveBeenCalledWith('team_123', 'ch_456', 'msg_123', 10)
134
+ expect(consoleSpy).toHaveBeenCalled()
135
+ const output = consoleSpy.mock.calls[0][0]
136
+ expect(output).toContain('reply_123')
137
+ expect(output).toContain('msg_123')
138
+ })
139
+
140
+ it('search: returns search results and passes pagination options', async () => {
141
+ const consoleSpy = mock((_msg: string) => {})
142
+ console.log = consoleSpy
143
+
144
+ await searchAction('deploy', { limit: 10, from: 5, pretty: false })
145
+
146
+ expect(clientSearchMessagesSpy).toHaveBeenCalledWith('deploy', { limit: 10, from: 5 })
147
+ expect(consoleSpy).toHaveBeenCalled()
148
+ const output = consoleSpy.mock.calls[0][0]
149
+ expect(output).toContain('search_123')
150
+ expect(output).toContain('Deploy complete')
151
+ })
152
+
153
+ it.each([
154
+ ['abc', Number.NaN],
155
+ ['1.5', Number.NaN],
156
+ ['1abc', Number.NaN],
157
+ ['', Number.NaN],
158
+ ['10', 10],
159
+ [' 10 ', 10],
160
+ ['-1', -1],
161
+ ])('strictParseInt(%p) -> %p (rejects truncatable strings)', (input, expected) => {
162
+ const result = strictParseInt(input)
163
+ if (Number.isNaN(expected)) {
164
+ expect(Number.isNaN(result)).toBe(true)
165
+ } else {
166
+ expect(result).toBe(expected as number)
167
+ }
168
+ })
169
+
170
+ it.each([
171
+ ['--limit abc', { limit: Number.NaN, from: 0 }],
172
+ ['--limit 0', { limit: 0, from: 0 }],
173
+ ['--limit -1', { limit: -1, from: 0 }],
174
+ ['--from -1', { limit: 20, from: -1 }],
175
+ ])('search: rejects invalid pagination %s', async (_label, options) => {
176
+ const consoleErrorSpy = mock((_msg: string) => {})
177
+ console.error = consoleErrorSpy
178
+ const exitSpy = spyOn(process, 'exit').mockImplementation((code?: string | number | null) => {
179
+ throw new Error(`exit:${code}`)
180
+ })
181
+
182
+ await expect(searchAction('deploy', { ...options, pretty: false })).rejects.toThrow('exit:1')
183
+
184
+ expect(consoleErrorSpy).toHaveBeenCalled()
185
+ expect(clientSearchMessagesSpy).not.toHaveBeenCalled()
186
+ exitSpy.mockRestore()
187
+ })
188
+
95
189
  it('get: returns single message', async () => {
96
190
  const consoleSpy = mock((_msg: string) => {})
97
191
  console.log = consoleSpy