agent-messenger 2.1.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 (207) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.env.template +35 -17
  3. package/README.md +7 -7
  4. package/bun.lock +6 -6
  5. package/dist/package.json +2 -2
  6. package/dist/src/platforms/channeltalk/commands/auth.d.ts.map +1 -1
  7. package/dist/src/platforms/channeltalk/commands/auth.js +35 -28
  8. package/dist/src/platforms/channeltalk/commands/auth.js.map +1 -1
  9. package/dist/src/platforms/channeltalk/ensure-auth.js +6 -6
  10. package/dist/src/platforms/channeltalk/ensure-auth.js.map +1 -1
  11. package/dist/src/platforms/channeltalk/token-extractor.d.ts +23 -1
  12. package/dist/src/platforms/channeltalk/token-extractor.d.ts.map +1 -1
  13. package/dist/src/platforms/channeltalk/token-extractor.js +299 -29
  14. package/dist/src/platforms/channeltalk/token-extractor.js.map +1 -1
  15. package/dist/src/platforms/discord/commands/auth.d.ts.map +1 -1
  16. package/dist/src/platforms/discord/commands/auth.js +57 -49
  17. package/dist/src/platforms/discord/commands/auth.js.map +1 -1
  18. package/dist/src/platforms/discord/ensure-auth.js +3 -3
  19. package/dist/src/platforms/discord/ensure-auth.js.map +1 -1
  20. package/dist/src/platforms/discord/token-extractor.d.ts +6 -1
  21. package/dist/src/platforms/discord/token-extractor.d.ts.map +1 -1
  22. package/dist/src/platforms/discord/token-extractor.js +167 -14
  23. package/dist/src/platforms/discord/token-extractor.js.map +1 -1
  24. package/dist/src/platforms/instagram/client.d.ts +2 -0
  25. package/dist/src/platforms/instagram/client.d.ts.map +1 -1
  26. package/dist/src/platforms/instagram/client.js +2 -2
  27. package/dist/src/platforms/instagram/client.js.map +1 -1
  28. package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
  29. package/dist/src/platforms/instagram/commands/auth.js +107 -14
  30. package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
  31. package/dist/src/platforms/instagram/ensure-auth.d.ts.map +1 -1
  32. package/dist/src/platforms/instagram/ensure-auth.js +57 -11
  33. package/dist/src/platforms/instagram/ensure-auth.js.map +1 -1
  34. package/dist/src/platforms/instagram/index.d.ts +1 -0
  35. package/dist/src/platforms/instagram/index.d.ts.map +1 -1
  36. package/dist/src/platforms/instagram/index.js +1 -0
  37. package/dist/src/platforms/instagram/index.js.map +1 -1
  38. package/dist/src/platforms/instagram/token-extractor.d.ts +44 -0
  39. package/dist/src/platforms/instagram/token-extractor.d.ts.map +1 -0
  40. package/dist/src/platforms/instagram/token-extractor.js +407 -0
  41. package/dist/src/platforms/instagram/token-extractor.js.map +1 -0
  42. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  43. package/dist/src/platforms/kakaotalk/client.js +2 -1
  44. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  45. package/dist/src/platforms/kakaotalk/commands/auth.d.ts.map +1 -1
  46. package/dist/src/platforms/kakaotalk/commands/auth.js +14 -13
  47. package/dist/src/platforms/kakaotalk/commands/auth.js.map +1 -1
  48. package/dist/src/platforms/kakaotalk/protocol/connection.d.ts.map +1 -1
  49. package/dist/src/platforms/kakaotalk/protocol/connection.js +2 -1
  50. package/dist/src/platforms/kakaotalk/protocol/connection.js.map +1 -1
  51. package/dist/src/platforms/line/commands/auth.d.ts.map +1 -1
  52. package/dist/src/platforms/line/commands/auth.js +6 -5
  53. package/dist/src/platforms/line/commands/auth.js.map +1 -1
  54. package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
  55. package/dist/src/platforms/slack/commands/auth.js +11 -10
  56. package/dist/src/platforms/slack/commands/auth.js.map +1 -1
  57. package/dist/src/platforms/slack/token-extractor.d.ts +9 -0
  58. package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
  59. package/dist/src/platforms/slack/token-extractor.js +300 -23
  60. package/dist/src/platforms/slack/token-extractor.js.map +1 -1
  61. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
  62. package/dist/src/platforms/teams/commands/auth.js +9 -8
  63. package/dist/src/platforms/teams/commands/auth.js.map +1 -1
  64. package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
  65. package/dist/src/platforms/teams/ensure-auth.js +2 -1
  66. package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
  67. package/dist/src/platforms/teams/token-extractor.d.ts +5 -0
  68. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  69. package/dist/src/platforms/teams/token-extractor.js +161 -29
  70. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  71. package/dist/src/platforms/telegram/client.d.ts.map +1 -1
  72. package/dist/src/platforms/telegram/client.js +25 -7
  73. package/dist/src/platforms/telegram/client.js.map +1 -1
  74. package/dist/src/platforms/telegram/commands/auth.d.ts.map +1 -1
  75. package/dist/src/platforms/telegram/commands/auth.js +6 -5
  76. package/dist/src/platforms/telegram/commands/auth.js.map +1 -1
  77. package/dist/src/platforms/webex/client.d.ts +10 -0
  78. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  79. package/dist/src/platforms/webex/client.js +124 -0
  80. package/dist/src/platforms/webex/client.js.map +1 -1
  81. package/dist/src/platforms/webex/commands/auth.d.ts +4 -0
  82. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
  83. package/dist/src/platforms/webex/commands/auth.js +46 -4
  84. package/dist/src/platforms/webex/commands/auth.js.map +1 -1
  85. package/dist/src/platforms/webex/credential-manager.js +1 -1
  86. package/dist/src/platforms/webex/credential-manager.js.map +1 -1
  87. package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -1
  88. package/dist/src/platforms/webex/ensure-auth.js +21 -5
  89. package/dist/src/platforms/webex/ensure-auth.js.map +1 -1
  90. package/dist/src/platforms/webex/index.d.ts +2 -0
  91. package/dist/src/platforms/webex/index.d.ts.map +1 -1
  92. package/dist/src/platforms/webex/index.js +1 -0
  93. package/dist/src/platforms/webex/index.js.map +1 -1
  94. package/dist/src/platforms/webex/token-extractor.d.ts +28 -0
  95. package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -0
  96. package/dist/src/platforms/webex/token-extractor.js +344 -0
  97. package/dist/src/platforms/webex/token-extractor.js.map +1 -0
  98. package/dist/src/platforms/webex/types.d.ts +4 -1
  99. package/dist/src/platforms/webex/types.d.ts.map +1 -1
  100. package/dist/src/platforms/webex/types.js +2 -1
  101. package/dist/src/platforms/webex/types.js.map +1 -1
  102. package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
  103. package/dist/src/platforms/whatsapp/client.js +6 -2
  104. package/dist/src/platforms/whatsapp/client.js.map +1 -1
  105. package/dist/src/shared/utils/derived-key-cache.d.ts +1 -1
  106. package/dist/src/shared/utils/derived-key-cache.d.ts.map +1 -1
  107. package/dist/src/shared/utils/error-handler.d.ts +1 -1
  108. package/dist/src/shared/utils/error-handler.d.ts.map +1 -1
  109. package/dist/src/shared/utils/error-handler.js +3 -2
  110. package/dist/src/shared/utils/error-handler.js.map +1 -1
  111. package/dist/src/shared/utils/stderr.d.ts +5 -0
  112. package/dist/src/shared/utils/stderr.d.ts.map +1 -0
  113. package/dist/src/shared/utils/stderr.js +18 -0
  114. package/dist/src/shared/utils/stderr.js.map +1 -0
  115. package/docs/content/docs/cli/channeltalk.mdx +7 -7
  116. package/docs/content/docs/cli/discord.mdx +3 -3
  117. package/docs/content/docs/cli/instagram.mdx +28 -6
  118. package/docs/content/docs/cli/slack.mdx +2 -2
  119. package/docs/content/docs/cli/teams.mdx +6 -4
  120. package/docs/content/docs/cli/webex.mdx +30 -11
  121. package/e2e/README.md +132 -8
  122. package/e2e/channeltalk.e2e.test.ts +2 -7
  123. package/e2e/channeltalkbot.e2e.test.ts +2 -6
  124. package/e2e/config.ts +172 -10
  125. package/e2e/helpers.ts +7 -0
  126. package/e2e/instagram.e2e.test.ts +97 -0
  127. package/e2e/kakaotalk.e2e.test.ts +74 -0
  128. package/e2e/line.e2e.test.ts +92 -0
  129. package/e2e/teams.e2e.test.ts +46 -1
  130. package/e2e/telegram.e2e.test.ts +84 -0
  131. package/e2e/webex.e2e.test.ts +190 -0
  132. package/e2e/whatsapp.e2e.test.ts +90 -0
  133. package/e2e/whatsappbot.e2e.test.ts +78 -0
  134. package/package.json +2 -2
  135. package/skills/agent-channeltalk/SKILL.md +9 -9
  136. package/skills/agent-channeltalk/references/authentication.md +21 -18
  137. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  138. package/skills/agent-discord/SKILL.md +5 -5
  139. package/skills/agent-discord/references/authentication.md +8 -8
  140. package/skills/agent-discordbot/SKILL.md +1 -1
  141. package/skills/agent-instagram/SKILL.md +51 -9
  142. package/skills/agent-instagram/references/authentication.md +35 -3
  143. package/skills/agent-kakaotalk/SKILL.md +1 -1
  144. package/skills/agent-line/SKILL.md +1 -1
  145. package/skills/agent-slack/SKILL.md +5 -5
  146. package/skills/agent-slack/references/authentication.md +8 -8
  147. package/skills/agent-slackbot/SKILL.md +1 -1
  148. package/skills/agent-teams/SKILL.md +6 -6
  149. package/skills/agent-teams/references/authentication.md +8 -8
  150. package/skills/agent-telegram/SKILL.md +1 -1
  151. package/skills/agent-webex/SKILL.md +35 -15
  152. package/skills/agent-webex/references/authentication.md +62 -9
  153. package/skills/agent-webex/references/common-patterns.md +6 -3
  154. package/skills/agent-whatsapp/SKILL.md +1 -1
  155. package/skills/agent-whatsappbot/SKILL.md +1 -1
  156. package/src/platforms/channeltalk/commands/auth.test.ts +5 -5
  157. package/src/platforms/channeltalk/commands/auth.ts +38 -32
  158. package/src/platforms/channeltalk/ensure-auth.test.ts +6 -6
  159. package/src/platforms/channeltalk/ensure-auth.ts +6 -6
  160. package/src/platforms/channeltalk/token-extractor.test.ts +182 -15
  161. package/src/platforms/channeltalk/token-extractor.ts +344 -30
  162. package/src/platforms/discord/commands/auth.test.ts +3 -3
  163. package/src/platforms/discord/commands/auth.ts +58 -54
  164. package/src/platforms/discord/ensure-auth.test.ts +3 -3
  165. package/src/platforms/discord/ensure-auth.ts +3 -3
  166. package/src/platforms/discord/token-extractor.test.ts +199 -27
  167. package/src/platforms/discord/token-extractor.ts +190 -17
  168. package/src/platforms/instagram/client.ts +2 -2
  169. package/src/platforms/instagram/commands/auth.ts +133 -14
  170. package/src/platforms/instagram/ensure-auth.ts +63 -12
  171. package/src/platforms/instagram/index.ts +1 -0
  172. package/src/platforms/instagram/token-extractor.test.ts +424 -0
  173. package/src/platforms/instagram/token-extractor.ts +478 -0
  174. package/src/platforms/kakaotalk/client.ts +3 -1
  175. package/src/platforms/kakaotalk/commands/auth.ts +14 -13
  176. package/src/platforms/kakaotalk/protocol/connection.ts +3 -1
  177. package/src/platforms/line/commands/auth.ts +7 -6
  178. package/src/platforms/slack/cli.test.ts +6 -5
  179. package/src/platforms/slack/commands/auth.test.ts +11 -7
  180. package/src/platforms/slack/commands/auth.ts +11 -10
  181. package/src/platforms/slack/token-extractor.test.ts +98 -1
  182. package/src/platforms/slack/token-extractor.ts +338 -26
  183. package/src/platforms/teams/commands/auth.ts +9 -8
  184. package/src/platforms/teams/ensure-auth.ts +3 -1
  185. package/src/platforms/teams/token-extractor.test.ts +136 -17
  186. package/src/platforms/teams/token-extractor.ts +182 -31
  187. package/src/platforms/telegram/client.test.ts +134 -0
  188. package/src/platforms/telegram/client.ts +27 -6
  189. package/src/platforms/telegram/commands/auth.ts +6 -5
  190. package/src/platforms/webex/client.test.ts +314 -0
  191. package/src/platforms/webex/client.ts +158 -0
  192. package/src/platforms/webex/commands/auth.ts +67 -4
  193. package/src/platforms/webex/commands/member.test.ts +10 -1
  194. package/src/platforms/webex/commands/message.test.ts +9 -5
  195. package/src/platforms/webex/commands/snapshot.test.ts +13 -4
  196. package/src/platforms/webex/commands/space.test.ts +12 -2
  197. package/src/platforms/webex/credential-manager.ts +1 -1
  198. package/src/platforms/webex/ensure-auth.test.ts +4 -0
  199. package/src/platforms/webex/ensure-auth.ts +23 -4
  200. package/src/platforms/webex/index.ts +2 -0
  201. package/src/platforms/webex/token-extractor.test.ts +327 -0
  202. package/src/platforms/webex/token-extractor.ts +393 -0
  203. package/src/platforms/webex/types.ts +4 -2
  204. package/src/platforms/whatsapp/client.ts +11 -7
  205. package/src/shared/utils/derived-key-cache.ts +1 -1
  206. package/src/shared/utils/error-handler.ts +4 -2
  207. package/src/shared/utils/stderr.ts +22 -0
@@ -34,6 +34,13 @@ interface CDPMessage {
34
34
  error?: { code: number; message: string }
35
35
  }
36
36
 
37
+ interface BrowserConfig {
38
+ name: string
39
+ darwin: string
40
+ linux: string
41
+ win32: string
42
+ }
43
+
37
44
  const TOKEN_REGEX = /[\w-]{24,}\.[\w-]{6}\.[\w-]{25,110}/
38
45
  const MFA_TOKEN_REGEX = /mfa\.[\w-]{84}/
39
46
  const ENCRYPTED_PREFIX = 'dQw4w9WgXcQ:'
@@ -55,6 +62,51 @@ const DISCORD_APP_PATHS: Record<DiscordVariant, { darwin: string }> = {
55
62
  ptb: { darwin: '/Applications/Discord PTB.app/Contents/MacOS/Discord PTB' },
56
63
  }
57
64
 
65
+ const BROWSERS: BrowserConfig[] = [
66
+ {
67
+ name: 'Chrome',
68
+ darwin: join('Google', 'Chrome'),
69
+ linux: 'google-chrome',
70
+ win32: join('Google', 'Chrome', 'User Data'),
71
+ },
72
+ {
73
+ name: 'Chrome Canary',
74
+ darwin: join('Google', 'Chrome Canary'),
75
+ linux: 'google-chrome-unstable',
76
+ win32: join('Google', 'Chrome SxS', 'User Data'),
77
+ },
78
+ {
79
+ name: 'Edge',
80
+ darwin: 'Microsoft Edge',
81
+ linux: 'microsoft-edge',
82
+ win32: join('Microsoft', 'Edge', 'User Data'),
83
+ },
84
+ {
85
+ name: 'Arc',
86
+ darwin: join('Arc', 'User Data'),
87
+ linux: '',
88
+ win32: join('Arc', 'User Data'),
89
+ },
90
+ {
91
+ name: 'Brave',
92
+ darwin: join('BraveSoftware', 'Brave-Browser'),
93
+ linux: join('BraveSoftware', 'Brave-Browser'),
94
+ win32: join('BraveSoftware', 'Brave-Browser', 'User Data'),
95
+ },
96
+ {
97
+ name: 'Vivaldi',
98
+ darwin: 'Vivaldi',
99
+ linux: 'vivaldi',
100
+ win32: join('Vivaldi', 'User Data'),
101
+ },
102
+ {
103
+ name: 'Chromium',
104
+ darwin: 'Chromium',
105
+ linux: 'chromium',
106
+ win32: join('Chromium', 'User Data'),
107
+ },
108
+ ]
109
+
58
110
  export class DiscordTokenExtractor {
59
111
  private platform: NodeJS.Platform
60
112
  private startupWait: number
@@ -92,6 +144,75 @@ export class DiscordTokenExtractor {
92
144
  }
93
145
  }
94
146
 
147
+ getBrowserLevelDBDirs(): string[] {
148
+ const dirs: string[] = []
149
+
150
+ for (const browser of BROWSERS) {
151
+ const browserBase = this.getBrowserBasePath(browser)
152
+ if (!browserBase) continue
153
+
154
+ const profileDirs = this.discoverProfileDirs(browserBase)
155
+ for (const profileDir of profileDirs) {
156
+ dirs.push(join(profileDir, 'Local Storage', 'leveldb'))
157
+ }
158
+ }
159
+
160
+ return dirs
161
+ }
162
+
163
+ private getBrowserBasePath(browser: BrowserConfig): string | null {
164
+ let relative: string
165
+
166
+ switch (this.platform) {
167
+ case 'darwin':
168
+ relative = browser.darwin
169
+ if (!relative) return null
170
+ return join(homedir(), 'Library', 'Application Support', relative)
171
+ case 'linux':
172
+ relative = browser.linux
173
+ if (!relative) return null
174
+ return join(homedir(), '.config', relative)
175
+ case 'win32':
176
+ relative = browser.win32
177
+ if (!relative) return null
178
+ return join(
179
+ process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'),
180
+ relative,
181
+ )
182
+ default:
183
+ return null
184
+ }
185
+ }
186
+
187
+ private discoverProfileDirs(browserBase: string): string[] {
188
+ const dirs: string[] = []
189
+
190
+ dirs.push(join(browserBase, 'Default'))
191
+
192
+ if (!existsSync(browserBase)) return dirs
193
+
194
+ try {
195
+ const entries = readdirSync(browserBase, { withFileTypes: true })
196
+ for (const entry of entries) {
197
+ if (!entry.isDirectory()) continue
198
+ if (!/^Profile \d+$/i.test(entry.name)) continue
199
+ dirs.push(join(browserBase, entry.name))
200
+ }
201
+ } catch {}
202
+
203
+ return dirs
204
+ }
205
+
206
+ private findBrowserBaseDirForPath(path: string): string | null {
207
+ const parts = path.split(/[/\\]/)
208
+ for (let levels = 2; levels <= 5; levels++) {
209
+ if (parts.length < levels) break
210
+ const base = parts.slice(0, parts.length - levels).join('/')
211
+ if (existsSync(join(base, 'Local State'))) return base
212
+ }
213
+ return null
214
+ }
215
+
95
216
  getKeychainVariants(): KeychainVariant[] {
96
217
  return [
97
218
  // Modern Discord (lowercase)
@@ -102,6 +223,14 @@ export class DiscordTokenExtractor {
102
223
  { service: 'Discord Safe Storage', account: 'Discord' },
103
224
  { service: 'Discord Canary Safe Storage', account: 'Discord Canary' },
104
225
  { service: 'Discord PTB Safe Storage', account: 'Discord PTB' },
226
+ // Browser keychain entries for fallback
227
+ { service: 'Chrome Safe Storage', account: 'Chrome' },
228
+ { service: 'Chrome Canary Safe Storage', account: 'Chrome Canary' },
229
+ { service: 'Microsoft Edge Safe Storage', account: 'Microsoft Edge' },
230
+ { service: 'Arc Safe Storage', account: 'Arc' },
231
+ { service: 'Brave Safe Storage', account: 'Brave' },
232
+ { service: 'Vivaldi Safe Storage', account: 'Vivaldi' },
233
+ { service: 'Chromium Safe Storage', account: 'Chromium' },
105
234
  ]
106
235
  }
107
236
 
@@ -121,22 +250,34 @@ export class DiscordTokenExtractor {
121
250
  return token.startsWith(ENCRYPTED_PREFIX)
122
251
  }
123
252
 
124
- async extract(): Promise<ExtractedDiscordToken | null> {
253
+ async extract(): Promise<ExtractedDiscordToken[]> {
125
254
  await this.loadCachedKey()
126
255
 
127
- const levelDbToken = await this.extractFromLevelDB()
128
- if (levelDbToken) {
129
- return levelDbToken
256
+ const results: ExtractedDiscordToken[] = []
257
+ const seenTokens = new Set<string>()
258
+
259
+ for (const t of await this.extractFromLevelDB()) {
260
+ if (!seenTokens.has(t.token)) {
261
+ seenTokens.add(t.token)
262
+ results.push(t)
263
+ }
130
264
  }
131
265
 
132
- if (this.platform === 'darwin') {
266
+ for (const t of await this.extractFromBrowserLevelDB()) {
267
+ if (!seenTokens.has(t.token)) {
268
+ seenTokens.add(t.token)
269
+ results.push(t)
270
+ }
271
+ }
272
+
273
+ if (results.length === 0 && this.platform === 'darwin') {
133
274
  const cdpToken = await this.tryExtractViaCDP()
134
- if (cdpToken) {
135
- return { token: cdpToken }
275
+ if (cdpToken && !seenTokens.has(cdpToken)) {
276
+ results.push({ token: cdpToken })
136
277
  }
137
278
  }
138
279
 
139
- return null
280
+ return results
140
281
  }
141
282
 
142
283
  private async loadCachedKey(): Promise<void> {
@@ -175,19 +316,45 @@ export class DiscordTokenExtractor {
175
316
  return null
176
317
  }
177
318
 
178
- private async extractFromLevelDB(): Promise<ExtractedDiscordToken | null> {
319
+ private async extractFromLevelDB(): Promise<ExtractedDiscordToken[]> {
179
320
  const dirs = this.getDiscordDirs()
321
+ const results: ExtractedDiscordToken[] = []
322
+ const seenTokens = new Set<string>()
180
323
 
181
324
  for (const dir of dirs) {
182
325
  if (!existsSync(dir)) continue
183
326
 
184
327
  const token = await this.extractFromDir(dir)
185
- if (token) {
186
- return { token }
328
+ if (token && !seenTokens.has(token)) {
329
+ seenTokens.add(token)
330
+ results.push({ token })
187
331
  }
188
332
  }
189
333
 
190
- return null
334
+ return results
335
+ }
336
+
337
+ private async extractFromBrowserLevelDB(): Promise<ExtractedDiscordToken[]> {
338
+ const leveldbDirs = this.getBrowserLevelDBDirs()
339
+ const results: ExtractedDiscordToken[] = []
340
+ const seenTokens = new Set<string>()
341
+
342
+ for (const leveldbDir of leveldbDirs) {
343
+ if (!existsSync(leveldbDir)) continue
344
+
345
+ // The browser base dir contains the Local State file needed for Windows decryption
346
+ const browserBaseDir = this.findBrowserBaseDirForPath(leveldbDir)
347
+ const tokens = this.extractTokensFromLDBFiles(leveldbDir, browserBaseDir ?? leveldbDir)
348
+
349
+ for (const token of tokens) {
350
+ if (!seenTokens.has(token)) {
351
+ seenTokens.add(token)
352
+ results.push({ token })
353
+ }
354
+ }
355
+ }
356
+
357
+ return results
191
358
  }
192
359
 
193
360
  private async extractFromDir(discordDir: string): Promise<string | null> {
@@ -196,7 +363,7 @@ export class DiscordTokenExtractor {
196
363
  if (!existsSync(levelDbPath)) return null
197
364
 
198
365
  const tokens = this.extractTokensFromLDBFiles(levelDbPath, discordDir)
199
- return tokens.length > 0 ? tokens[0] : null
366
+ return tokens.length > 0 ? tokens[0]! : null
200
367
  }
201
368
 
202
369
  private extractTokensFromLDBFiles(dbPath: string, discordDir: string): string[] {
@@ -307,11 +474,12 @@ export class DiscordTokenExtractor {
307
474
  private decryptMacToken(encryptedData: Buffer, discordDir: string): string | null {
308
475
  if (this.cachedKey) {
309
476
  const decrypted = this.decryptAESCBC(encryptedData, this.cachedKey)
310
- if (decrypted) return decrypted
477
+ if (decrypted && this.isValidToken(decrypted)) return decrypted
311
478
  }
312
479
 
313
480
  const variant = this.getVariantFromPath(discordDir)
314
- const keychainVariants = this.getKeychainVariants().filter((v) => {
481
+ // Try Discord-specific variants first, then fall back to all browser variants
482
+ const discordVariants = this.getKeychainVariants().filter((v) => {
315
483
  const lowerAccount = v.account.toLowerCase()
316
484
  if (variant === 'stable') return lowerAccount === 'discord' || lowerAccount === 'discord key'
317
485
  if (variant === 'canary') return lowerAccount === 'discord canary' || lowerAccount === 'discordcanary key'
@@ -319,7 +487,12 @@ export class DiscordTokenExtractor {
319
487
  return false
320
488
  })
321
489
 
322
- for (const keychainVariant of keychainVariants) {
490
+ const browserVariants = this.getKeychainVariants().filter((v) => {
491
+ const lowerService = v.service.toLowerCase()
492
+ return !lowerService.includes('discord')
493
+ })
494
+
495
+ for (const keychainVariant of [...discordVariants, ...browserVariants]) {
323
496
  try {
324
497
  const password = execSync(
325
498
  `security find-generic-password -s "${keychainVariant.service}" -a "${keychainVariant.account}" -w 2>/dev/null`,
@@ -328,7 +501,7 @@ export class DiscordTokenExtractor {
328
501
 
329
502
  const key = pbkdf2Sync(password, 'saltysalt', 1003, 16, 'sha1')
330
503
  const decrypted = this.decryptAESCBC(encryptedData, key)
331
- if (decrypted) {
504
+ if (decrypted && this.isValidToken(decrypted)) {
332
505
  this.cachedKey = key
333
506
  this.keyCache.set('discord', key).catch(() => {})
334
507
  return decrypted
@@ -24,7 +24,7 @@ const DEVICE_MANUFACTURER = 'samsung'
24
24
  const DEVICE_MODEL = 'SM-S911B'
25
25
  const DEVICE_CHIPSET = 'qcom'
26
26
 
27
- function generateDeviceString(): string {
27
+ export function generateDeviceString(): string {
28
28
  return `${ANDROID_VERSION}/${ANDROID_RELEASE}; ${DEVICE_DPI}; ${DEVICE_RESOLUTION}; ${DEVICE_MANUFACTURER}; ${DEVICE_MODEL}; ${DEVICE_MODEL}; ${DEVICE_CHIPSET}; en_US; ${IG_VERSION_CODE}`
29
29
  }
30
30
 
@@ -32,7 +32,7 @@ function buildUserAgent(deviceString: string): string {
32
32
  return `Instagram ${IG_VERSION} Android (${deviceString})`
33
33
  }
34
34
 
35
- function generateAndroidDeviceId(): string {
35
+ export function generateAndroidDeviceId(): string {
36
36
  return `android-${randomBytes(8).toString('hex')}`
37
37
  }
38
38
 
@@ -1,10 +1,14 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { mkdir, writeFile } from 'node:fs/promises'
1
3
  import { Writable } from 'node:stream'
2
4
 
3
5
  import { Command } from 'commander'
4
6
  import { handleError } from '@/shared/utils/error-handler'
5
7
  import { formatOutput } from '@/shared/utils/output'
6
- import { InstagramClient } from '../client'
8
+ import { info, warn, error as stderrError, debug } from '@/shared/utils/stderr'
9
+ import { InstagramClient, generateAndroidDeviceId, generateDeviceString } from '../client'
7
10
  import { InstagramCredentialManager } from '../credential-manager'
11
+ import { InstagramTokenExtractor } from '../token-extractor'
8
12
  import { createAccountId } from '../types'
9
13
 
10
14
  function isInteractive(): boolean {
@@ -88,7 +92,7 @@ async function loginAction(options: LoginOptions): Promise<void> {
88
92
  process.exit(1)
89
93
  }
90
94
  username = await promptText('Instagram username')
91
- if (!username) { console.error('Username is required.'); process.exit(1) }
95
+ if (!username) { stderrError('Username is required.'); process.exit(1) }
92
96
  }
93
97
 
94
98
  if (!password) {
@@ -99,7 +103,7 @@ async function loginAction(options: LoginOptions): Promise<void> {
99
103
  process.exit(1)
100
104
  }
101
105
  password = await promptHidden('Password')
102
- if (!password) { console.error('Password is required.'); process.exit(1) }
106
+ if (!password) { stderrError('Password is required.'); process.exit(1) }
103
107
  }
104
108
 
105
109
  const manager = new InstagramCredentialManager()
@@ -109,7 +113,7 @@ async function loginAction(options: LoginOptions): Promise<void> {
109
113
  const client = new InstagramClient(manager)
110
114
  client.setSessionPath(paths.session_path)
111
115
  if (options.debug) {
112
- client.setDebugLog((msg) => console.error(`[debug] ${msg}`))
116
+ client.setDebugLog((msg) => debug(`[debug] ${msg}`))
113
117
  }
114
118
 
115
119
  const result = await client.authenticate(username, password)
@@ -118,9 +122,9 @@ async function loginAction(options: LoginOptions): Promise<void> {
118
122
  const twoFactorIdentifier = (result.twoFactorInfo?.['two_factor_identifier'] as string) ?? ''
119
123
 
120
124
  if (interactive) {
121
- console.error('\n Two-factor authentication required.')
125
+ info('\n Two-factor authentication required.')
122
126
  const code = await promptText('Verification code')
123
- if (!code) { console.error('Code is required.'); process.exit(1) }
127
+ if (!code) { stderrError('Code is required.'); process.exit(1) }
124
128
 
125
129
  const tfResult = await client.twoFactorLogin(username, code, twoFactorIdentifier)
126
130
  await saveAccountAndPrint(manager, accountId, username, tfResult.userId, options.pretty)
@@ -161,24 +165,24 @@ async function handleInteractiveChallenge(
161
165
  challengePath: string,
162
166
  pretty?: boolean,
163
167
  ): Promise<void> {
164
- console.error('\n Security challenge required by Instagram.')
165
- console.error(' Choose verification method:')
166
- console.error(' 1. Email')
167
- console.error(' 2. SMS')
168
- console.error('')
168
+ info('\n Security challenge required by Instagram.')
169
+ info(' Choose verification method:')
170
+ info(' 1. Email')
171
+ info(' 2. SMS')
172
+ info('')
169
173
 
170
174
  const choice = await promptText('Method (1/2)')
171
175
  const method = choice === '2' ? 'sms' as const : 'email' as const
172
176
 
173
177
  const sendResult = await client.challengeSendCode(challengePath, method)
174
178
  if (sendResult.contactPoint) {
175
- console.error(`\n Code sent to: ${sendResult.contactPoint}`)
179
+ info(`\n Code sent to: ${sendResult.contactPoint}`)
176
180
  } else {
177
- console.error('\n Verification code sent.')
181
+ info('\n Verification code sent.')
178
182
  }
179
183
 
180
184
  const code = await promptText('Verification code')
181
- if (!code) { console.error('Code is required.'); process.exit(1) }
185
+ if (!code) { stderrError('Code is required.'); process.exit(1) }
182
186
 
183
187
  const verifyResult = await client.challengeSubmitCode(challengePath, code)
184
188
  await saveAccountAndPrint(manager, accountId, username, verifyResult.userId, pretty)
@@ -322,6 +326,114 @@ async function useAction(accountId: string, options: { pretty?: boolean }): Prom
322
326
  }
323
327
  }
324
328
 
329
+ async function extractAction(options: { pretty?: boolean; debug?: boolean }): Promise<void> {
330
+ try {
331
+ const extractor = new InstagramTokenExtractor(
332
+ undefined,
333
+ options.debug ? (msg) => debug(`[debug] ${msg}`) : undefined,
334
+ )
335
+
336
+ if (options.debug) {
337
+ debug('[debug] Searching browser profiles for Instagram cookies...')
338
+ }
339
+
340
+ const results = await extractor.extract()
341
+
342
+ if (results.length === 0) {
343
+ console.log(
344
+ formatOutput(
345
+ {
346
+ error: 'No Instagram cookies found in any browser. Make sure you are logged in to instagram.com in Chrome, Edge, Arc, or Brave.',
347
+ hint: 'Run "auth login --username <username>" to log in manually.',
348
+ },
349
+ options.pretty,
350
+ ),
351
+ )
352
+ process.exit(1)
353
+ return
354
+ }
355
+
356
+ const manager = new InstagramCredentialManager()
357
+
358
+ for (const extracted of results) {
359
+ const session = {
360
+ cookies: [
361
+ `sessionid=${extracted.sessionid}`,
362
+ `ds_user_id=${extracted.ds_user_id}`,
363
+ `csrftoken=${extracted.csrftoken}`,
364
+ extracted.mid ? `mid=${extracted.mid}` : null,
365
+ extracted.ig_did ? `ig_did=${extracted.ig_did}` : null,
366
+ extracted.rur ? `rur=${extracted.rur}` : null,
367
+ ]
368
+ .filter(Boolean)
369
+ .join('; '),
370
+ device: {
371
+ phone_id: randomUUID(),
372
+ uuid: randomUUID(),
373
+ android_device_id: generateAndroidDeviceId(),
374
+ advertising_id: randomUUID(),
375
+ client_session_id: randomUUID(),
376
+ device_string: generateDeviceString(),
377
+ },
378
+ user_id: extracted.ds_user_id,
379
+ mid: extracted.mid,
380
+ }
381
+
382
+ const accountId = createAccountId(extracted.ds_user_id)
383
+ const paths = await manager.ensureAccountPaths(accountId)
384
+
385
+ await mkdir(paths.account_dir, { recursive: true })
386
+ await writeFile(paths.session_path, JSON.stringify(session, null, 2), { mode: 0o600 })
387
+
388
+ const client = new InstagramClient(manager)
389
+ client.setSessionPath(paths.session_path)
390
+ if (options.debug) {
391
+ client.setDebugLog((msg) => debug(`[debug] ${msg}`))
392
+ }
393
+
394
+ await client.loadSession(paths.session_path)
395
+
396
+ let username = extracted.ds_user_id
397
+ try {
398
+ const { data } = await (client as any).request('GET', '/accounts/current_user/?edit=true')
399
+ const user = data['user'] as Record<string, unknown> | undefined
400
+ if (user?.['username']) {
401
+ username = user['username'] as string
402
+ }
403
+ await (client as any).saveSession()
404
+ } catch (err) {
405
+ if (options.debug) {
406
+ debug(`[debug] Session validation failed: ${err}`)
407
+ }
408
+ warn('Warning: Could not validate session. Cookies may be expired.')
409
+ }
410
+
411
+ const now = new Date().toISOString()
412
+ await manager.setAccount({
413
+ account_id: accountId,
414
+ username,
415
+ pk: extracted.ds_user_id,
416
+ created_at: now,
417
+ updated_at: now,
418
+ })
419
+ await manager.setCurrent(accountId)
420
+
421
+ console.log(
422
+ formatOutput(
423
+ {
424
+ authenticated: true,
425
+ account_id: accountId,
426
+ username,
427
+ },
428
+ options.pretty,
429
+ ),
430
+ )
431
+ }
432
+ } catch (error) {
433
+ handleError(error as Error)
434
+ }
435
+ }
436
+
325
437
  async function logoutAction(options: { account?: string; pretty?: boolean }): Promise<void> {
326
438
  try {
327
439
  const manager = new InstagramCredentialManager()
@@ -354,6 +466,13 @@ export const authCommand = new Command('auth')
354
466
  .option('--debug', 'Show raw API responses')
355
467
  .action(loginAction),
356
468
  )
469
+ .addCommand(
470
+ new Command('extract')
471
+ .description('Extract Instagram cookies from browser (Chrome, Edge, Arc, Brave)')
472
+ .option('--pretty', 'Pretty print JSON output')
473
+ .option('--debug', 'Show debug output')
474
+ .action(extractAction),
475
+ )
357
476
  .addCommand(
358
477
  new Command('verify')
359
478
  .description('Complete two-factor authentication (non-interactive)')
@@ -1,24 +1,75 @@
1
+ import { randomUUID } from 'node:crypto'
1
2
  import { existsSync } from 'node:fs'
3
+ import { mkdir, writeFile } from 'node:fs/promises'
4
+
2
5
  import { formatOutput } from '@/shared/utils/output'
6
+
7
+ import { generateAndroidDeviceId, generateDeviceString } from './client'
3
8
  import { InstagramCredentialManager } from './credential-manager'
9
+ import { InstagramTokenExtractor } from './token-extractor'
10
+ import { createAccountId } from './types'
4
11
 
5
12
  export async function ensureInstagramAuth(): Promise<void> {
6
13
  const manager = new InstagramCredentialManager()
7
14
  const account = await manager.getAccount()
8
15
 
9
- if (!account) {
10
- console.log(formatOutput({
11
- error: 'Not authenticated. Run "agent-instagram auth login --username <username>" first.',
12
- }))
13
- process.exit(1)
16
+ if (account) {
17
+ const paths = manager.getAccountPaths(account.account_id)
18
+ if (existsSync(paths.session_path)) {
19
+ return
20
+ }
14
21
  }
15
22
 
16
- const paths = manager.getAccountPaths(account.account_id)
23
+ try {
24
+ const extractor = new InstagramTokenExtractor()
25
+ const results = await extractor.extract()
26
+ const cookies = results[0]
17
27
 
18
- if (!existsSync(paths.session_path)) {
19
- console.log(formatOutput({
20
- error: 'Session expired or missing. Run "agent-instagram auth login --username <username>" to re-authenticate.',
21
- }))
22
- process.exit(1)
23
- }
28
+ if (cookies) {
29
+ const session = {
30
+ cookies: [
31
+ `sessionid=${cookies.sessionid}`,
32
+ `ds_user_id=${cookies.ds_user_id}`,
33
+ `csrftoken=${cookies.csrftoken}`,
34
+ cookies.mid ? `mid=${cookies.mid}` : null,
35
+ cookies.ig_did ? `ig_did=${cookies.ig_did}` : null,
36
+ cookies.rur ? `rur=${cookies.rur}` : null,
37
+ ]
38
+ .filter(Boolean)
39
+ .join('; '),
40
+ device: {
41
+ phone_id: randomUUID(),
42
+ uuid: randomUUID(),
43
+ android_device_id: generateAndroidDeviceId(),
44
+ advertising_id: randomUUID(),
45
+ client_session_id: randomUUID(),
46
+ device_string: generateDeviceString(),
47
+ },
48
+ user_id: cookies.ds_user_id,
49
+ mid: cookies.mid,
50
+ }
51
+
52
+ const accountId = createAccountId(cookies.ds_user_id)
53
+ const paths = await manager.ensureAccountPaths(accountId)
54
+
55
+ await mkdir(paths.account_dir, { recursive: true })
56
+ await writeFile(paths.session_path, JSON.stringify(session, null, 2), { mode: 0o600 })
57
+
58
+ const now = new Date().toISOString()
59
+ await manager.setAccount({
60
+ account_id: accountId,
61
+ username: cookies.ds_user_id,
62
+ pk: cookies.ds_user_id,
63
+ created_at: now,
64
+ updated_at: now,
65
+ })
66
+ await manager.setCurrent(accountId)
67
+ return
68
+ }
69
+ } catch {}
70
+
71
+ console.log(formatOutput({
72
+ error: 'Not authenticated. Run "agent-instagram auth extract" to extract from browser, or "agent-instagram auth login --username <username>" to log in.',
73
+ }))
74
+ process.exit(1)
24
75
  }
@@ -1,6 +1,7 @@
1
1
  export { InstagramClient } from './client'
2
2
  export { InstagramCredentialManager } from './credential-manager'
3
3
  export { InstagramListener, type InstagramListenerEventMap } from './listener'
4
+ export { InstagramTokenExtractor, type ExtractedInstagramCookies } from './token-extractor'
4
5
  export {
5
6
  createAccountId,
6
7
  extractMediaUrl,