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
@@ -0,0 +1,393 @@
1
+ import {
2
+ copyFileSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readdirSync,
6
+ readFileSync,
7
+ rmSync,
8
+ statSync,
9
+ } from 'node:fs'
10
+ import { homedir, tmpdir } from 'node:os'
11
+ import { join } from 'node:path'
12
+
13
+ import { ClassicLevel } from 'classic-level'
14
+
15
+ export interface ExtractedWebexToken {
16
+ accessToken: string
17
+ refreshToken?: string
18
+ expiresAt?: number
19
+ deviceUrl?: string
20
+ }
21
+
22
+ interface BrowserConfig {
23
+ name: string
24
+ darwin: string
25
+ linux: string
26
+ win32: string
27
+ }
28
+
29
+ const BROWSERS: BrowserConfig[] = [
30
+ {
31
+ name: 'Chrome',
32
+ darwin: join('Google', 'Chrome'),
33
+ linux: 'google-chrome',
34
+ win32: join('Google', 'Chrome', 'User Data'),
35
+ },
36
+ {
37
+ name: 'Chrome Canary',
38
+ darwin: join('Google', 'Chrome Canary'),
39
+ linux: 'google-chrome-unstable',
40
+ win32: join('Google', 'Chrome SxS', 'User Data'),
41
+ },
42
+ {
43
+ name: 'Edge',
44
+ darwin: join('Microsoft Edge'),
45
+ linux: 'microsoft-edge',
46
+ win32: join('Microsoft', 'Edge', 'User Data'),
47
+ },
48
+ {
49
+ name: 'Arc',
50
+ darwin: join('Arc', 'User Data'),
51
+ linux: '',
52
+ win32: join('Arc', 'User Data'),
53
+ },
54
+ {
55
+ name: 'Brave',
56
+ darwin: join('BraveSoftware', 'Brave-Browser'),
57
+ linux: join('BraveSoftware', 'Brave-Browser'),
58
+ win32: join('BraveSoftware', 'Brave-Browser', 'User Data'),
59
+ },
60
+ {
61
+ name: 'Vivaldi',
62
+ darwin: 'Vivaldi',
63
+ linux: 'vivaldi',
64
+ win32: join('Vivaldi', 'User Data'),
65
+ },
66
+ {
67
+ name: 'Chromium',
68
+ darwin: 'Chromium',
69
+ linux: 'chromium',
70
+ win32: join('Chromium', 'User Data'),
71
+ },
72
+ ]
73
+
74
+ const WEBEX_STORAGE_KEY = '_https://web.webex.com\x00\x01webex-web-client-bounded'
75
+
76
+ export class WebexTokenExtractor {
77
+ private platform: NodeJS.Platform
78
+ private baseDir: string | null
79
+ private debugLog: ((message: string) => void) | null
80
+
81
+ constructor(platform?: NodeJS.Platform, debugLog?: (message: string) => void, baseDir?: string) {
82
+ this.platform = platform ?? process.platform
83
+ this.debugLog = debugLog ?? null
84
+ this.baseDir = baseDir ?? null
85
+ }
86
+
87
+ private debug(message: string): void {
88
+ this.debugLog?.(message)
89
+ }
90
+
91
+ getBrowserProfileDirs(): string[] {
92
+ if (this.baseDir) {
93
+ return this.discoverProfileDirs(this.baseDir)
94
+ }
95
+
96
+ const dirs: string[] = []
97
+
98
+ for (const browser of BROWSERS) {
99
+ const browserBase = this.getBrowserBasePath(browser)
100
+ if (!browserBase) continue
101
+
102
+ const profileDirs = this.discoverProfileDirs(browserBase)
103
+ dirs.push(...profileDirs)
104
+ }
105
+
106
+ return dirs
107
+ }
108
+
109
+ private getBrowserBasePath(browser: BrowserConfig): string | null {
110
+ let relative: string
111
+
112
+ switch (this.platform) {
113
+ case 'darwin':
114
+ relative = browser.darwin
115
+ if (!relative) return null
116
+ return join(homedir(), 'Library', 'Application Support', relative)
117
+ case 'linux':
118
+ relative = browser.linux
119
+ if (!relative) return null
120
+ return join(homedir(), '.config', relative)
121
+ case 'win32':
122
+ relative = browser.win32
123
+ if (!relative) return null
124
+ return join(
125
+ process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'),
126
+ relative,
127
+ )
128
+ default:
129
+ return null
130
+ }
131
+ }
132
+
133
+ private discoverProfileDirs(browserBase: string): string[] {
134
+ const dirs: string[] = []
135
+
136
+ if (!existsSync(browserBase)) return dirs
137
+
138
+ const defaultLeveldb = join(browserBase, 'Default', 'Local Storage', 'leveldb')
139
+ if (existsSync(defaultLeveldb)) {
140
+ dirs.push(defaultLeveldb)
141
+ }
142
+
143
+ try {
144
+ const entries = readdirSync(browserBase, { withFileTypes: true })
145
+ for (const entry of entries) {
146
+ if (!entry.isDirectory()) continue
147
+ if (!/^Profile \d+$/i.test(entry.name)) continue
148
+
149
+ const leveldb = join(browserBase, entry.name, 'Local Storage', 'leveldb')
150
+ if (existsSync(leveldb)) {
151
+ dirs.push(leveldb)
152
+ }
153
+ }
154
+ } catch {
155
+ // Ignore read errors
156
+ }
157
+
158
+ return dirs
159
+ }
160
+
161
+ async extract(): Promise<ExtractedWebexToken | null> {
162
+ const profileDirs = this.getBrowserProfileDirs()
163
+
164
+ if (profileDirs.length === 0) {
165
+ this.debug('No browser profile directories found')
166
+ return null
167
+ }
168
+
169
+ for (const leveldbDir of profileDirs) {
170
+ this.debug(`Scanning: ${leveldbDir}`)
171
+
172
+ const token = await this.extractViaClassicLevelCopy(leveldbDir)
173
+ ?? this.extractFromRawFiles(leveldbDir)
174
+
175
+ if (token) {
176
+ this.debug(`Found token in: ${leveldbDir}`)
177
+ return token
178
+ }
179
+ }
180
+
181
+ this.debug('No Webex tokens found in any browser profile')
182
+ return null
183
+ }
184
+
185
+ private async extractViaClassicLevelCopy(dbPath: string): Promise<ExtractedWebexToken | null> {
186
+ const tempDir = join(tmpdir(), `webex-leveldb-${Date.now()}-${Math.random().toString(36).slice(2)}`)
187
+
188
+ try {
189
+ mkdirSync(tempDir, { recursive: true })
190
+
191
+ const files = readdirSync(dbPath)
192
+ for (const file of files) {
193
+ if (file === 'LOCK') continue
194
+ const src = join(dbPath, file)
195
+ try {
196
+ if (statSync(src).isFile()) {
197
+ copyFileSync(src, join(tempDir, file))
198
+ }
199
+ } catch {}
200
+ }
201
+
202
+ return await this.extractViaClassicLevel(tempDir)
203
+ } catch {
204
+ return null
205
+ } finally {
206
+ try {
207
+ rmSync(tempDir, { recursive: true, force: true })
208
+ } catch {}
209
+ }
210
+ }
211
+
212
+ private async extractViaClassicLevel(dbPath: string): Promise<ExtractedWebexToken | null> {
213
+ let db: ClassicLevel<string, Buffer> | null = null
214
+ try {
215
+ db = new ClassicLevel(dbPath, { keyEncoding: 'utf8', valueEncoding: 'buffer' })
216
+
217
+ for await (const [key, value] of db.iterator()) {
218
+ if (!key.includes('web.webex.com')) continue
219
+
220
+ const decoded = this.decodeLevelDBValue(value)
221
+ if (!decoded.includes('"supertoken"') && !decoded.includes('"Credentials"')) continue
222
+
223
+ const token = this.extractTokenFromString(decoded)
224
+ if (token) return token
225
+ }
226
+ } catch (e) {
227
+ this.debug(`ClassicLevel failed: ${e instanceof Error ? e.message : String(e)}`)
228
+ } finally {
229
+ if (db) {
230
+ try {
231
+ await db.close()
232
+ } catch {}
233
+ }
234
+ }
235
+ return null
236
+ }
237
+
238
+ private decodeLevelDBValue(buf: Buffer): string {
239
+ if (buf.length < 2) return buf.toString('utf8')
240
+ // Chromium localStorage: 0x00 prefix = UTF-16LE, 0x01 prefix = Latin1/UTF-8
241
+ if (buf[0] === 0x00 && (buf.length - 1) % 2 === 0) {
242
+ return buf.subarray(1).toString('utf16le')
243
+ }
244
+ if (buf[0] === 0x01) {
245
+ return buf.subarray(1).toString('utf8')
246
+ }
247
+ return buf.toString('utf8')
248
+ }
249
+
250
+ private extractFromRawFiles(leveldbDir: string): ExtractedWebexToken | null {
251
+ try {
252
+ const files = readdirSync(leveldbDir)
253
+
254
+ const sorted = [...files]
255
+ .filter((f) => f.endsWith('.log') || f.endsWith('.ldb'))
256
+ .sort((a, b) => {
257
+ const aIsLog = a.endsWith('.log') ? 0 : 1
258
+ const bIsLog = b.endsWith('.log') ? 0 : 1
259
+ if (aIsLog !== bIsLog) return aIsLog - bIsLog
260
+ try {
261
+ return statSync(join(leveldbDir, b)).mtimeMs - statSync(join(leveldbDir, a)).mtimeMs
262
+ } catch {
263
+ return 0
264
+ }
265
+ })
266
+
267
+ for (const file of sorted) {
268
+ const token = this.extractFromFile(join(leveldbDir, file))
269
+ if (token) return token
270
+ }
271
+ } catch {
272
+ this.debug(`Failed to read directory: ${leveldbDir}`)
273
+ }
274
+
275
+ return null
276
+ }
277
+
278
+ private extractFromFile(filePath: string): ExtractedWebexToken | null {
279
+ try {
280
+ const stat = statSync(filePath)
281
+ if (stat.size > 50 * 1024 * 1024) return null
282
+
283
+ const buffer = readFileSync(filePath)
284
+ return this.extractTokenFromBuffer(buffer)
285
+ } catch {
286
+ return null
287
+ }
288
+ }
289
+
290
+ private extractTokenFromBuffer(buffer: Buffer): ExtractedWebexToken | null {
291
+ // Try UTF-8 first, then strip null bytes for Chromium's UTF-16LE localStorage encoding
292
+ return this.extractTokenFromString(buffer.toString('utf8'))
293
+ ?? this.extractTokenFromString(this.stripNullBytes(buffer))
294
+ }
295
+
296
+ private stripNullBytes(buffer: Buffer): string {
297
+ const bytes: number[] = []
298
+ for (let i = 0; i < buffer.length; i++) {
299
+ if (buffer[i] !== 0) bytes.push(buffer[i]!)
300
+ }
301
+ return Buffer.from(bytes).toString('utf8')
302
+ }
303
+
304
+ private extractTokenFromString(content: string): ExtractedWebexToken | null {
305
+ const outerObjectMarkerIdx = content.indexOf('"Credentials"')
306
+ const innermostMarkerIdx = content.indexOf('"supertoken"')
307
+
308
+ const markerIdx =
309
+ outerObjectMarkerIdx !== -1 ? outerObjectMarkerIdx
310
+ : innermostMarkerIdx !== -1 ? innermostMarkerIdx
311
+ : -1
312
+
313
+ if (markerIdx === -1) return null
314
+
315
+ const json = this.extractJsonAroundIndex(content, markerIdx)
316
+ if (!json) return null
317
+
318
+ return this.parseWebexStorage(json)
319
+ }
320
+
321
+ private extractJsonAroundIndex(content: string, markerIdx: number): string | null {
322
+ let depth = 0
323
+ let start = -1
324
+ for (let i = markerIdx; i >= 0; i--) {
325
+ if (content[i] === '}') depth++
326
+ if (content[i] === '{') {
327
+ if (depth === 0) {
328
+ start = i
329
+ break
330
+ }
331
+ depth--
332
+ }
333
+ }
334
+ if (start === -1) return null
335
+
336
+ depth = 0
337
+ let end = -1
338
+ for (let i = start; i < content.length; i++) {
339
+ if (content[i] === '{') depth++
340
+ if (content[i] === '}') {
341
+ depth--
342
+ if (depth === 0) {
343
+ end = i + 1
344
+ break
345
+ }
346
+ }
347
+ }
348
+ if (end === -1) return null
349
+
350
+ return content.substring(start, end)
351
+ }
352
+
353
+ parseWebexStorage(jsonStr: string): ExtractedWebexToken | null {
354
+ try {
355
+ const data = JSON.parse(jsonStr)
356
+
357
+ const supertoken =
358
+ data?.Credentials?.['@']?.supertoken ??
359
+ data?.['@']?.supertoken ??
360
+ data?.supertoken ??
361
+ null
362
+
363
+ if (!supertoken?.access_token) return null
364
+
365
+ const accessToken = String(supertoken.access_token)
366
+ if (accessToken.length < 20) return null
367
+
368
+ const result: ExtractedWebexToken = { accessToken }
369
+
370
+ if (supertoken.refresh_token) {
371
+ result.refreshToken = String(supertoken.refresh_token)
372
+ }
373
+
374
+ if (typeof supertoken.expires === 'number') {
375
+ result.expiresAt = supertoken.expires
376
+ } else if (typeof supertoken.expires_in === 'number') {
377
+ result.expiresAt = Date.now() + supertoken.expires_in * 1000
378
+ }
379
+
380
+ const deviceUrl =
381
+ data?.Device?.['@']?.url ??
382
+ data?.['@']?.deviceUrl ??
383
+ null
384
+ if (deviceUrl && typeof deviceUrl === 'string') {
385
+ result.deviceUrl = deviceUrl
386
+ }
387
+
388
+ return result
389
+ } catch {
390
+ return null
391
+ }
392
+ }
393
+ }
@@ -55,7 +55,8 @@ export interface WebexConfig {
55
55
  expiresAt: number
56
56
  clientId?: string
57
57
  clientSecret?: string
58
- tokenType?: 'oauth' | 'manual'
58
+ tokenType?: 'oauth' | 'manual' | 'extracted'
59
+ deviceUrl?: string
59
60
  }
60
61
 
61
62
  export class WebexError extends Error {
@@ -123,5 +124,6 @@ export const WebexConfigSchema = z.object({
123
124
  expiresAt: z.number(),
124
125
  clientId: z.string().optional(),
125
126
  clientSecret: z.string().optional(),
126
- tokenType: z.enum(['oauth', 'manual']).optional(),
127
+ tokenType: z.enum(['oauth', 'manual', 'extracted']).optional(),
128
+ deviceUrl: z.string().optional(),
127
129
  })
@@ -25,10 +25,14 @@ import {
25
25
 
26
26
  const MAX_MESSAGES_PER_CHAT = 500
27
27
 
28
- function toTimestampMs(ts: number | { toNumber(): number } | null | undefined): number {
28
+ function toTimestampMs(ts: unknown): number {
29
29
  if (ts == null) return 0
30
- if (typeof ts === 'object') return ts.toNumber() * 1000
31
- return ts * 1000
30
+ if (typeof ts === 'number') return ts * 1000
31
+ if (typeof ts === 'object' && ts !== null && 'toNumber' in ts && typeof (ts as Record<string, unknown>).toNumber === 'function') {
32
+ return (ts as { toNumber(): number }).toNumber() * 1000
33
+ }
34
+ const n = Number(ts)
35
+ return Number.isNaN(n) ? 0 : n * 1000
32
36
  }
33
37
 
34
38
  function resolveJid(input: string): string {
@@ -443,8 +447,8 @@ export class WhatsAppClient {
443
447
  const sorted = chats.sort((a, b) => {
444
448
  const aTime = a.conversationTimestamp
445
449
  const bTime = b.conversationTimestamp
446
- const aMs = toTimestampMs(aTime as number | { toNumber(): number } | null | undefined)
447
- const bMs = toTimestampMs(bTime as number | { toNumber(): number } | null | undefined)
450
+ const aMs = toTimestampMs(aTime)
451
+ const bMs = toTimestampMs(bTime)
448
452
  return bMs - aMs
449
453
  })
450
454
 
@@ -474,8 +478,8 @@ export class WhatsAppClient {
474
478
 
475
479
  const msgs = this.messages.get(resolvedJid) ?? []
476
480
  const sorted = [...msgs].sort((a, b) => {
477
- const aMs = toTimestampMs(a.messageTimestamp as number | { toNumber(): number } | null | undefined)
478
- const bMs = toTimestampMs(b.messageTimestamp as number | { toNumber(): number } | null | undefined)
481
+ const aMs = toTimestampMs(a.messageTimestamp)
482
+ const bMs = toTimestampMs(b.messageTimestamp)
479
483
  return aMs - bMs
480
484
  })
481
485
 
@@ -3,7 +3,7 @@ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
3
3
  import { homedir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
 
6
- export type Platform = 'slack' | 'discord' | 'teams'
6
+ export type Platform = 'slack' | 'discord' | 'teams' | 'webex'
7
7
 
8
8
  /**
9
9
  * Caches derived encryption keys to avoid repeated macOS Keychain prompts.
@@ -1,4 +1,6 @@
1
- export function handleError(error: Error): void {
2
- console.error(JSON.stringify({ error: error.message }))
1
+ import { error } from './stderr'
2
+
3
+ export function handleError(err: Error): void {
4
+ error(JSON.stringify({ error: err.message }))
3
5
  process.exit(1)
4
6
  }
@@ -0,0 +1,22 @@
1
+ const isTTY = process.stderr.isTTY ?? false
2
+
3
+ const RESET = isTTY ? '\x1b[0m' : ''
4
+ const RED = isTTY ? '\x1b[31m' : ''
5
+ const YELLOW = isTTY ? '\x1b[33m' : ''
6
+ const DIM = isTTY ? '\x1b[2m' : ''
7
+
8
+ export function info(message: string): void {
9
+ process.stderr.write(`${message}\n`)
10
+ }
11
+
12
+ export function warn(message: string): void {
13
+ process.stderr.write(`${YELLOW}${message}${RESET}\n`)
14
+ }
15
+
16
+ export function error(message: string): void {
17
+ process.stderr.write(`${RED}${message}${RESET}\n`)
18
+ }
19
+
20
+ export function debug(message: string): void {
21
+ process.stderr.write(`${DIM}${message}${RESET}\n`)
22
+ }