agent-messenger 2.28.0 → 2.29.1

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 (159) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +29 -1
  3. package/dist/package.json +1 -1
  4. package/dist/src/platforms/discord/client.d.ts +6 -2
  5. package/dist/src/platforms/discord/client.d.ts.map +1 -1
  6. package/dist/src/platforms/discord/client.js +10 -3
  7. package/dist/src/platforms/discord/client.js.map +1 -1
  8. package/dist/src/platforms/instagram/client.d.ts +24 -2
  9. package/dist/src/platforms/instagram/client.d.ts.map +1 -1
  10. package/dist/src/platforms/instagram/client.js +343 -53
  11. package/dist/src/platforms/instagram/client.js.map +1 -1
  12. package/dist/src/platforms/instagram/commands/auth.d.ts +1 -0
  13. package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
  14. package/dist/src/platforms/instagram/commands/auth.js +123 -11
  15. package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
  16. package/dist/src/platforms/instagram/credential-manager.d.ts +4 -1
  17. package/dist/src/platforms/instagram/credential-manager.d.ts.map +1 -1
  18. package/dist/src/platforms/instagram/credential-manager.js +17 -1
  19. package/dist/src/platforms/instagram/credential-manager.js.map +1 -1
  20. package/dist/src/platforms/instagram/types.d.ts +10 -8
  21. package/dist/src/platforms/instagram/types.d.ts.map +1 -1
  22. package/dist/src/platforms/instagram/types.js.map +1 -1
  23. package/dist/src/platforms/teams/app-config.d.ts +36 -0
  24. package/dist/src/platforms/teams/app-config.d.ts.map +1 -0
  25. package/dist/src/platforms/teams/app-config.js +69 -0
  26. package/dist/src/platforms/teams/app-config.js.map +1 -0
  27. package/dist/src/platforms/teams/client.d.ts +20 -2
  28. package/dist/src/platforms/teams/client.d.ts.map +1 -1
  29. package/dist/src/platforms/teams/client.js +311 -7
  30. package/dist/src/platforms/teams/client.js.map +1 -1
  31. package/dist/src/platforms/teams/commands/auth.d.ts +10 -0
  32. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
  33. package/dist/src/platforms/teams/commands/auth.js +137 -3
  34. package/dist/src/platforms/teams/commands/auth.js.map +1 -1
  35. package/dist/src/platforms/teams/commands/file.d.ts +3 -0
  36. package/dist/src/platforms/teams/commands/file.d.ts.map +1 -1
  37. package/dist/src/platforms/teams/commands/file.js +59 -2
  38. package/dist/src/platforms/teams/commands/file.js.map +1 -1
  39. package/dist/src/platforms/teams/commands/message.d.ts +11 -0
  40. package/dist/src/platforms/teams/commands/message.d.ts.map +1 -1
  41. package/dist/src/platforms/teams/commands/message.js +121 -2
  42. package/dist/src/platforms/teams/commands/message.js.map +1 -1
  43. package/dist/src/platforms/teams/credential-manager.d.ts +23 -1
  44. package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
  45. package/dist/src/platforms/teams/credential-manager.js +46 -2
  46. package/dist/src/platforms/teams/credential-manager.js.map +1 -1
  47. package/dist/src/platforms/teams/device-code.d.ts +48 -0
  48. package/dist/src/platforms/teams/device-code.d.ts.map +1 -0
  49. package/dist/src/platforms/teams/device-code.js +199 -0
  50. package/dist/src/platforms/teams/device-code.js.map +1 -0
  51. package/dist/src/platforms/teams/device-login.d.ts +37 -0
  52. package/dist/src/platforms/teams/device-login.d.ts.map +1 -0
  53. package/dist/src/platforms/teams/device-login.js +129 -0
  54. package/dist/src/platforms/teams/device-login.js.map +1 -0
  55. package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
  56. package/dist/src/platforms/teams/ensure-auth.js +12 -0
  57. package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
  58. package/dist/src/platforms/teams/index.d.ts +7 -3
  59. package/dist/src/platforms/teams/index.d.ts.map +1 -1
  60. package/dist/src/platforms/teams/index.js +5 -2
  61. package/dist/src/platforms/teams/index.js.map +1 -1
  62. package/dist/src/platforms/teams/listener.d.ts +40 -0
  63. package/dist/src/platforms/teams/listener.d.ts.map +1 -0
  64. package/dist/src/platforms/teams/listener.js +286 -0
  65. package/dist/src/platforms/teams/listener.js.map +1 -0
  66. package/dist/src/platforms/teams/realm-discovery.d.ts +3 -0
  67. package/dist/src/platforms/teams/realm-discovery.d.ts.map +1 -0
  68. package/dist/src/platforms/teams/realm-discovery.js +33 -0
  69. package/dist/src/platforms/teams/realm-discovery.js.map +1 -0
  70. package/dist/src/platforms/teams/token-extractor.d.ts +3 -0
  71. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  72. package/dist/src/platforms/teams/token-extractor.js +60 -0
  73. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  74. package/dist/src/platforms/teams/token-provider.d.ts +25 -0
  75. package/dist/src/platforms/teams/token-provider.d.ts.map +1 -0
  76. package/dist/src/platforms/teams/token-provider.js +190 -0
  77. package/dist/src/platforms/teams/token-provider.js.map +1 -0
  78. package/dist/src/platforms/teams/trouter.d.ts +38 -0
  79. package/dist/src/platforms/teams/trouter.d.ts.map +1 -0
  80. package/dist/src/platforms/teams/trouter.js +266 -0
  81. package/dist/src/platforms/teams/trouter.js.map +1 -0
  82. package/dist/src/platforms/teams/types.d.ts +103 -0
  83. package/dist/src/platforms/teams/types.d.ts.map +1 -1
  84. package/dist/src/platforms/teams/types.js +30 -0
  85. package/dist/src/platforms/teams/types.js.map +1 -1
  86. package/dist/src/platforms/webex/types.d.ts +1 -1
  87. package/dist/src/tui/adapters/instagram-adapter.d.ts.map +1 -1
  88. package/dist/src/tui/adapters/instagram-adapter.js +16 -0
  89. package/dist/src/tui/adapters/instagram-adapter.js.map +1 -1
  90. package/dist/src/tui/adapters/teams-adapter.d.ts +3 -0
  91. package/dist/src/tui/adapters/teams-adapter.d.ts.map +1 -1
  92. package/dist/src/tui/adapters/teams-adapter.js +21 -0
  93. package/dist/src/tui/adapters/teams-adapter.js.map +1 -1
  94. package/dist/src/tui/app.js +3 -3
  95. package/docs/content/docs/cli/instagram.mdx +1 -0
  96. package/docs/content/docs/cli/teams.mdx +36 -0
  97. package/docs/content/docs/sdk/teams.mdx +30 -0
  98. package/package.json +1 -1
  99. package/skills/agent-channeltalk/SKILL.md +1 -1
  100. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  101. package/skills/agent-discord/SKILL.md +1 -1
  102. package/skills/agent-discordbot/SKILL.md +1 -1
  103. package/skills/agent-imessage/SKILL.md +1 -1
  104. package/skills/agent-instagram/SKILL.md +1 -1
  105. package/skills/agent-instagram/references/authentication.md +11 -0
  106. package/skills/agent-kakaotalk/SKILL.md +1 -1
  107. package/skills/agent-line/SKILL.md +1 -1
  108. package/skills/agent-slack/SKILL.md +1 -1
  109. package/skills/agent-slackbot/SKILL.md +1 -1
  110. package/skills/agent-teams/SKILL.md +82 -6
  111. package/skills/agent-teams/references/authentication.md +31 -1
  112. package/skills/agent-telegram/SKILL.md +1 -1
  113. package/skills/agent-telegrambot/SKILL.md +1 -1
  114. package/skills/agent-webex/SKILL.md +1 -1
  115. package/skills/agent-webexbot/SKILL.md +1 -1
  116. package/skills/agent-wechatbot/SKILL.md +1 -1
  117. package/skills/agent-whatsapp/SKILL.md +1 -1
  118. package/skills/agent-whatsappbot/SKILL.md +1 -1
  119. package/src/platforms/discord/client.test.ts +86 -0
  120. package/src/platforms/discord/client.ts +10 -3
  121. package/src/platforms/instagram/client.test.ts +478 -33
  122. package/src/platforms/instagram/client.ts +449 -68
  123. package/src/platforms/instagram/commands/auth.test.ts +177 -1
  124. package/src/platforms/instagram/commands/auth.ts +182 -11
  125. package/src/platforms/instagram/credential-manager.test.ts +30 -0
  126. package/src/platforms/instagram/credential-manager.ts +23 -1
  127. package/src/platforms/instagram/types.ts +11 -8
  128. package/src/platforms/teams/app-config.ts +92 -0
  129. package/src/platforms/teams/client.test.ts +335 -1
  130. package/src/platforms/teams/client.ts +370 -7
  131. package/src/platforms/teams/commands/auth.test.ts +93 -1
  132. package/src/platforms/teams/commands/auth.ts +182 -3
  133. package/src/platforms/teams/commands/file.test.ts +92 -1
  134. package/src/platforms/teams/commands/file.ts +76 -1
  135. package/src/platforms/teams/commands/message.test.ts +95 -1
  136. package/src/platforms/teams/commands/message.ts +145 -3
  137. package/src/platforms/teams/credential-manager.test.ts +20 -0
  138. package/src/platforms/teams/credential-manager.ts +68 -3
  139. package/src/platforms/teams/device-code.test.ts +311 -0
  140. package/src/platforms/teams/device-code.ts +277 -0
  141. package/src/platforms/teams/device-login.test.ts +288 -0
  142. package/src/platforms/teams/device-login.ts +206 -0
  143. package/src/platforms/teams/ensure-auth.ts +11 -0
  144. package/src/platforms/teams/index.test.ts +10 -0
  145. package/src/platforms/teams/index.ts +17 -1
  146. package/src/platforms/teams/listener.test.ts +243 -0
  147. package/src/platforms/teams/listener.ts +351 -0
  148. package/src/platforms/teams/realm-discovery.test.ts +73 -0
  149. package/src/platforms/teams/realm-discovery.ts +42 -0
  150. package/src/platforms/teams/token-extractor.ts +69 -0
  151. package/src/platforms/teams/token-provider.test.ts +227 -0
  152. package/src/platforms/teams/token-provider.ts +284 -0
  153. package/src/platforms/teams/trouter.test.ts +174 -0
  154. package/src/platforms/teams/trouter.ts +323 -0
  155. package/src/platforms/teams/types.test.ts +17 -0
  156. package/src/platforms/teams/types.ts +96 -0
  157. package/src/tui/adapters/instagram-adapter.ts +13 -0
  158. package/src/tui/adapters/teams-adapter.ts +23 -0
  159. package/src/tui/app.ts +3 -3
@@ -0,0 +1,323 @@
1
+ import { gunzipSync } from 'node:zlib'
2
+
3
+ import type { TeamsAccountType } from './types'
4
+
5
+ // Trouter is Teams' internal real-time notification service. Protocol shape was
6
+ // reverse-engineered from the Teams web client and cross-checked against the
7
+ // open-source purple-teams and eisbaw/ost implementations. It speaks socket.io
8
+ // v1 (colon-prefixed frames) over a WebSocket, NOT engine.io.
9
+
10
+ const TROUTER_BOOTSTRAP_URL = 'https://go.trouter.teams.microsoft.com/v4/a'
11
+ // Personal/TFL accounts register against edge.skype.com; work accounts use
12
+ // teams.microsoft.com.
13
+ const REGISTRAR_URL_PERSONAL = 'https://edge.skype.com/registrar/prod/v2/registrations'
14
+ const REGISTRAR_URL_WORK = 'https://teams.microsoft.com/registrar/prod/V2/registrations'
15
+ const CLIENTINFO_VERSION = '27/1.0.0.2024101502'
16
+ const TC = JSON.stringify({ cv: '2024.04.01.1', ua: 'TeamsCDL', hr: '', v: CLIENTINFO_VERSION })
17
+ const USER_AGENT =
18
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Electron/35.3.0'
19
+ const REGISTRATION_TTL_SECONDS = 86400
20
+
21
+ export interface TrouterConnectParams {
22
+ [key: string]: string
23
+ }
24
+
25
+ export interface TrouterInfo {
26
+ socketio: string
27
+ surl: string
28
+ connectparams: TrouterConnectParams
29
+ ccid?: string
30
+ }
31
+
32
+ export interface TrouterRequestFrame {
33
+ id: number
34
+ url?: string
35
+ headers?: Record<string, string>
36
+ body?: string
37
+ }
38
+
39
+ interface RegistrationSpec {
40
+ appId: string
41
+ templateKey: string
42
+ pathSuffix: string
43
+ productContext: string
44
+ }
45
+
46
+ const REGISTRATION_SPECS: RegistrationSpec[] = [
47
+ {
48
+ appId: 'NextGenCalling',
49
+ templateKey: 'DesktopNgc_2.5:SkypeNgc',
50
+ pathSuffix: 'NGCallManagerWin',
51
+ productContext: '',
52
+ },
53
+ { appId: 'SkypeSpacesWeb', templateKey: 'SkypeSpacesWeb_2.4', pathSuffix: 'SkypeSpacesWeb', productContext: '' },
54
+ { appId: 'TeamsCDLWebWorker', templateKey: 'TeamsCDLWebWorker_2.6', pathSuffix: '', productContext: 'TFL' },
55
+ ]
56
+
57
+ export function buildTrouterQuery(info: TrouterInfo, endpointId: string): string {
58
+ const params = new URLSearchParams()
59
+ params.set('v', 'v4')
60
+ for (const [key, value] of Object.entries(info.connectparams)) params.set(key, value)
61
+ params.set('tc', TC)
62
+ params.set('timeout', '40')
63
+ params.set('auth', 'true')
64
+ params.set('epid', endpointId)
65
+ if (info.ccid) params.set('ccid', info.ccid)
66
+ params.set('con_num', `${Date.now()}_1`)
67
+ return params.toString()
68
+ }
69
+
70
+ export async function fetchTrouterInfo(skypeToken: string, endpointId: string): Promise<TrouterInfo> {
71
+ const response = await fetch(`${TROUTER_BOOTSTRAP_URL}?epid=${encodeURIComponent(endpointId)}`, {
72
+ method: 'POST',
73
+ headers: { 'x-skypetoken': skypeToken, 'Content-Length': '0', 'User-Agent': USER_AGENT },
74
+ })
75
+ if (!response.ok) {
76
+ throw new Error(`Trouter bootstrap failed: ${response.status} ${response.statusText}`)
77
+ }
78
+ return (await response.json()) as TrouterInfo
79
+ }
80
+
81
+ export async function fetchTrouterSessionId(
82
+ info: TrouterInfo,
83
+ skypeToken: string,
84
+ endpointId: string,
85
+ ): Promise<string> {
86
+ const url = `${info.socketio}socket.io/1/?${buildTrouterQuery(info, endpointId)}`
87
+ const response = await fetch(url, { headers: { 'X-Skypetoken': skypeToken, 'User-Agent': USER_AGENT } })
88
+ if (!response.ok) {
89
+ throw new Error(`Trouter handshake failed: ${response.status} ${response.statusText}`)
90
+ }
91
+ const body = await response.text()
92
+ return body.split(':')[0]
93
+ }
94
+
95
+ export function buildWebSocketUrl(info: TrouterInfo, sessionId: string, endpointId: string): string {
96
+ return `${info.socketio}socket.io/1/websocket/${sessionId}?${buildTrouterQuery(info, endpointId)}`
97
+ }
98
+
99
+ export function buildAuthenticateFrame(info: TrouterInfo, idToken: string): string {
100
+ const message = {
101
+ name: 'user.authenticate',
102
+ args: [
103
+ {
104
+ headers: {
105
+ 'X-Ms-Test-User': 'False',
106
+ Authorization: `Bearer ${idToken}`,
107
+ 'X-MS-Migration': 'True',
108
+ },
109
+ connectparams: info.connectparams,
110
+ },
111
+ ],
112
+ }
113
+ return `5:::${JSON.stringify(message)}`
114
+ }
115
+
116
+ export function buildActivityFrame(sequence: number): string {
117
+ return `5:${sequence}::{"name":"user.activity","args":[{"state":"active"}]}`
118
+ }
119
+
120
+ export function buildPingFrame(sequence: number): string {
121
+ return `5:${sequence}::{"name":"ping"}`
122
+ }
123
+
124
+ // Trouter delivers messages as socket.io "3:::" data frames, which require an
125
+ // HTTP-style 200 acknowledgement or the server marks the endpoint unresponsive
126
+ // and stops routing.
127
+ export function buildRequestAck(requestId: number): string {
128
+ return `3:::${JSON.stringify({ id: requestId, status: 200, body: '' })}`
129
+ }
130
+
131
+ // Server "5:<id>+::" event frames must be acked with "6:<id>::" or trouter
132
+ // eventually drops the connection.
133
+ export function buildEventAck(frame: string): string | null {
134
+ const match = frame.match(/^5:(\d+)\+?::/)
135
+ return match ? `6:${match[1]}::` : null
136
+ }
137
+
138
+ export async function registerEndpoint(
139
+ info: TrouterInfo,
140
+ skypeToken: string,
141
+ idToken: string,
142
+ endpointId: string,
143
+ accountType: TeamsAccountType,
144
+ makeId: () => string,
145
+ ): Promise<void> {
146
+ const registrarUrl = accountType === 'personal' ? REGISTRAR_URL_PERSONAL : REGISTRAR_URL_WORK
147
+
148
+ for (const spec of REGISTRATION_SPECS) {
149
+ // The messaging worker must reuse the endpoint id; call apps get fresh ids.
150
+ const registrationId = spec.appId === 'TeamsCDLWebWorker' ? endpointId : makeId()
151
+ const payload = {
152
+ clientDescription: {
153
+ appId: spec.appId,
154
+ aesKey: '',
155
+ languageId: 'en-US',
156
+ platform: 'edge',
157
+ templateKey: spec.templateKey,
158
+ platformUIVersion: CLIENTINFO_VERSION,
159
+ productContext: accountType === 'personal' ? spec.productContext : '',
160
+ },
161
+ registrationId,
162
+ nodeId: '',
163
+ transports: { TROUTER: [{ context: '', path: `${info.surl}${spec.pathSuffix}`, ttl: REGISTRATION_TTL_SECONDS }] },
164
+ }
165
+
166
+ const response = await fetch(registrarUrl, {
167
+ method: 'POST',
168
+ headers: {
169
+ 'Content-Type': 'application/json',
170
+ 'X-Skypetoken': skypeToken,
171
+ Authorization: `Bearer ${idToken}`,
172
+ 'User-Agent': USER_AGENT,
173
+ },
174
+ body: JSON.stringify(payload),
175
+ })
176
+ if (!response.ok) {
177
+ throw new Error(`Trouter registration for ${spec.appId} failed: ${response.status}`)
178
+ }
179
+ }
180
+ }
181
+
182
+ export function parseRequestFrame(frame: string): TrouterRequestFrame | null {
183
+ if (!frame.startsWith('3:::')) return null
184
+ try {
185
+ const parsed = JSON.parse(frame.slice(4)) as unknown
186
+ if (typeof parsed !== 'object' || parsed === null) return null
187
+ const { id } = parsed as { id?: unknown }
188
+ if (typeof id !== 'number' || !Number.isFinite(id)) return null
189
+ return parsed as TrouterRequestFrame
190
+ } catch {
191
+ return null
192
+ }
193
+ }
194
+
195
+ export function isMessageLossFrame(frame: string): boolean {
196
+ if (!frame.startsWith('5:')) return false
197
+ const jsonStart = frame.indexOf('{')
198
+ if (jsonStart === -1) return false
199
+ try {
200
+ const payload = JSON.parse(frame.slice(jsonStart)) as { name?: string }
201
+ return payload.name === 'trouter.message_loss'
202
+ } catch {
203
+ return false
204
+ }
205
+ }
206
+
207
+ // Trouter message bodies may be gzip+base64 encoded, and nest a further encoded
208
+ // payload under `cp` (gzip+base64) or `gp` (base64).
209
+ export function decodeMessageBody(headers: Record<string, string>, body: string): Record<string, unknown> {
210
+ let raw = body
211
+ if (headers['X-Microsoft-Skype-Content-Encoding'] === 'gzip') {
212
+ raw = gunzipSync(Buffer.from(body, 'base64')).toString('utf8')
213
+ }
214
+ const obj = JSON.parse(raw) as Record<string, unknown>
215
+ if (typeof obj.cp === 'string') {
216
+ return JSON.parse(gunzipSync(Buffer.from(obj.cp, 'base64')).toString('utf8')) as Record<string, unknown>
217
+ }
218
+ if (typeof obj.gp === 'string') {
219
+ return JSON.parse(Buffer.from(obj.gp, 'base64').toString('utf8')) as Record<string, unknown>
220
+ }
221
+ return obj
222
+ }
223
+
224
+ export function extractChatId(link: string | undefined): string | null {
225
+ if (!link) return null
226
+ const match = link.match(/conversations\/([^/]+)/)
227
+ return match ? decodeURIComponent(match[1]) : null
228
+ }
229
+
230
+ // `id` is Teams' positional mention index (the content span's `itemid`); `mri`
231
+ // is the target's Skype MRI, present only via `properties.mentions` metadata.
232
+ export interface TrouterMention {
233
+ id: string
234
+ mri?: string
235
+ displayName: string
236
+ }
237
+
238
+ // 1:1 chats resolve to a unique roster / one-to-one conversation id; anything on
239
+ // a thread (group chat OR team channel) shares the @thread.* family. The thread
240
+ // family alone cannot tell a group chat from a channel — that needs the
241
+ // conversation's groupId, which the realtime resource does not carry.
242
+ export function isThreadConversation(conversationId: string): boolean {
243
+ return conversationId.includes('@thread.tacv2') || conversationId.includes('@thread.v2')
244
+ }
245
+
246
+ function parseJsonRecord(value: unknown): Record<string, unknown> | undefined {
247
+ const source = typeof value === 'string' ? safeJsonParse(value) : value
248
+ if (typeof source !== 'object' || source === null || Array.isArray(source)) return undefined
249
+ return source as Record<string, unknown>
250
+ }
251
+
252
+ function parseJsonArray(value: unknown): unknown[] | undefined {
253
+ const source = typeof value === 'string' ? safeJsonParse(value) : value
254
+ return Array.isArray(source) ? source : undefined
255
+ }
256
+
257
+ function safeJsonParse(value: string): unknown {
258
+ try {
259
+ return JSON.parse(value)
260
+ } catch {
261
+ return undefined
262
+ }
263
+ }
264
+
265
+ // Mentions arrive two ways: the authoritative `properties.mentions` array (JSON
266
+ // or JSON-string) carries real MRIs, while the message content only has
267
+ // positional `<span itemtype=".../Mention" itemid="N">Name</span>` markup with
268
+ // no MRI. Prefer the metadata; fall back to scraping the content spans so a
269
+ // mention is still surfaced when properties are missing. Never throws — bad
270
+ // data yields an empty list.
271
+ export function parseMentions(properties: unknown, content: string): TrouterMention[] {
272
+ const fromProperties = parseMentionsFromProperties(properties)
273
+ if (fromProperties.length > 0) return fromProperties
274
+ return parseMentionsFromContent(content)
275
+ }
276
+
277
+ function parseMentionsFromProperties(properties: unknown): TrouterMention[] {
278
+ const record = parseJsonRecord(properties)
279
+ if (!record) return []
280
+ const rawMentions = parseJsonArray(record.mentions)
281
+ if (!rawMentions) return []
282
+
283
+ const mentions: TrouterMention[] = []
284
+ for (const entry of rawMentions) {
285
+ if (typeof entry !== 'object' || entry === null) continue
286
+ const item = entry as Record<string, unknown>
287
+ const id = item.itemid
288
+ if (typeof id !== 'string' && typeof id !== 'number') continue
289
+ mentions.push({
290
+ id: String(id),
291
+ mri: typeof item.mri === 'string' ? item.mri : undefined,
292
+ displayName: typeof item.displayName === 'string' ? item.displayName : '',
293
+ })
294
+ }
295
+ return mentions
296
+ }
297
+
298
+ const MENTION_SPAN_REGEX = /<span\b[^>]*itemtype=["'][^"']*Mention[^"']*["'][^>]*>(.*?)<\/span>/gi
299
+
300
+ function parseMentionsFromContent(content: string): TrouterMention[] {
301
+ const mentions: TrouterMention[] = []
302
+ for (const match of content.matchAll(MENTION_SPAN_REGEX)) {
303
+ const attributes = match[0]
304
+ const itemId = attributes.match(/itemid=["']([^"']*)["']/i)?.[1]
305
+ if (itemId === undefined) continue
306
+ mentions.push({
307
+ id: itemId,
308
+ displayName: stripTags(match[1]),
309
+ })
310
+ }
311
+ return mentions
312
+ }
313
+
314
+ function stripTags(html: string): string {
315
+ return html
316
+ .replace(/<[^>]*>/g, '')
317
+ .replace(/&amp;/g, '&')
318
+ .replace(/&lt;/g, '<')
319
+ .replace(/&gt;/g, '>')
320
+ .replace(/&quot;/g, '"')
321
+ .replace(/&#39;/g, "'")
322
+ .trim()
323
+ }
@@ -8,6 +8,7 @@ import {
8
8
  TeamsFileSchema,
9
9
  TeamsMessageSchema,
10
10
  TeamsReactionSchema,
11
+ TeamsSearchResultSchema,
11
12
  TeamsTeamSchema,
12
13
  TeamsUserSchema,
13
14
  } from './types'
@@ -87,6 +88,22 @@ it('TeamsMessageSchema rejects missing required fields', () => {
87
88
  expect(result.success).toBe(false)
88
89
  })
89
90
 
91
+ it('TeamsSearchResultSchema validates search result shape', () => {
92
+ const result = TeamsSearchResultSchema.safeParse({
93
+ id: 'msg-123',
94
+ content: 'Deploy complete',
95
+ author: { id: 'user-123', displayName: 'Alice' },
96
+ channel_id: 'channel-123',
97
+ thread_id: 'thread-123',
98
+ team_name: 'Team One',
99
+ channel_name: 'General',
100
+ timestamp: '2024-01-01T00:00:00.000Z',
101
+ permalink: 'https://teams.microsoft.com/l/message/msg-123',
102
+ })
103
+
104
+ expect(result.success).toBe(true)
105
+ })
106
+
90
107
  // TeamsUserSchema tests
91
108
  it('TeamsUserSchema validates correct user', () => {
92
109
  const result = TeamsUserSchema.safeParse({
@@ -26,6 +26,24 @@ export interface TeamsMessage {
26
26
  }
27
27
  content: string
28
28
  timestamp: string
29
+ root_message_id?: string
30
+ parent_message_id?: string
31
+ is_thread_reply?: boolean
32
+ }
33
+
34
+ export interface TeamsSearchResult {
35
+ id: string
36
+ content: string
37
+ author: {
38
+ id: string
39
+ displayName: string
40
+ }
41
+ channel_id: string
42
+ thread_id?: string
43
+ team_name?: string
44
+ channel_name?: string
45
+ timestamp: string
46
+ permalink?: string
29
47
  }
30
48
 
31
49
  export interface TeamsUser {
@@ -56,6 +74,8 @@ export interface TeamsFile {
56
74
  size: number
57
75
  url: string
58
76
  contentType?: string
77
+ sharepoint_url?: string
78
+ object_url?: string
59
79
  }
60
80
 
61
81
  export interface TeamsCredentials {
@@ -67,6 +87,8 @@ export type TeamsAccountType = 'work' | 'personal'
67
87
 
68
88
  export type TeamsRegion = 'amer' | 'emea' | 'apac'
69
89
 
90
+ export type TeamsAuthMethod = 'device-code' | 'browser' | 'extracted' | 'manual'
91
+
70
92
  export interface TeamsAccount {
71
93
  token: string
72
94
  token_expires_at?: string
@@ -81,6 +103,10 @@ export interface TeamsAccount {
81
103
  team_name: string
82
104
  }
83
105
  >
106
+ auth_method?: TeamsAuthMethod
107
+ aad_refresh_token?: string
108
+ aad_client_id?: string
109
+ aad_tenant_id?: string
84
110
  }
85
111
 
86
112
  export interface TeamsConfig {
@@ -125,6 +151,24 @@ export const TeamsMessageSchema = z.object({
125
151
  }),
126
152
  content: z.string(),
127
153
  timestamp: z.string(),
154
+ root_message_id: z.string().optional(),
155
+ parent_message_id: z.string().optional(),
156
+ is_thread_reply: z.boolean().optional(),
157
+ })
158
+
159
+ export const TeamsSearchResultSchema = z.object({
160
+ id: z.string(),
161
+ content: z.string(),
162
+ author: z.object({
163
+ id: z.string(),
164
+ displayName: z.string(),
165
+ }),
166
+ channel_id: z.string(),
167
+ thread_id: z.string().optional(),
168
+ team_name: z.string().optional(),
169
+ channel_name: z.string().optional(),
170
+ timestamp: z.string(),
171
+ permalink: z.string().optional(),
128
172
  })
129
173
 
130
174
  export const TeamsUserSchema = z.object({
@@ -155,6 +199,8 @@ export const TeamsFileSchema = z.object({
155
199
  size: z.number(),
156
200
  url: z.string(),
157
201
  contentType: z.string().optional(),
202
+ sharepoint_url: z.string().optional(),
203
+ object_url: z.string().optional(),
158
204
  })
159
205
 
160
206
  export const TeamsCredentialsSchema = z.object({
@@ -166,6 +212,8 @@ export const TeamsAccountTypeSchema = z.enum(['work', 'personal'])
166
212
 
167
213
  export const TeamsRegionSchema = z.enum(['amer', 'emea', 'apac'])
168
214
 
215
+ export const TeamsAuthMethodSchema = z.enum(['device-code', 'browser', 'extracted', 'manual'])
216
+
169
217
  export const TeamsAccountSchema = z.object({
170
218
  token: z.string(),
171
219
  token_expires_at: z.string().optional(),
@@ -180,6 +228,10 @@ export const TeamsAccountSchema = z.object({
180
228
  team_name: z.string(),
181
229
  }),
182
230
  ),
231
+ auth_method: TeamsAuthMethodSchema.optional(),
232
+ aad_refresh_token: z.string().optional(),
233
+ aad_client_id: z.string().optional(),
234
+ aad_tenant_id: z.string().optional(),
183
235
  })
184
236
 
185
237
  export const TeamsConfigSchema = z.object({
@@ -200,6 +252,41 @@ export const TeamsConfigLegacySchema = z.object({
200
252
  ),
201
253
  })
202
254
 
255
+ export interface TeamsMention {
256
+ id: string
257
+ mri?: string
258
+ displayName: string
259
+ }
260
+
261
+ export interface TeamsRealtimeMessage {
262
+ id: string
263
+ chatId: string
264
+ conversationType: 'chat' | 'channel'
265
+ teamId?: string
266
+ channelId?: string
267
+ content: string
268
+ mentions: TeamsMention[]
269
+ author: {
270
+ id: string
271
+ displayName: string
272
+ }
273
+ messageType: string
274
+ timestamp: string
275
+ }
276
+
277
+ export interface TeamsTrouterGenericEvent {
278
+ resourceType: string
279
+ [key: string]: unknown
280
+ }
281
+
282
+ export interface TeamsListenerEventMap {
283
+ message: [message: TeamsRealtimeMessage]
284
+ teams_event: [event: TeamsTrouterGenericEvent]
285
+ connected: [info: { endpointId: string }]
286
+ disconnected: []
287
+ error: [error: Error]
288
+ }
289
+
203
290
  export class TeamsError extends Error {
204
291
  code: string
205
292
 
@@ -209,3 +296,12 @@ export class TeamsError extends Error {
209
296
  this.code = code
210
297
  }
211
298
  }
299
+
300
+ export class TeamsAuthCapabilityError extends Error {
301
+ constructor() {
302
+ super(
303
+ 'Requires `agent-teams auth login` — cookie-based auth (`auth extract`) can only provide a Skype token, not the Microsoft token needed for search or SharePoint/OneDrive file downloads.',
304
+ )
305
+ this.name = 'TeamsAuthCapabilityError'
306
+ }
307
+ }
@@ -107,8 +107,21 @@ export class InstagramAdapter implements PlatformAdapter {
107
107
  if (!code) throw new Error('Verification code is required')
108
108
  const challengeResult = await client.challengeSubmitCode(result.challengePath, code)
109
109
  userId = challengeResult.userId || userId
110
+ } else if (result.oneClickEmailAvailable) {
111
+ const { parseOneClickLoginLink } = await import('@/platforms/instagram/client')
112
+ io.print('Password login was rejected; this account can log in by email. Sending a login email...')
113
+ const { contactPoint } = await client.sendOneClickLoginEmail(username)
114
+ io.print(contactPoint ? `Login email sent to ${contactPoint}.` : 'Login email sent.')
115
+ const link = await io.prompt('Paste the "Login as ..." link from the email')
116
+ if (!link) throw new Error('Login link is required')
117
+ const parsed = parseOneClickLoginLink(link)
118
+ if (!parsed) throw new Error('Could not read uid and token from the pasted link')
119
+ const emailResult = await client.oneClickLogin(parsed.uid, parsed.token)
120
+ userId = emailResult.userId || userId
110
121
  }
111
122
 
123
+ if (!userId) throw new Error('Login did not complete')
124
+
112
125
  const now = new Date().toISOString()
113
126
  await this.credManager.setAccount({
114
127
  account_id: accountId,
@@ -1,4 +1,5 @@
1
1
  import { TeamsClient } from '@/platforms/teams/client'
2
+ import { TeamsListener } from '@/platforms/teams/listener'
2
3
 
3
4
  import type { AuthHint, AuthIO, PlatformAdapter, UnifiedChannel, UnifiedMessage, Workspace } from './types'
4
5
 
@@ -6,6 +7,7 @@ export class TeamsAdapter implements PlatformAdapter {
6
7
  readonly name = 'Teams'
7
8
 
8
9
  private client: TeamsClient | null = null
10
+ private listener: TeamsListener | null = null
9
11
  private teamId: string | null = null
10
12
  private teamName: string | null = null
11
13
  private teams: Workspace[] = []
@@ -49,6 +51,27 @@ export class TeamsAdapter implements PlatformAdapter {
49
51
  await this.client!.sendMessage(this.teamId!, channelId, text)
50
52
  }
51
53
 
54
+ async startListening(onMessage: (msg: UnifiedMessage) => void): Promise<void> {
55
+ this.ensureClient()
56
+ const listener = new TeamsListener(this.client!)
57
+ listener.on('message', (message) => {
58
+ onMessage({
59
+ id: message.id,
60
+ channelId: message.chatId,
61
+ author: message.author.displayName,
62
+ content: message.content,
63
+ timestamp: message.timestamp,
64
+ })
65
+ })
66
+ await listener.start()
67
+ this.listener = listener
68
+ }
69
+
70
+ stopListening(): void {
71
+ this.listener?.stop()
72
+ this.listener = null
73
+ }
74
+
52
75
  async getWorkspaces(): Promise<Workspace[]> {
53
76
  return this.teams
54
77
  }
package/src/tui/app.ts CHANGED
@@ -270,7 +270,7 @@ export async function createApp(): Promise<void> {
270
270
  left: 0,
271
271
  top: 1,
272
272
  width: 28,
273
- height: '100%-4',
273
+ height: '100%-5',
274
274
  border: { type: 'line' },
275
275
  style: {
276
276
  border: { fg: '#444444' },
@@ -288,7 +288,7 @@ export async function createApp(): Promise<void> {
288
288
  left: 28,
289
289
  top: 1,
290
290
  right: 0,
291
- height: '100%-4',
291
+ height: '100%-5',
292
292
  border: { type: 'line' },
293
293
  style: {
294
294
  border: { fg: '#444444' },
@@ -304,7 +304,7 @@ export async function createApp(): Promise<void> {
304
304
 
305
305
  const inputBox = blessed.textarea({
306
306
  parent: screen,
307
- bottom: 0,
307
+ bottom: 1,
308
308
  left: 0,
309
309
  right: 0,
310
310
  height: 3,