agent-messenger 2.0.0 → 2.2.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 (272) hide show
  1. package/.claude-plugin/marketplace.json +14 -1
  2. package/.claude-plugin/plugin.json +4 -2
  3. package/.env.template +35 -17
  4. package/README.md +37 -33
  5. package/bun.lock +6 -6
  6. package/dist/package.json +11 -3
  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/channeltalk/commands/auth.d.ts.map +1 -1
  11. package/dist/src/platforms/channeltalk/commands/auth.js +35 -28
  12. package/dist/src/platforms/channeltalk/commands/auth.js.map +1 -1
  13. package/dist/src/platforms/channeltalk/ensure-auth.js +6 -6
  14. package/dist/src/platforms/channeltalk/ensure-auth.js.map +1 -1
  15. package/dist/src/platforms/channeltalk/token-extractor.d.ts +23 -1
  16. package/dist/src/platforms/channeltalk/token-extractor.d.ts.map +1 -1
  17. package/dist/src/platforms/channeltalk/token-extractor.js +299 -29
  18. package/dist/src/platforms/channeltalk/token-extractor.js.map +1 -1
  19. package/dist/src/platforms/discord/commands/auth.d.ts.map +1 -1
  20. package/dist/src/platforms/discord/commands/auth.js +57 -49
  21. package/dist/src/platforms/discord/commands/auth.js.map +1 -1
  22. package/dist/src/platforms/discord/ensure-auth.js +3 -3
  23. package/dist/src/platforms/discord/ensure-auth.js.map +1 -1
  24. package/dist/src/platforms/discord/token-extractor.d.ts +6 -1
  25. package/dist/src/platforms/discord/token-extractor.d.ts.map +1 -1
  26. package/dist/src/platforms/discord/token-extractor.js +167 -14
  27. package/dist/src/platforms/discord/token-extractor.js.map +1 -1
  28. package/dist/src/platforms/instagram/client.d.ts +2 -0
  29. package/dist/src/platforms/instagram/client.d.ts.map +1 -1
  30. package/dist/src/platforms/instagram/client.js +2 -2
  31. package/dist/src/platforms/instagram/client.js.map +1 -1
  32. package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
  33. package/dist/src/platforms/instagram/commands/auth.js +107 -14
  34. package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
  35. package/dist/src/platforms/instagram/ensure-auth.d.ts.map +1 -1
  36. package/dist/src/platforms/instagram/ensure-auth.js +57 -11
  37. package/dist/src/platforms/instagram/ensure-auth.js.map +1 -1
  38. package/dist/src/platforms/instagram/index.d.ts +1 -0
  39. package/dist/src/platforms/instagram/index.d.ts.map +1 -1
  40. package/dist/src/platforms/instagram/index.js +1 -0
  41. package/dist/src/platforms/instagram/index.js.map +1 -1
  42. package/dist/src/platforms/instagram/token-extractor.d.ts +44 -0
  43. package/dist/src/platforms/instagram/token-extractor.d.ts.map +1 -0
  44. package/dist/src/platforms/instagram/token-extractor.js +407 -0
  45. package/dist/src/platforms/instagram/token-extractor.js.map +1 -0
  46. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  47. package/dist/src/platforms/kakaotalk/client.js +2 -1
  48. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  49. package/dist/src/platforms/kakaotalk/commands/auth.d.ts.map +1 -1
  50. package/dist/src/platforms/kakaotalk/commands/auth.js +14 -13
  51. package/dist/src/platforms/kakaotalk/commands/auth.js.map +1 -1
  52. package/dist/src/platforms/kakaotalk/protocol/connection.d.ts.map +1 -1
  53. package/dist/src/platforms/kakaotalk/protocol/connection.js +2 -1
  54. package/dist/src/platforms/kakaotalk/protocol/connection.js.map +1 -1
  55. package/dist/src/platforms/line/commands/auth.d.ts.map +1 -1
  56. package/dist/src/platforms/line/commands/auth.js +6 -5
  57. package/dist/src/platforms/line/commands/auth.js.map +1 -1
  58. package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
  59. package/dist/src/platforms/slack/commands/auth.js +11 -10
  60. package/dist/src/platforms/slack/commands/auth.js.map +1 -1
  61. package/dist/src/platforms/slack/token-extractor.d.ts +9 -0
  62. package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
  63. package/dist/src/platforms/slack/token-extractor.js +300 -23
  64. package/dist/src/platforms/slack/token-extractor.js.map +1 -1
  65. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
  66. package/dist/src/platforms/teams/commands/auth.js +9 -8
  67. package/dist/src/platforms/teams/commands/auth.js.map +1 -1
  68. package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
  69. package/dist/src/platforms/teams/ensure-auth.js +2 -1
  70. package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
  71. package/dist/src/platforms/teams/token-extractor.d.ts +5 -0
  72. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  73. package/dist/src/platforms/teams/token-extractor.js +161 -29
  74. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  75. package/dist/src/platforms/telegram/client.d.ts.map +1 -1
  76. package/dist/src/platforms/telegram/client.js +25 -7
  77. package/dist/src/platforms/telegram/client.js.map +1 -1
  78. package/dist/src/platforms/telegram/commands/auth.d.ts.map +1 -1
  79. package/dist/src/platforms/telegram/commands/auth.js +6 -5
  80. package/dist/src/platforms/telegram/commands/auth.js.map +1 -1
  81. package/dist/src/platforms/webex/app-config.d.ts +7 -0
  82. package/dist/src/platforms/webex/app-config.d.ts.map +1 -0
  83. package/dist/src/platforms/webex/app-config.js +20 -0
  84. package/dist/src/platforms/webex/app-config.js.map +1 -0
  85. package/dist/src/platforms/webex/cli.d.ts +5 -0
  86. package/dist/src/platforms/webex/cli.d.ts.map +1 -0
  87. package/dist/src/platforms/webex/cli.js +32 -0
  88. package/dist/src/platforms/webex/cli.js.map +1 -0
  89. package/dist/src/platforms/webex/client.d.ts +55 -0
  90. package/dist/src/platforms/webex/client.d.ts.map +1 -0
  91. package/dist/src/platforms/webex/client.js +299 -0
  92. package/dist/src/platforms/webex/client.js.map +1 -0
  93. package/dist/src/platforms/webex/commands/auth.d.ts +19 -0
  94. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -0
  95. package/dist/src/platforms/webex/commands/auth.js +166 -0
  96. package/dist/src/platforms/webex/commands/auth.js.map +1 -0
  97. package/dist/src/platforms/webex/commands/index.d.ts +6 -0
  98. package/dist/src/platforms/webex/commands/index.d.ts.map +1 -0
  99. package/dist/src/platforms/webex/commands/index.js +6 -0
  100. package/dist/src/platforms/webex/commands/index.js.map +1 -0
  101. package/dist/src/platforms/webex/commands/member.d.ts +7 -0
  102. package/dist/src/platforms/webex/commands/member.d.ts.map +1 -0
  103. package/dist/src/platforms/webex/commands/member.js +34 -0
  104. package/dist/src/platforms/webex/commands/member.js.map +1 -0
  105. package/dist/src/platforms/webex/commands/message.d.ts +26 -0
  106. package/dist/src/platforms/webex/commands/message.d.ts.map +1 -0
  107. package/dist/src/platforms/webex/commands/message.js +153 -0
  108. package/dist/src/platforms/webex/commands/message.js.map +1 -0
  109. package/dist/src/platforms/webex/commands/snapshot.d.ts +9 -0
  110. package/dist/src/platforms/webex/commands/snapshot.d.ts.map +1 -0
  111. package/dist/src/platforms/webex/commands/snapshot.js +72 -0
  112. package/dist/src/platforms/webex/commands/snapshot.js.map +1 -0
  113. package/dist/src/platforms/webex/commands/space.d.ts +11 -0
  114. package/dist/src/platforms/webex/commands/space.d.ts.map +1 -0
  115. package/dist/src/platforms/webex/commands/space.js +59 -0
  116. package/dist/src/platforms/webex/commands/space.js.map +1 -0
  117. package/dist/src/platforms/webex/credential-manager.d.ts +23 -0
  118. package/dist/src/platforms/webex/credential-manager.d.ts.map +1 -0
  119. package/dist/src/platforms/webex/credential-manager.js +148 -0
  120. package/dist/src/platforms/webex/credential-manager.js.map +1 -0
  121. package/dist/src/platforms/webex/ensure-auth.d.ts +2 -0
  122. package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -0
  123. package/dist/src/platforms/webex/ensure-auth.js +36 -0
  124. package/dist/src/platforms/webex/ensure-auth.js.map +1 -0
  125. package/dist/src/platforms/webex/index.d.ts +8 -0
  126. package/dist/src/platforms/webex/index.d.ts.map +1 -0
  127. package/dist/src/platforms/webex/index.js +6 -0
  128. package/dist/src/platforms/webex/index.js.map +1 -0
  129. package/dist/src/platforms/webex/token-extractor.d.ts +28 -0
  130. package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -0
  131. package/dist/src/platforms/webex/token-extractor.js +344 -0
  132. package/dist/src/platforms/webex/token-extractor.js.map +1 -0
  133. package/dist/src/platforms/webex/types.d.ts +127 -0
  134. package/dist/src/platforms/webex/types.d.ts.map +1 -0
  135. package/dist/src/platforms/webex/types.js +64 -0
  136. package/dist/src/platforms/webex/types.js.map +1 -0
  137. package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
  138. package/dist/src/platforms/whatsapp/client.js +6 -2
  139. package/dist/src/platforms/whatsapp/client.js.map +1 -1
  140. package/dist/src/shared/utils/derived-key-cache.d.ts +1 -1
  141. package/dist/src/shared/utils/derived-key-cache.d.ts.map +1 -1
  142. package/dist/src/shared/utils/error-handler.d.ts +1 -1
  143. package/dist/src/shared/utils/error-handler.d.ts.map +1 -1
  144. package/dist/src/shared/utils/error-handler.js +3 -2
  145. package/dist/src/shared/utils/error-handler.js.map +1 -1
  146. package/dist/src/shared/utils/stderr.d.ts +5 -0
  147. package/dist/src/shared/utils/stderr.d.ts.map +1 -0
  148. package/dist/src/shared/utils/stderr.js +18 -0
  149. package/dist/src/shared/utils/stderr.js.map +1 -0
  150. package/dist/src/tui/adapters/webex-adapter.d.ts +14 -0
  151. package/dist/src/tui/adapters/webex-adapter.d.ts.map +1 -0
  152. package/dist/src/tui/adapters/webex-adapter.js +79 -0
  153. package/dist/src/tui/adapters/webex-adapter.js.map +1 -0
  154. package/dist/src/tui/app.d.ts.map +1 -1
  155. package/dist/src/tui/app.js +2 -0
  156. package/dist/src/tui/app.js.map +1 -1
  157. package/docs/content/docs/cli/channeltalk.mdx +7 -7
  158. package/docs/content/docs/cli/discord.mdx +3 -3
  159. package/docs/content/docs/cli/instagram.mdx +28 -6
  160. package/docs/content/docs/cli/meta.json +1 -0
  161. package/docs/content/docs/cli/slack.mdx +2 -2
  162. package/docs/content/docs/cli/teams.mdx +6 -4
  163. package/docs/content/docs/cli/webex.mdx +310 -0
  164. package/docs/content/docs/sdk/meta.json +1 -1
  165. package/docs/content/docs/sdk/webex.mdx +260 -0
  166. package/docs/content/docs/tui.mdx +4 -3
  167. package/docs/src/app/page.tsx +2 -2
  168. package/e2e/README.md +132 -8
  169. package/e2e/channeltalk.e2e.test.ts +2 -7
  170. package/e2e/channeltalkbot.e2e.test.ts +2 -6
  171. package/e2e/config.ts +172 -10
  172. package/e2e/helpers.ts +7 -0
  173. package/e2e/instagram.e2e.test.ts +97 -0
  174. package/e2e/kakaotalk.e2e.test.ts +74 -0
  175. package/e2e/line.e2e.test.ts +92 -0
  176. package/e2e/teams.e2e.test.ts +46 -1
  177. package/e2e/telegram.e2e.test.ts +84 -0
  178. package/e2e/webex.e2e.test.ts +190 -0
  179. package/e2e/whatsapp.e2e.test.ts +90 -0
  180. package/e2e/whatsappbot.e2e.test.ts +78 -0
  181. package/package.json +11 -3
  182. package/skills/agent-channeltalk/SKILL.md +9 -9
  183. package/skills/agent-channeltalk/references/authentication.md +21 -18
  184. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  185. package/skills/agent-discord/SKILL.md +5 -5
  186. package/skills/agent-discord/references/authentication.md +8 -8
  187. package/skills/agent-discordbot/SKILL.md +1 -1
  188. package/skills/agent-instagram/SKILL.md +51 -9
  189. package/skills/agent-instagram/references/authentication.md +35 -3
  190. package/skills/agent-kakaotalk/SKILL.md +1 -1
  191. package/skills/agent-line/SKILL.md +1 -1
  192. package/skills/agent-slack/SKILL.md +5 -5
  193. package/skills/agent-slack/references/authentication.md +8 -8
  194. package/skills/agent-slackbot/SKILL.md +1 -1
  195. package/skills/agent-teams/SKILL.md +6 -6
  196. package/skills/agent-teams/references/authentication.md +8 -8
  197. package/skills/agent-telegram/SKILL.md +1 -1
  198. package/skills/agent-webex/SKILL.md +406 -0
  199. package/skills/agent-webex/references/authentication.md +371 -0
  200. package/skills/agent-webex/references/common-patterns.md +726 -0
  201. package/skills/agent-webex/templates/monitor-space.sh +165 -0
  202. package/skills/agent-webex/templates/post-message.sh +170 -0
  203. package/skills/agent-whatsapp/SKILL.md +1 -1
  204. package/skills/agent-whatsappbot/SKILL.md +1 -1
  205. package/src/cli.ts +4 -0
  206. package/src/platforms/channeltalk/commands/auth.test.ts +5 -5
  207. package/src/platforms/channeltalk/commands/auth.ts +38 -32
  208. package/src/platforms/channeltalk/ensure-auth.test.ts +6 -6
  209. package/src/platforms/channeltalk/ensure-auth.ts +6 -6
  210. package/src/platforms/channeltalk/token-extractor.test.ts +182 -15
  211. package/src/platforms/channeltalk/token-extractor.ts +344 -30
  212. package/src/platforms/discord/commands/auth.test.ts +3 -3
  213. package/src/platforms/discord/commands/auth.ts +58 -54
  214. package/src/platforms/discord/ensure-auth.test.ts +3 -3
  215. package/src/platforms/discord/ensure-auth.ts +3 -3
  216. package/src/platforms/discord/token-extractor.test.ts +199 -27
  217. package/src/platforms/discord/token-extractor.ts +190 -17
  218. package/src/platforms/instagram/client.ts +2 -2
  219. package/src/platforms/instagram/commands/auth.ts +133 -14
  220. package/src/platforms/instagram/ensure-auth.ts +63 -12
  221. package/src/platforms/instagram/index.ts +1 -0
  222. package/src/platforms/instagram/token-extractor.test.ts +424 -0
  223. package/src/platforms/instagram/token-extractor.ts +478 -0
  224. package/src/platforms/kakaotalk/client.ts +3 -1
  225. package/src/platforms/kakaotalk/commands/auth.ts +14 -13
  226. package/src/platforms/kakaotalk/protocol/connection.ts +3 -1
  227. package/src/platforms/line/commands/auth.ts +7 -6
  228. package/src/platforms/slack/cli.test.ts +6 -5
  229. package/src/platforms/slack/commands/auth.test.ts +11 -7
  230. package/src/platforms/slack/commands/auth.ts +11 -10
  231. package/src/platforms/slack/token-extractor.test.ts +98 -1
  232. package/src/platforms/slack/token-extractor.ts +338 -26
  233. package/src/platforms/teams/commands/auth.ts +9 -8
  234. package/src/platforms/teams/ensure-auth.ts +3 -1
  235. package/src/platforms/teams/token-extractor.test.ts +136 -17
  236. package/src/platforms/teams/token-extractor.ts +182 -31
  237. package/src/platforms/telegram/client.test.ts +134 -0
  238. package/src/platforms/telegram/client.ts +27 -6
  239. package/src/platforms/telegram/commands/auth.ts +6 -5
  240. package/src/platforms/webex/app-config.test.ts +98 -0
  241. package/src/platforms/webex/app-config.ts +31 -0
  242. package/src/platforms/webex/cli.test.ts +58 -0
  243. package/src/platforms/webex/cli.ts +39 -0
  244. package/src/platforms/webex/client.test.ts +743 -0
  245. package/src/platforms/webex/client.ts +405 -0
  246. package/src/platforms/webex/commands/auth.test.ts +222 -0
  247. package/src/platforms/webex/commands/auth.ts +243 -0
  248. package/src/platforms/webex/commands/index.ts +5 -0
  249. package/src/platforms/webex/commands/member.test.ts +112 -0
  250. package/src/platforms/webex/commands/member.ts +45 -0
  251. package/src/platforms/webex/commands/message.test.ts +235 -0
  252. package/src/platforms/webex/commands/message.ts +204 -0
  253. package/src/platforms/webex/commands/snapshot.test.ts +105 -0
  254. package/src/platforms/webex/commands/snapshot.ts +91 -0
  255. package/src/platforms/webex/commands/space.test.ts +216 -0
  256. package/src/platforms/webex/commands/space.ts +74 -0
  257. package/src/platforms/webex/credential-manager.test.ts +314 -0
  258. package/src/platforms/webex/credential-manager.ts +197 -0
  259. package/src/platforms/webex/ensure-auth.test.ts +89 -0
  260. package/src/platforms/webex/ensure-auth.ts +38 -0
  261. package/src/platforms/webex/index.test.ts +25 -0
  262. package/src/platforms/webex/index.ts +19 -0
  263. package/src/platforms/webex/token-extractor.test.ts +327 -0
  264. package/src/platforms/webex/token-extractor.ts +393 -0
  265. package/src/platforms/webex/types.test.ts +307 -0
  266. package/src/platforms/webex/types.ts +129 -0
  267. package/src/platforms/whatsapp/client.ts +11 -7
  268. package/src/shared/utils/derived-key-cache.ts +1 -1
  269. package/src/shared/utils/error-handler.ts +4 -2
  270. package/src/shared/utils/stderr.ts +22 -0
  271. package/src/tui/adapters/webex-adapter.ts +103 -0
  272. package/src/tui/app.ts +2 -0
@@ -75,13 +75,16 @@ describe('TokenExtractor', () => {
75
75
  })
76
76
 
77
77
  describe('extract', () => {
78
- test('throws error when Slack directory does not exist', async () => {
79
- // Given: Slack directory does not exist (use unique path to avoid any collision)
78
+ test('returns empty array when Slack directory does not exist (falls back to browser)', async () => {
79
+ // given
80
80
  const nonExistentPath = `/tmp/nonexistent-slack-${Date.now()}-${Math.random()}`
81
81
  extractor = new TokenExtractor('darwin', nonExistentPath)
82
82
 
83
- // When/Then: extract should throw
84
- await expect(extractor.extract()).rejects.toThrow('Slack directory not found')
83
+ // when
84
+ const result = await extractor.extract()
85
+
86
+ // then
87
+ expect(result).toEqual([])
85
88
  })
86
89
 
87
90
  test('returns empty array when no tokens found', async () => {
@@ -430,12 +433,13 @@ describe('Error Handling', () => {
430
433
  })
431
434
 
432
435
  test('handles missing Slack installation gracefully', async () => {
433
- // Given: Slack is not installed (use unique path)
436
+ // given Slack is not installed
434
437
  const nonExistentPath = `/tmp/nonexistent-slack-${Date.now()}-${Math.random()}`
435
438
  const extractor = new TokenExtractor('darwin', nonExistentPath)
436
439
 
437
- // When/Then: Should throw descriptive error
438
- await expect(extractor.extract()).rejects.toThrow('Slack directory not found')
440
+ // when/then falls back to browser profiles, returns empty array
441
+ const result = await extractor.extract()
442
+ expect(result).toEqual([])
439
443
  })
440
444
 
441
445
  test('handles empty Slack directory gracefully', async () => {
@@ -2,6 +2,7 @@ import { Command } from 'commander'
2
2
 
3
3
  import { handleError } from '@/shared/utils/error-handler'
4
4
  import { formatOutput } from '@/shared/utils/output'
5
+ import { debug } from '@/shared/utils/stderr'
5
6
 
6
7
  import { SlackClient, SlackError } from '../client'
7
8
  import { CredentialManager } from '../credential-manager'
@@ -23,7 +24,7 @@ async function extractAction(options: {
23
24
  if (options.unsafelyShowSecrets) {
24
25
  options.debug = true
25
26
  }
26
- const debugLog = options.debug ? (msg: string) => console.error(`[debug] ${msg}`) : undefined
27
+ const debugLog = options.debug ? (msg: string) => debug(`[debug] ${msg}`) : undefined
27
28
  const extractor = new TokenExtractor(undefined, undefined, undefined, debugLog)
28
29
 
29
30
  if (process.platform === 'darwin') {
@@ -44,15 +45,15 @@ async function extractAction(options: {
44
45
  }
45
46
 
46
47
  if (options.debug) {
47
- console.error(`[debug] Slack directory: ${extractor.getSlackDir()}`)
48
+ debug(`[debug] Slack directory: ${extractor.getSlackDir()}`)
48
49
  }
49
50
 
50
51
  const workspaces = await extractor.extract()
51
52
 
52
53
  if (options.debug) {
53
- console.error(`[debug] Found ${workspaces.length} workspace(s)`)
54
+ debug(`[debug] Found ${workspaces.length} workspace(s)`)
54
55
  for (const ws of workspaces) {
55
- console.error(`[debug] - ${formatCredentialDebug(ws, options.unsafelyShowSecrets)}`)
56
+ debug(`[debug] - ${formatCredentialDebug(ws, options.unsafelyShowSecrets)}`)
56
57
  }
57
58
  }
58
59
 
@@ -77,7 +78,7 @@ async function extractAction(options: {
77
78
  const failureReasons: string[] = []
78
79
  for (const ws of workspaces) {
79
80
  if (options.debug) {
80
- console.error(`[debug] Testing credentials for ${ws.workspace_id}...`)
81
+ debug(`[debug] Testing credentials for ${ws.workspace_id}...`)
81
82
  }
82
83
 
83
84
  try {
@@ -89,7 +90,7 @@ async function extractAction(options: {
89
90
  await credManager.setWorkspace(ws)
90
91
 
91
92
  if (options.debug) {
92
- console.error(`[debug] ✓ Valid: ${authInfo.team} (${authInfo.user})`)
93
+ debug(`[debug] ✓ Valid: ${authInfo.team} (${authInfo.user})`)
93
94
  }
94
95
  } catch (error) {
95
96
  const code = error instanceof SlackError ? error.code : undefined
@@ -97,12 +98,12 @@ async function extractAction(options: {
97
98
  failureReasons.push(code)
98
99
  }
99
100
  if (options.debug) {
100
- console.error(`[debug] ✗ Invalid: ${(error as Error).message}`)
101
+ debug(`[debug] ✗ Invalid: ${(error as Error).message}`)
101
102
  }
102
103
 
103
104
  if (options.debug) {
104
105
  const domain = workspaceDomains[ws.workspace_id]
105
- console.error(
106
+ debug(
106
107
  `[debug] Attempting web token refresh for ${ws.workspace_id}${domain ? ` (${domain}.slack.com)` : ''}...`,
107
108
  )
108
109
  }
@@ -114,10 +115,10 @@ async function extractAction(options: {
114
115
  await credManager.setWorkspace(ws)
115
116
 
116
117
  if (options.debug) {
117
- console.error(`[debug] ✓ Web refresh succeeded: ${refreshed.workspace_name}`)
118
+ debug(`[debug] ✓ Web refresh succeeded: ${refreshed.workspace_name}`)
118
119
  }
119
120
  } else if (options.debug) {
120
- console.error('[debug] ✗ Web refresh failed')
121
+ debug('[debug] ✗ Web refresh failed')
121
122
  }
122
123
  }
123
124
  }
@@ -6,7 +6,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
6
6
  import { tmpdir } from 'node:os'
7
7
  import { join } from 'node:path'
8
8
 
9
- import { TokenExtractor } from './token-extractor'
9
+ import { ExtractedWorkspace, TokenExtractor } from './token-extractor'
10
10
 
11
11
  const tempDirs: string[] = []
12
12
 
@@ -790,3 +790,100 @@ describe('TokenExtractor getWorkspaceDomains', () => {
790
790
  expect(domains).toEqual({ T111: 'acme-corp' })
791
791
  })
792
792
  })
793
+
794
+ describe('TokenExtractor browser fallback', () => {
795
+ test('extractFromBrowsers returns empty array when no browser profiles have tokens', async () => {
796
+ const slackDir = mkdtempSync(join(tmpdir(), 'slack-nonexistent-'))
797
+ tempDirs.push(slackDir)
798
+ rmSync(slackDir, { recursive: true, force: true })
799
+
800
+ const extractor = new TokenExtractor('darwin', slackDir)
801
+ const result = await extractor.extractFromBrowsers()
802
+ expect(result).toEqual([])
803
+ })
804
+
805
+ test('extract tries desktop before browser profiles', async () => {
806
+ // given — slackDir with LevelDB token data
807
+ const slackDir = mkdtempSync(join(tmpdir(), 'slack-fallback-desktop-'))
808
+ tempDirs.push(slackDir)
809
+
810
+ const hex64 = 'a'.repeat(64)
811
+ const token = `xoxc-1111111111-2222222222-3333333333-${hex64}`
812
+ const leveldbDir = join(slackDir, 'Local Storage', 'leveldb')
813
+ mkdirSync(leveldbDir, { recursive: true })
814
+ writeFileSync(join(leveldbDir, '000001.log'), `"${token}"T12345678"name":"desktop-workspace"`)
815
+
816
+ const extractFromBrowsersSpy = spyOn(
817
+ TokenExtractor.prototype as any,
818
+ 'extractFromBrowsers',
819
+ ).mockResolvedValue([])
820
+
821
+ // when
822
+ const extractor = new TokenExtractor('darwin', slackDir)
823
+ const result = await extractor.extract()
824
+
825
+ // then — desktop extraction succeeded, browser not called
826
+ expect(result.length).toBeGreaterThan(0)
827
+ expect(extractFromBrowsersSpy).not.toHaveBeenCalled()
828
+
829
+ extractFromBrowsersSpy.mockRestore()
830
+ })
831
+
832
+ test('extract falls back to browser profiles when desktop has no tokens', async () => {
833
+ // given — empty slackDir (no tokens)
834
+ const slackDir = mkdtempSync(join(tmpdir(), 'slack-fallback-browser-'))
835
+ tempDirs.push(slackDir)
836
+
837
+ const hex64 = 'b'.repeat(64)
838
+ const browserToken = `xoxc-9999999999-8888888888-7777777777-${hex64}`
839
+ const browserWorkspace: ExtractedWorkspace = {
840
+ workspace_id: 'T99999999',
841
+ workspace_name: 'browser-workspace',
842
+ token: browserToken,
843
+ cookie: '',
844
+ }
845
+
846
+ const extractFromBrowsersSpy = spyOn(
847
+ TokenExtractor.prototype as any,
848
+ 'extractFromBrowsers',
849
+ ).mockResolvedValue([browserWorkspace])
850
+
851
+ // when
852
+ const extractor = new TokenExtractor('darwin', slackDir)
853
+ const result = await extractor.extract()
854
+
855
+ // then — browser fallback used
856
+ expect(extractFromBrowsersSpy).toHaveBeenCalled()
857
+ expect(result).toEqual([browserWorkspace])
858
+
859
+ extractFromBrowsersSpy.mockRestore()
860
+ })
861
+
862
+ test('extract falls back to browser when slackDir does not exist', async () => {
863
+ // given — non-existent slackDir
864
+ const slackDir = '/nonexistent/slack/dir'
865
+ const hex64 = 'c'.repeat(64)
866
+ const browserToken = `xoxc-1234567890-0987654321-1122334455-${hex64}`
867
+ const browserWorkspace: ExtractedWorkspace = {
868
+ workspace_id: 'T11111111',
869
+ workspace_name: 'browser-ws',
870
+ token: browserToken,
871
+ cookie: 'xoxd-browser-cookie',
872
+ }
873
+
874
+ const extractFromBrowsersSpy = spyOn(
875
+ TokenExtractor.prototype as any,
876
+ 'extractFromBrowsers',
877
+ ).mockResolvedValue([browserWorkspace])
878
+
879
+ // when
880
+ const extractor = new TokenExtractor('darwin', slackDir)
881
+ const result = await extractor.extract()
882
+
883
+ // then
884
+ expect(extractFromBrowsersSpy).toHaveBeenCalled()
885
+ expect(result).toEqual([browserWorkspace])
886
+
887
+ extractFromBrowsersSpy.mockRestore()
888
+ })
889
+ })
@@ -10,6 +10,58 @@ import { ClassicLevel } from 'classic-level'
10
10
  import { DerivedKeyCache } from '@/shared/utils/derived-key-cache'
11
11
  import { lookupLinuxKeyringPassword } from '@/shared/utils/linux-keyring'
12
12
 
13
+ interface BrowserConfig {
14
+ name: string
15
+ darwin: string
16
+ linux: string
17
+ win32: string
18
+ }
19
+
20
+ const BROWSERS: BrowserConfig[] = [
21
+ {
22
+ name: 'Chrome',
23
+ darwin: join('Google', 'Chrome'),
24
+ linux: 'google-chrome',
25
+ win32: join('Google', 'Chrome', 'User Data'),
26
+ },
27
+ {
28
+ name: 'Chrome Canary',
29
+ darwin: join('Google', 'Chrome Canary'),
30
+ linux: 'google-chrome-unstable',
31
+ win32: join('Google', 'Chrome SxS', 'User Data'),
32
+ },
33
+ {
34
+ name: 'Edge',
35
+ darwin: 'Microsoft Edge',
36
+ linux: 'microsoft-edge',
37
+ win32: join('Microsoft', 'Edge', 'User Data'),
38
+ },
39
+ {
40
+ name: 'Arc',
41
+ darwin: join('Arc', 'User Data'),
42
+ linux: '',
43
+ win32: join('Arc', 'User Data'),
44
+ },
45
+ {
46
+ name: 'Brave',
47
+ darwin: join('BraveSoftware', 'Brave-Browser'),
48
+ linux: join('BraveSoftware', 'Brave-Browser'),
49
+ win32: join('BraveSoftware', 'Brave-Browser', 'User Data'),
50
+ },
51
+ {
52
+ name: 'Vivaldi',
53
+ darwin: 'Vivaldi',
54
+ linux: 'vivaldi',
55
+ win32: join('Vivaldi', 'User Data'),
56
+ },
57
+ {
58
+ name: 'Chromium',
59
+ darwin: 'Chromium',
60
+ linux: 'chromium',
61
+ win32: join('Chromium', 'User Data'),
62
+ },
63
+ ]
64
+
13
65
  const require = createRequire(import.meta.url)
14
66
 
15
67
  export interface ExtractedWorkspace {
@@ -177,40 +229,300 @@ export class TokenExtractor {
177
229
  }
178
230
 
179
231
  async extract(): Promise<ExtractedWorkspace[]> {
180
- if (!existsSync(this.slackDir)) {
181
- throw new Error(`Slack directory not found: ${this.slackDir}`)
182
- }
183
-
184
- await this.getDerivedKeyAsync()
185
-
186
- const tokens = await this.extractTokensFromLevelDB()
187
- if (tokens.length === 0) {
188
- return []
189
- }
190
-
191
- const cookie = await this.extractCookieFromSQLite()
232
+ if (existsSync(this.slackDir)) {
233
+ await this.getDerivedKeyAsync()
234
+
235
+ const tokens = await this.extractTokensFromLevelDB()
236
+ if (tokens.length > 0) {
237
+ const cookie = await this.extractCookieFromSQLite()
238
+
239
+ if (!cookie && this.usedCachedKey) {
240
+ await this.clearKeyCache()
241
+ this.cachedKey = this.getDerivedKeyFromKeychain()
242
+ if (this.cachedKey) {
243
+ await this.keyCache.set('slack', this.cachedKey)
244
+ const retryCookie = await this.extractCookieFromSQLite()
245
+ return tokens.map((t) => ({
246
+ workspace_id: t.teamId,
247
+ workspace_name: t.teamName,
248
+ token: t.token,
249
+ cookie: retryCookie,
250
+ }))
251
+ }
252
+ }
192
253
 
193
- if (!cookie && this.usedCachedKey) {
194
- await this.clearKeyCache()
195
- this.cachedKey = this.getDerivedKeyFromKeychain()
196
- if (this.cachedKey) {
197
- await this.keyCache.set('slack', this.cachedKey)
198
- const retryCookie = await this.extractCookieFromSQLite()
199
254
  return tokens.map((t) => ({
200
255
  workspace_id: t.teamId,
201
256
  workspace_name: t.teamName,
202
257
  token: t.token,
203
- cookie: retryCookie,
258
+ cookie: cookie,
204
259
  }))
205
260
  }
206
261
  }
207
262
 
208
- return tokens.map((t) => ({
209
- workspace_id: t.teamId,
210
- workspace_name: t.teamName,
211
- token: t.token,
212
- cookie: cookie,
213
- }))
263
+ return this.extractFromBrowsers()
264
+ }
265
+
266
+ async extractFromBrowsers(): Promise<ExtractedWorkspace[]> {
267
+ const results: ExtractedWorkspace[] = []
268
+ const seenTokens = new Set<string>()
269
+
270
+ for (const browser of BROWSERS) {
271
+ const browserBase = this.getBrowserBasePath(browser)
272
+ if (!browserBase) continue
273
+
274
+ const profileDirs = this.discoverBrowserProfileDirs(browserBase)
275
+ for (const profileDir of profileDirs) {
276
+ const leveldbDir = join(profileDir, 'Local Storage', 'leveldb')
277
+ if (!existsSync(leveldbDir)) continue
278
+
279
+ let tokenInfos: TokenInfo[]
280
+ try {
281
+ tokenInfos = await this.extractFromLevelDB(leveldbDir, 'local-storage')
282
+ } catch {
283
+ continue
284
+ }
285
+
286
+ if (tokenInfos.length === 0) continue
287
+
288
+ const cookie = await this.extractCookieFromBrowserProfile(profileDir, browserBase)
289
+
290
+ for (const t of tokenInfos) {
291
+ if (seenTokens.has(t.token)) continue
292
+ seenTokens.add(t.token)
293
+ results.push({
294
+ workspace_id: t.teamId,
295
+ workspace_name: t.teamName,
296
+ token: t.token,
297
+ cookie,
298
+ })
299
+ }
300
+ }
301
+ }
302
+
303
+ return results
304
+ }
305
+
306
+ private getBrowserBasePath(browser: BrowserConfig): string | null {
307
+ let relative: string
308
+
309
+ switch (this.platform) {
310
+ case 'darwin':
311
+ relative = browser.darwin
312
+ if (!relative) return null
313
+ return join(homedir(), 'Library', 'Application Support', relative)
314
+ case 'linux':
315
+ relative = browser.linux
316
+ if (!relative) return null
317
+ return join(homedir(), '.config', relative)
318
+ case 'win32':
319
+ relative = browser.win32
320
+ if (!relative) return null
321
+ return join(
322
+ process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'),
323
+ relative,
324
+ )
325
+ default:
326
+ return null
327
+ }
328
+ }
329
+
330
+ private discoverBrowserProfileDirs(browserBase: string): string[] {
331
+ const dirs: string[] = []
332
+
333
+ dirs.push(join(browserBase, 'Default'))
334
+
335
+ if (!existsSync(browserBase)) return dirs
336
+
337
+ try {
338
+ const entries = readdirSync(browserBase, { withFileTypes: true })
339
+ for (const entry of entries) {
340
+ if (!entry.isDirectory()) continue
341
+ if (!/^Profile \d+$/i.test(entry.name)) continue
342
+ dirs.push(join(browserBase, entry.name))
343
+ }
344
+ } catch {}
345
+
346
+ return dirs
347
+ }
348
+
349
+ private async extractCookieFromBrowserProfile(profileDir: string, browserBase: string): Promise<string> {
350
+ const cookiePaths = [
351
+ join(profileDir, 'Cookies'),
352
+ join(profileDir, 'Network', 'Cookies'),
353
+ ]
354
+
355
+ for (const cookiePath of cookiePaths) {
356
+ if (!existsSync(cookiePath)) continue
357
+
358
+ const cookie = await this.extractCookieFromBrowserSQLite(cookiePath, browserBase)
359
+ if (cookie) return cookie
360
+ }
361
+
362
+ return ''
363
+ }
364
+
365
+ private async extractCookieFromBrowserSQLite(cookiesPath: string, browserBase: string): Promise<string> {
366
+ const tempDbPath = join(tmpdir(), `slack-browser-cookies-${Date.now()}-${Math.random().toString(36).slice(2)}.db`)
367
+
368
+ try {
369
+ copyFileSync(cookiesPath, tempDbPath)
370
+ } catch {
371
+ return ''
372
+ }
373
+
374
+ try {
375
+ const sql = `SELECT value, encrypted_value
376
+ FROM cookies
377
+ WHERE name = 'd' AND host_key LIKE '%slack.com%'
378
+ ORDER BY last_access_utc DESC
379
+ LIMIT 1`
380
+
381
+ type CookieRow = {
382
+ value?: string
383
+ encrypted_value?: Uint8Array | Buffer
384
+ } | null
385
+
386
+ let row: CookieRow
387
+ if (typeof globalThis.Bun !== 'undefined') {
388
+ const { Database } = require('bun:sqlite')
389
+ const db = new Database(tempDbPath, { readonly: true })
390
+ row = db.query(sql).get() as CookieRow
391
+ db.close()
392
+ } else {
393
+ const Database = require('better-sqlite3')
394
+ const db = new Database(tempDbPath, { readonly: true })
395
+ row = db.prepare(sql).get() as CookieRow
396
+ db.close()
397
+ }
398
+
399
+ if (!row) return ''
400
+
401
+ if (row.value?.startsWith('xoxd-')) return row.value
402
+
403
+ if (row.encrypted_value && row.encrypted_value.length > 0) {
404
+ return this.decryptBrowserCookieForSlack(Buffer.from(row.encrypted_value), browserBase) ?? ''
405
+ }
406
+
407
+ return ''
408
+ } catch {
409
+ return ''
410
+ } finally {
411
+ try {
412
+ rmSync(tempDbPath, { force: true })
413
+ } catch {}
414
+ }
415
+ }
416
+
417
+ private decryptBrowserCookieForSlack(encrypted: Buffer, browserBase: string): string | null {
418
+ const str = encrypted.toString('utf8')
419
+ if (str.startsWith('xoxd-')) return str
420
+
421
+ const prefix = encrypted.length > 3 ? encrypted.subarray(0, 3).toString() : ''
422
+
423
+ if (prefix === 'v10') {
424
+ if (this.platform === 'win32') {
425
+ return this.decryptBrowserV10CookieWindows(encrypted, browserBase)
426
+ }
427
+ if (this.platform === 'linux') {
428
+ return this.decryptV10CookieLinux(encrypted)
429
+ }
430
+ return this.decryptBrowserV10CookieMac(encrypted)
431
+ }
432
+
433
+ if (prefix === 'v11') {
434
+ if (this.platform === 'linux') {
435
+ return this.decryptV11CookieLinux(encrypted)
436
+ }
437
+ if (this.platform === 'darwin') {
438
+ return this.decryptBrowserV10CookieMac(encrypted)
439
+ }
440
+ }
441
+
442
+ if (this.platform === 'win32' && encrypted.length > 0) {
443
+ const decrypted = this.decryptDPAPI(encrypted)
444
+ if (decrypted) {
445
+ const text = decrypted.toString('utf8')
446
+ const match = text.match(/xoxd-[A-Za-z0-9%]+/)
447
+ return match ? match[0] : null
448
+ }
449
+ }
450
+
451
+ return null
452
+ }
453
+
454
+ private decryptBrowserV10CookieMac(encrypted: Buffer): string | null {
455
+ const keychainVariants = [
456
+ { service: 'Chrome Safe Storage', account: 'Chrome' },
457
+ { service: 'Chrome Canary Safe Storage', account: 'Chrome Canary' },
458
+ { service: 'Microsoft Edge Safe Storage', account: 'Microsoft Edge' },
459
+ { service: 'Arc Safe Storage', account: 'Arc' },
460
+ { service: 'Brave Safe Storage', account: 'Brave' },
461
+ { service: 'Vivaldi Safe Storage', account: 'Vivaldi' },
462
+ { service: 'Chromium Safe Storage', account: 'Chromium' },
463
+ ]
464
+
465
+ for (const variant of keychainVariants) {
466
+ try {
467
+ const password = execSync(
468
+ `security find-generic-password -s "${variant.service}" -a "${variant.account}" -w 2>/dev/null`,
469
+ { encoding: 'utf8' },
470
+ ).trim()
471
+
472
+ const key = pbkdf2Sync(password, 'saltysalt', 1003, 16, 'sha1')
473
+ const result = this.decryptLinuxCookieWithKey(encrypted, key)
474
+ if (result) return result
475
+ } catch {}
476
+ }
477
+
478
+ return null
479
+ }
480
+
481
+ private decryptBrowserV10CookieWindows(encrypted: Buffer, browserBase: string): string | null {
482
+ try {
483
+ const masterKey = this.getBrowserWindowsMasterKey(browserBase)
484
+ if (!masterKey) {
485
+ const decrypted = this.decryptDPAPI(encrypted.subarray(3))
486
+ if (!decrypted) return null
487
+ const text = decrypted.toString('utf8')
488
+ const match = text.match(/xoxd-[A-Za-z0-9%]+/)
489
+ return match ? match[0] : null
490
+ }
491
+
492
+ const nonce = encrypted.subarray(3, 3 + 12)
493
+ const ciphertextWithTag = encrypted.subarray(3 + 12)
494
+ const tag = ciphertextWithTag.subarray(ciphertextWithTag.length - 16)
495
+ const ciphertext = ciphertextWithTag.subarray(0, ciphertextWithTag.length - 16)
496
+
497
+ const decipher = createDecipheriv('aes-256-gcm', masterKey, nonce)
498
+ decipher.setAuthTag(tag)
499
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8')
500
+
501
+ const match = decrypted.match(/xoxd-[A-Za-z0-9%]+/)
502
+ return match ? match[0] : null
503
+ } catch {
504
+ return null
505
+ }
506
+ }
507
+
508
+ private getBrowserWindowsMasterKey(browserBase: string): Buffer | null {
509
+ try {
510
+ const localStatePath = join(browserBase, 'Local State')
511
+ if (!existsSync(localStatePath)) return null
512
+
513
+ const localState = JSON.parse(readFileSync(localStatePath, 'utf8')) as {
514
+ os_crypt?: { encrypted_key?: string }
515
+ }
516
+ const encryptedKeyB64 = localState?.os_crypt?.encrypted_key
517
+ if (!encryptedKeyB64) return null
518
+
519
+ const encryptedKey = Buffer.from(encryptedKeyB64, 'base64')
520
+ if (encryptedKey.subarray(0, 5).toString() !== 'DPAPI') return null
521
+
522
+ return this.decryptDPAPI(encryptedKey.subarray(5))
523
+ } catch {
524
+ return null
525
+ }
214
526
  }
215
527
 
216
528
  private async extractTokensFromLevelDB(): Promise<TokenInfo[]> {
@@ -845,7 +1157,7 @@ export class TokenExtractor {
845
1157
  }
846
1158
 
847
1159
  private decryptV11CookieLinux(encrypted: Buffer): string | null {
848
- const appNames = ['Slack', 'slack']
1160
+ const appNames = ['Slack', 'slack', 'Chrome', 'chrome', 'Chromium', 'chromium', 'Microsoft Edge', 'Brave', 'Vivaldi']
849
1161
  for (const appName of appNames) {
850
1162
  try {
851
1163
  const keyringPassword = this.getLinuxKeyringPassword(appName)