agent-messenger 1.0.0 → 1.1.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.
- package/.claude/commands/release.md +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/e2e.yml.disabled +69 -0
- package/README.md +16 -14
- package/biome.json +33 -1
- package/bun.lock +63 -0
- package/dist/package.json +8 -4
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +4 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/platforms/discord/cli.js +1 -1
- package/dist/src/platforms/discord/client.d.ts.map +1 -1
- package/dist/src/platforms/discord/client.js +3 -3
- package/dist/src/platforms/discord/client.js.map +1 -1
- package/dist/src/platforms/discord/commands/user.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/user.js +10 -1
- package/dist/src/platforms/discord/commands/user.js.map +1 -1
- package/dist/src/platforms/discord/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/discord/credential-manager.js +18 -12
- package/dist/src/platforms/discord/credential-manager.js.map +1 -1
- package/dist/src/platforms/slack/cli.js +1 -1
- package/dist/src/platforms/slack/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/slack/credential-manager.js +20 -6
- package/dist/src/platforms/slack/credential-manager.js.map +1 -1
- package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/slack/token-extractor.js +34 -9
- package/dist/src/platforms/slack/token-extractor.js.map +1 -1
- package/dist/src/platforms/teams/cli.d.ts.map +1 -0
- package/dist/{cli.js → src/platforms/teams/cli.js} +11 -10
- package/dist/src/platforms/teams/cli.js.map +1 -0
- package/dist/src/platforms/teams/client.d.ts +32 -0
- package/dist/src/platforms/teams/client.d.ts.map +1 -0
- package/dist/src/platforms/teams/client.js +202 -0
- package/dist/src/platforms/teams/client.js.map +1 -0
- package/dist/src/platforms/teams/commands/auth.d.ts +14 -0
- package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -0
- package/dist/src/platforms/teams/commands/auth.js +176 -0
- package/dist/src/platforms/teams/commands/auth.js.map +1 -0
- package/dist/src/platforms/teams/commands/channel.d.ts +13 -0
- package/dist/src/platforms/teams/commands/channel.d.ts.map +1 -0
- package/dist/src/platforms/teams/commands/channel.js +97 -0
- package/dist/src/platforms/teams/commands/channel.js.map +1 -0
- package/dist/src/platforms/teams/commands/file.d.ts +12 -0
- package/dist/src/platforms/teams/commands/file.d.ts.map +1 -0
- package/dist/src/platforms/teams/commands/file.js +104 -0
- package/dist/src/platforms/teams/commands/file.js.map +1 -0
- package/dist/{commands → src/platforms/teams/commands}/index.d.ts +5 -2
- package/dist/src/platforms/teams/commands/index.d.ts.map +1 -0
- package/dist/{commands → src/platforms/teams/commands}/index.js +5 -2
- package/dist/src/platforms/teams/commands/index.js.map +1 -0
- package/dist/src/platforms/teams/commands/message.d.ts +17 -0
- package/dist/src/platforms/teams/commands/message.d.ts.map +1 -0
- package/dist/src/platforms/teams/commands/message.js +133 -0
- package/dist/src/platforms/teams/commands/message.js.map +1 -0
- package/dist/src/platforms/teams/commands/reaction.d.ts +9 -0
- package/dist/src/platforms/teams/commands/reaction.d.ts.map +1 -0
- package/dist/src/platforms/teams/commands/reaction.js +68 -0
- package/dist/src/platforms/teams/commands/reaction.js.map +1 -0
- package/dist/src/platforms/teams/commands/snapshot.d.ts +10 -0
- package/dist/src/platforms/teams/commands/snapshot.d.ts.map +1 -0
- package/dist/src/platforms/teams/commands/snapshot.js +85 -0
- package/dist/src/platforms/teams/commands/snapshot.js.map +1 -0
- package/dist/src/platforms/teams/commands/team.d.ts +18 -0
- package/dist/src/platforms/teams/commands/team.d.ts.map +1 -0
- package/dist/src/platforms/teams/commands/team.js +130 -0
- package/dist/src/platforms/teams/commands/team.js.map +1 -0
- package/dist/src/platforms/teams/commands/user.d.ts.map +1 -0
- package/dist/src/platforms/teams/commands/user.js +88 -0
- package/dist/src/platforms/teams/commands/user.js.map +1 -0
- package/dist/src/platforms/teams/credential-manager.d.ts +18 -0
- package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -0
- package/dist/src/platforms/teams/credential-manager.js +81 -0
- package/dist/src/platforms/teams/credential-manager.js.map +1 -0
- package/dist/src/platforms/teams/index.d.ts +4 -0
- package/dist/src/platforms/teams/index.d.ts.map +1 -0
- package/dist/src/platforms/teams/index.js +6 -0
- package/dist/src/platforms/teams/index.js.map +1 -0
- package/dist/src/platforms/teams/token-extractor.d.ts +36 -0
- package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -0
- package/dist/src/platforms/teams/token-extractor.js +335 -0
- package/dist/src/platforms/teams/token-extractor.js.map +1 -0
- package/dist/src/platforms/teams/types.d.ts +209 -0
- package/dist/src/platforms/teams/types.d.ts.map +1 -0
- package/dist/src/platforms/teams/types.js +65 -0
- package/dist/src/platforms/teams/types.js.map +1 -0
- package/docs/teams.md +321 -0
- package/e2e/README.md +256 -0
- package/e2e/config.ts +45 -0
- package/e2e/discord.e2e.test.ts +252 -0
- package/e2e/helpers.ts +107 -0
- package/e2e/slack.e2e.test.ts +309 -0
- package/package.json +8 -4
- package/scripts/postbuild.ts +15 -0
- package/skills/agent-teams/SKILL.md +292 -0
- package/skills/agent-teams/references/authentication.md +375 -0
- package/skills/agent-teams/references/common-patterns.md +596 -0
- package/skills/agent-teams/templates/monitor-channel.sh +239 -0
- package/skills/agent-teams/templates/post-message.sh +224 -0
- package/skills/agent-teams/templates/team-summary.sh +210 -0
- package/src/cli.ts +4 -0
- package/src/platforms/discord/client.ts +3 -3
- package/src/platforms/discord/commands/auth.test.ts +48 -32
- package/src/platforms/discord/commands/channel.test.ts +54 -42
- package/src/platforms/discord/commands/file.test.ts +40 -53
- package/src/platforms/discord/commands/guild.test.ts +47 -27
- package/src/platforms/discord/commands/message.test.ts +54 -51
- package/src/platforms/discord/commands/reaction.test.ts +54 -42
- package/src/platforms/discord/commands/user.ts +12 -1
- package/src/platforms/discord/credential-manager.test.ts +137 -136
- package/src/platforms/discord/credential-manager.ts +20 -13
- package/src/platforms/discord/token-extractor.test.ts +133 -383
- package/{tests → src/platforms/slack}/cli.test.ts +3 -3
- package/{tests/slack-client.test.ts → src/platforms/slack/client.test.ts} +1 -1
- package/{tests → src/platforms/slack}/commands/auth.test.ts +25 -13
- package/{tests → src/platforms/slack}/commands/channel.test.ts +2 -2
- package/{tests → src/platforms/slack}/commands/file.test.ts +2 -2
- package/{tests → src/platforms/slack}/commands/message.test.ts +2 -2
- package/{tests → src/platforms/slack}/commands/reaction.test.ts +1 -1
- package/{tests → src/platforms/slack}/commands/snapshot.test.ts +117 -105
- package/{tests → src/platforms/slack}/commands/user.test.ts +3 -3
- package/{tests → src/platforms/slack}/commands/workspace.test.ts +44 -95
- package/{tests → src/platforms/slack}/credential-manager.test.ts +2 -2
- package/src/platforms/slack/credential-manager.ts +22 -7
- package/src/platforms/slack/token-extractor-node-test.ts +40 -0
- package/src/platforms/slack/token-extractor-node.test.ts +10 -0
- package/src/platforms/slack/token-extractor.ts +36 -10
- package/{tests → src/platforms/slack}/types.test.ts +1 -1
- package/src/platforms/teams/cli.ts +36 -0
- package/src/platforms/teams/client.test.ts +500 -0
- package/src/platforms/teams/client.ts +365 -0
- package/src/platforms/teams/commands/auth.test.ts +99 -0
- package/src/platforms/teams/commands/auth.ts +232 -0
- package/src/platforms/teams/commands/channel.test.ts +147 -0
- package/src/platforms/teams/commands/channel.ts +129 -0
- package/src/platforms/teams/commands/file.test.ts +88 -0
- package/src/platforms/teams/commands/file.ts +144 -0
- package/src/platforms/teams/commands/index.ts +12 -0
- package/src/platforms/teams/commands/message.test.ts +110 -0
- package/src/platforms/teams/commands/message.ts +188 -0
- package/src/platforms/teams/commands/reaction.test.ts +87 -0
- package/src/platforms/teams/commands/reaction.ts +104 -0
- package/src/platforms/teams/commands/snapshot.test.ts +35 -0
- package/src/platforms/teams/commands/snapshot.ts +115 -0
- package/src/platforms/teams/commands/team.test.ts +157 -0
- package/src/platforms/teams/commands/team.ts +164 -0
- package/src/platforms/teams/commands/user.test.ts +83 -0
- package/src/platforms/teams/commands/user.ts +112 -0
- package/src/platforms/teams/credential-manager.test.ts +178 -0
- package/src/platforms/teams/credential-manager.ts +92 -0
- package/src/platforms/teams/index.ts +5 -0
- package/src/platforms/teams/token-extractor.test.ts +429 -0
- package/src/platforms/teams/token-extractor.ts +462 -0
- package/src/platforms/teams/types.test.ts +226 -0
- package/src/platforms/teams/types.ts +140 -0
- package/tsconfig.json +1 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands/auth.d.ts +0 -3
- package/dist/commands/auth.d.ts.map +0 -1
- package/dist/commands/auth.js +0 -140
- package/dist/commands/auth.js.map +0 -1
- package/dist/commands/channel.d.ts +0 -3
- package/dist/commands/channel.d.ts.map +0 -1
- package/dist/commands/channel.js +0 -118
- package/dist/commands/channel.js.map +0 -1
- package/dist/commands/file.d.ts +0 -3
- package/dist/commands/file.d.ts.map +0 -1
- package/dist/commands/file.js +0 -113
- package/dist/commands/file.js.map +0 -1
- package/dist/commands/index.d.ts.map +0 -1
- package/dist/commands/index.js.map +0 -1
- package/dist/commands/message.d.ts +0 -3
- package/dist/commands/message.d.ts.map +0 -1
- package/dist/commands/message.js +0 -214
- package/dist/commands/message.js.map +0 -1
- package/dist/commands/reaction.d.ts +0 -3
- package/dist/commands/reaction.d.ts.map +0 -1
- package/dist/commands/reaction.js +0 -100
- package/dist/commands/reaction.js.map +0 -1
- package/dist/commands/snapshot.d.ts +0 -3
- package/dist/commands/snapshot.d.ts.map +0 -1
- package/dist/commands/snapshot.js +0 -88
- package/dist/commands/snapshot.js.map +0 -1
- package/dist/commands/user.d.ts.map +0 -1
- package/dist/commands/user.js +0 -96
- package/dist/commands/user.js.map +0 -1
- package/dist/commands/workspace.d.ts +0 -3
- package/dist/commands/workspace.d.ts.map +0 -1
- package/dist/commands/workspace.js +0 -89
- package/dist/commands/workspace.js.map +0 -1
- package/dist/lib/credential-manager.d.ts +0 -13
- package/dist/lib/credential-manager.d.ts.map +0 -1
- package/dist/lib/credential-manager.js +0 -58
- package/dist/lib/credential-manager.js.map +0 -1
- package/dist/lib/index.d.ts +0 -3
- package/dist/lib/index.d.ts.map +0 -1
- package/dist/lib/index.js +0 -3
- package/dist/lib/index.js.map +0 -1
- package/dist/lib/ref-manager.d.ts +0 -26
- package/dist/lib/ref-manager.d.ts.map +0 -1
- package/dist/lib/ref-manager.js +0 -92
- package/dist/lib/ref-manager.js.map +0 -1
- package/dist/lib/slack-client.d.ts +0 -37
- package/dist/lib/slack-client.d.ts.map +0 -1
- package/dist/lib/slack-client.js +0 -379
- package/dist/lib/slack-client.js.map +0 -1
- package/dist/lib/token-extractor.d.ts +0 -28
- package/dist/lib/token-extractor.d.ts.map +0 -1
- package/dist/lib/token-extractor.js +0 -401
- package/dist/lib/token-extractor.js.map +0 -1
- package/dist/src/platforms/discord/client.test.d.ts +0 -2
- package/dist/src/platforms/discord/client.test.d.ts.map +0 -1
- package/dist/src/platforms/discord/client.test.js +0 -367
- package/dist/src/platforms/discord/client.test.js.map +0 -1
- package/dist/src/platforms/discord/commands/auth.test.d.ts +0 -2
- package/dist/src/platforms/discord/commands/auth.test.d.ts.map +0 -1
- package/dist/src/platforms/discord/commands/auth.test.js +0 -65
- package/dist/src/platforms/discord/commands/auth.test.js.map +0 -1
- package/dist/src/platforms/discord/commands/channel.test.d.ts +0 -2
- package/dist/src/platforms/discord/commands/channel.test.d.ts.map +0 -1
- package/dist/src/platforms/discord/commands/channel.test.js +0 -136
- package/dist/src/platforms/discord/commands/channel.test.js.map +0 -1
- package/dist/src/platforms/discord/commands/file.test.d.ts +0 -2
- package/dist/src/platforms/discord/commands/file.test.d.ts.map +0 -1
- package/dist/src/platforms/discord/commands/file.test.js +0 -83
- package/dist/src/platforms/discord/commands/file.test.js.map +0 -1
- package/dist/src/platforms/discord/commands/guild.test.d.ts +0 -2
- package/dist/src/platforms/discord/commands/guild.test.d.ts.map +0 -1
- package/dist/src/platforms/discord/commands/guild.test.js +0 -100
- package/dist/src/platforms/discord/commands/guild.test.js.map +0 -1
- package/dist/src/platforms/discord/commands/message.test.d.ts +0 -2
- package/dist/src/platforms/discord/commands/message.test.d.ts.map +0 -1
- package/dist/src/platforms/discord/commands/message.test.js +0 -91
- package/dist/src/platforms/discord/commands/message.test.js.map +0 -1
- package/dist/src/platforms/discord/commands/reaction.test.d.ts +0 -2
- package/dist/src/platforms/discord/commands/reaction.test.d.ts.map +0 -1
- package/dist/src/platforms/discord/commands/reaction.test.js +0 -115
- package/dist/src/platforms/discord/commands/reaction.test.js.map +0 -1
- package/dist/src/platforms/discord/commands/snapshot.test.d.ts +0 -2
- package/dist/src/platforms/discord/commands/snapshot.test.d.ts.map +0 -1
- package/dist/src/platforms/discord/commands/snapshot.test.js +0 -25
- package/dist/src/platforms/discord/commands/snapshot.test.js.map +0 -1
- package/dist/src/platforms/discord/commands/user.test.d.ts +0 -2
- package/dist/src/platforms/discord/commands/user.test.d.ts.map +0 -1
- package/dist/src/platforms/discord/commands/user.test.js +0 -103
- package/dist/src/platforms/discord/commands/user.test.js.map +0 -1
- package/dist/src/platforms/discord/credential-manager.test.d.ts +0 -2
- package/dist/src/platforms/discord/credential-manager.test.d.ts.map +0 -1
- package/dist/src/platforms/discord/credential-manager.test.js +0 -136
- package/dist/src/platforms/discord/credential-manager.test.js.map +0 -1
- package/dist/src/platforms/discord/token-extractor.test.d.ts +0 -2
- package/dist/src/platforms/discord/token-extractor.test.d.ts.map +0 -1
- package/dist/src/platforms/discord/token-extractor.test.js +0 -789
- package/dist/src/platforms/discord/token-extractor.test.js.map +0 -1
- package/dist/src/platforms/discord/types.test.d.ts +0 -2
- package/dist/src/platforms/discord/types.test.d.ts.map +0 -1
- package/dist/src/platforms/discord/types.test.js +0 -211
- package/dist/src/platforms/discord/types.test.js.map +0 -1
- package/dist/src/shared/utils/concurrency.test.d.ts +0 -2
- package/dist/src/shared/utils/concurrency.test.d.ts.map +0 -1
- package/dist/src/shared/utils/concurrency.test.js +0 -39
- package/dist/src/shared/utils/concurrency.test.js.map +0 -1
- package/dist/tests/cli.test.d.ts +0 -2
- package/dist/tests/cli.test.d.ts.map +0 -1
- package/dist/tests/cli.test.js +0 -83
- package/dist/tests/cli.test.js.map +0 -1
- package/dist/tests/commands/.test-slack-data/Local Storage/leveldb/CURRENT +0 -1
- package/dist/tests/commands/.test-slack-data/Local Storage/leveldb/LOCK +0 -0
- package/dist/tests/commands/.test-slack-data/Local Storage/leveldb/LOG +0 -3
- package/dist/tests/commands/.test-slack-data/Local Storage/leveldb/LOG.old +0 -1
- package/dist/tests/commands/.test-slack-data/Local Storage/leveldb/MANIFEST-000004 +0 -0
- package/dist/tests/commands/auth.test.d.ts +0 -2
- package/dist/tests/commands/auth.test.d.ts.map +0 -1
- package/dist/tests/commands/auth.test.js +0 -304
- package/dist/tests/commands/auth.test.js.map +0 -1
- package/dist/tests/commands/channel.test.d.ts +0 -2
- package/dist/tests/commands/channel.test.d.ts.map +0 -1
- package/dist/tests/commands/channel.test.js +0 -166
- package/dist/tests/commands/channel.test.js.map +0 -1
- package/dist/tests/commands/file.test.d.ts +0 -2
- package/dist/tests/commands/file.test.d.ts.map +0 -1
- package/dist/tests/commands/file.test.js +0 -175
- package/dist/tests/commands/file.test.js.map +0 -1
- package/dist/tests/commands/message.test.d.ts +0 -2
- package/dist/tests/commands/message.test.d.ts.map +0 -1
- package/dist/tests/commands/message.test.js +0 -293
- package/dist/tests/commands/message.test.js.map +0 -1
- package/dist/tests/commands/reaction.test.d.ts +0 -2
- package/dist/tests/commands/reaction.test.d.ts.map +0 -1
- package/dist/tests/commands/reaction.test.js +0 -84
- package/dist/tests/commands/reaction.test.js.map +0 -1
- package/dist/tests/commands/snapshot.test.d.ts +0 -2
- package/dist/tests/commands/snapshot.test.d.ts.map +0 -1
- package/dist/tests/commands/snapshot.test.js +0 -280
- package/dist/tests/commands/snapshot.test.js.map +0 -1
- package/dist/tests/commands/user.test.d.ts +0 -2
- package/dist/tests/commands/user.test.d.ts.map +0 -1
- package/dist/tests/commands/user.test.js +0 -117
- package/dist/tests/commands/user.test.js.map +0 -1
- package/dist/tests/commands/workspace.test.d.ts +0 -2
- package/dist/tests/commands/workspace.test.d.ts.map +0 -1
- package/dist/tests/commands/workspace.test.js +0 -453
- package/dist/tests/commands/workspace.test.js.map +0 -1
- package/dist/tests/credential-manager.test.d.ts +0 -2
- package/dist/tests/credential-manager.test.d.ts.map +0 -1
- package/dist/tests/credential-manager.test.js +0 -199
- package/dist/tests/credential-manager.test.js.map +0 -1
- package/dist/tests/slack-client.test.d.ts +0 -2
- package/dist/tests/slack-client.test.d.ts.map +0 -1
- package/dist/tests/slack-client.test.js +0 -741
- package/dist/tests/slack-client.test.js.map +0 -1
- package/dist/tests/types.test.d.ts +0 -2
- package/dist/tests/types.test.d.ts.map +0 -1
- package/dist/tests/types.test.js +0 -215
- package/dist/tests/types.test.js.map +0 -1
- package/dist/types/index.d.ts +0 -369
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -92
- package/dist/types/index.js.map +0 -1
- package/dist/utils/error-handler.d.ts +0 -2
- package/dist/utils/error-handler.d.ts.map +0 -1
- package/dist/utils/error-handler.js +0 -5
- package/dist/utils/error-handler.js.map +0 -1
- package/dist/utils/output.d.ts +0 -2
- package/dist/utils/output.d.ts.map +0 -1
- package/dist/utils/output.js +0 -4
- package/dist/utils/output.js.map +0 -1
- /package/dist/{cli.d.ts → src/platforms/teams/cli.d.ts} +0 -0
- /package/dist/{commands → src/platforms/teams/commands}/user.d.ts +0 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { basename } from 'node:path'
|
|
3
|
+
import type { TeamsChannel, TeamsFile, TeamsMessage, TeamsTeam, TeamsUser } from './types'
|
|
4
|
+
import { TeamsError } from './types'
|
|
5
|
+
|
|
6
|
+
interface RateLimitBucket {
|
|
7
|
+
remaining: number
|
|
8
|
+
resetAt: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const MSG_API_BASE = 'https://emea.ng.msg.teams.microsoft.com/v1'
|
|
12
|
+
const CSA_API_BASE = 'https://teams.microsoft.com/api'
|
|
13
|
+
const MAX_RETRIES = 3
|
|
14
|
+
const BASE_BACKOFF_MS = 100
|
|
15
|
+
|
|
16
|
+
export class TeamsClient {
|
|
17
|
+
private token: string
|
|
18
|
+
private tokenExpiresAt?: Date
|
|
19
|
+
private buckets: Map<string, RateLimitBucket> = new Map()
|
|
20
|
+
private globalRateLimitUntil: number = 0
|
|
21
|
+
|
|
22
|
+
constructor(token: string, tokenExpiresAt?: string) {
|
|
23
|
+
if (!token) {
|
|
24
|
+
throw new TeamsError('Token is required', 'missing_token')
|
|
25
|
+
}
|
|
26
|
+
this.token = token
|
|
27
|
+
if (tokenExpiresAt) {
|
|
28
|
+
this.tokenExpiresAt = new Date(tokenExpiresAt)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private isTokenExpired(): boolean {
|
|
33
|
+
if (!this.tokenExpiresAt) {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
return this.tokenExpiresAt.getTime() < Date.now()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private getBucketKey(method: string, path: string): string {
|
|
40
|
+
const normalized = path
|
|
41
|
+
.replace(/\/teams\/[^/]+/, '/teams/{team_id}')
|
|
42
|
+
.replace(/\/channels\/[^/]+/, '/channels/{channel_id}')
|
|
43
|
+
.replace(/\/messages\/[^/]+/, '/messages/{message_id}')
|
|
44
|
+
.replace(/\/users\/[^/]+/, '/users/{user_id}')
|
|
45
|
+
.replace(/\/members\/[^/]+/, '/members/{member_id}')
|
|
46
|
+
return `${method}:${normalized}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private async waitForRateLimit(bucketKey: string): Promise<void> {
|
|
50
|
+
const now = Date.now()
|
|
51
|
+
|
|
52
|
+
if (this.globalRateLimitUntil > now) {
|
|
53
|
+
await this.sleep(this.globalRateLimitUntil - now)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const bucket = this.buckets.get(bucketKey)
|
|
57
|
+
if (bucket && bucket.remaining === 0 && bucket.resetAt * 1000 > now) {
|
|
58
|
+
await this.sleep(bucket.resetAt * 1000 - now)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private updateBucket(bucketKey: string, response: Response): void {
|
|
63
|
+
const remaining = response.headers.get('X-RateLimit-Remaining')
|
|
64
|
+
const reset = response.headers.get('X-RateLimit-Reset')
|
|
65
|
+
|
|
66
|
+
if (remaining !== null && reset !== null) {
|
|
67
|
+
this.buckets.set(bucketKey, {
|
|
68
|
+
remaining: parseInt(remaining, 10),
|
|
69
|
+
resetAt: parseFloat(reset),
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async handleRateLimitResponse(response: Response): Promise<number> {
|
|
75
|
+
const retryAfter = response.headers.get('Retry-After')
|
|
76
|
+
const waitMs = parseFloat(retryAfter || '1') * 1000
|
|
77
|
+
|
|
78
|
+
this.globalRateLimitUntil = Date.now() + waitMs
|
|
79
|
+
await this.sleep(waitMs)
|
|
80
|
+
return waitMs
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private sleep(ms: number): Promise<void> {
|
|
84
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private async request<T>(
|
|
88
|
+
method: string,
|
|
89
|
+
path: string,
|
|
90
|
+
body?: unknown,
|
|
91
|
+
baseUrl: string = MSG_API_BASE
|
|
92
|
+
): Promise<T> {
|
|
93
|
+
if (this.isTokenExpired()) {
|
|
94
|
+
throw new TeamsError('Token has expired', 'token_expired')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const url = `${baseUrl}${path}`
|
|
98
|
+
const bucketKey = this.getBucketKey(method, path)
|
|
99
|
+
|
|
100
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
101
|
+
await this.waitForRateLimit(bucketKey)
|
|
102
|
+
|
|
103
|
+
const headers: Record<string, string> = {
|
|
104
|
+
'X-Skypetoken': this.token,
|
|
105
|
+
'Content-Type': 'application/json',
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const options: RequestInit = {
|
|
109
|
+
method,
|
|
110
|
+
headers,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (body !== undefined) {
|
|
114
|
+
options.body = JSON.stringify(body)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const response = await fetch(url, options)
|
|
118
|
+
this.updateBucket(bucketKey, response)
|
|
119
|
+
|
|
120
|
+
if (response.status === 429) {
|
|
121
|
+
if (attempt < MAX_RETRIES) {
|
|
122
|
+
await this.handleRateLimitResponse(response)
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
const errorBody = (await response.json().catch(() => null)) as {
|
|
126
|
+
message?: string
|
|
127
|
+
} | null
|
|
128
|
+
throw new TeamsError(errorBody?.message ?? 'Rate limited', 'rate_limited')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (response.status >= 500 && attempt < MAX_RETRIES) {
|
|
132
|
+
await this.sleep(BASE_BACKOFF_MS * 2 ** attempt)
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
const errorBody = (await response.json().catch(() => null)) as {
|
|
138
|
+
message?: string
|
|
139
|
+
code?: string | number
|
|
140
|
+
} | null
|
|
141
|
+
throw new TeamsError(
|
|
142
|
+
errorBody?.message ?? `HTTP ${response.status}`,
|
|
143
|
+
errorBody?.code?.toString() ?? `http_${response.status}`
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (response.status === 204) {
|
|
148
|
+
return undefined as T
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return response.json() as Promise<T>
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
throw new TeamsError('Request failed after retries', 'max_retries')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private async requestFormData<T>(
|
|
158
|
+
path: string,
|
|
159
|
+
formData: FormData,
|
|
160
|
+
baseUrl: string = MSG_API_BASE
|
|
161
|
+
): Promise<T> {
|
|
162
|
+
if (this.isTokenExpired()) {
|
|
163
|
+
throw new TeamsError('Token has expired', 'token_expired')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const url = `${baseUrl}${path}`
|
|
167
|
+
const bucketKey = this.getBucketKey('POST', path)
|
|
168
|
+
|
|
169
|
+
await this.waitForRateLimit(bucketKey)
|
|
170
|
+
|
|
171
|
+
const response = await fetch(url, {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: {
|
|
174
|
+
'X-Skypetoken': this.token,
|
|
175
|
+
},
|
|
176
|
+
body: formData,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
this.updateBucket(bucketKey, response)
|
|
180
|
+
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
const errorBody = (await response.json().catch(() => null)) as {
|
|
183
|
+
message?: string
|
|
184
|
+
code?: string | number
|
|
185
|
+
} | null
|
|
186
|
+
throw new TeamsError(
|
|
187
|
+
errorBody?.message ?? `HTTP ${response.status}`,
|
|
188
|
+
errorBody?.code?.toString() ?? `http_${response.status}`
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return response.json() as Promise<T>
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async testAuth(): Promise<TeamsUser> {
|
|
196
|
+
interface UserProperties {
|
|
197
|
+
userDetails?: string
|
|
198
|
+
locale?: string
|
|
199
|
+
}
|
|
200
|
+
const props = await this.request<UserProperties>('GET', '/users/ME/properties')
|
|
201
|
+
const userDetails = props.userDetails ? JSON.parse(props.userDetails) : {}
|
|
202
|
+
return {
|
|
203
|
+
id: 'ME',
|
|
204
|
+
displayName: userDetails.name || 'Teams User',
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async listTeams(): Promise<TeamsTeam[]> {
|
|
209
|
+
interface Conversation {
|
|
210
|
+
id: string
|
|
211
|
+
threadProperties?: {
|
|
212
|
+
groupId?: string
|
|
213
|
+
spaceThreadTopic?: string
|
|
214
|
+
productThreadType?: string
|
|
215
|
+
threadType?: string
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
interface ConversationsResponse {
|
|
219
|
+
conversations: Conversation[]
|
|
220
|
+
}
|
|
221
|
+
const data = await this.request<ConversationsResponse>('GET', '/users/ME/conversations')
|
|
222
|
+
|
|
223
|
+
const teamsMap = new Map<string, TeamsTeam>()
|
|
224
|
+
for (const conv of data.conversations) {
|
|
225
|
+
const tp = conv.threadProperties
|
|
226
|
+
if (!tp?.groupId) continue
|
|
227
|
+
if (!tp.productThreadType?.includes('Teams') && tp.threadType !== 'space') continue
|
|
228
|
+
|
|
229
|
+
if (!teamsMap.has(tp.groupId)) {
|
|
230
|
+
teamsMap.set(tp.groupId, {
|
|
231
|
+
id: tp.groupId,
|
|
232
|
+
name: tp.spaceThreadTopic || 'Unknown Team',
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return Array.from(teamsMap.values())
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async getTeam(teamId: string): Promise<TeamsTeam> {
|
|
241
|
+
return this.request<TeamsTeam>('GET', `/csa/api/v1/teams/${teamId}`, undefined, CSA_API_BASE)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async listChannels(teamId: string): Promise<TeamsChannel[]> {
|
|
245
|
+
return this.request<TeamsChannel[]>(
|
|
246
|
+
'GET',
|
|
247
|
+
`/csa/api/v1/teams/${teamId}/channels`,
|
|
248
|
+
undefined,
|
|
249
|
+
CSA_API_BASE
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async getChannel(teamId: string, channelId: string): Promise<TeamsChannel> {
|
|
254
|
+
return this.request<TeamsChannel>(
|
|
255
|
+
'GET',
|
|
256
|
+
`/csa/api/v1/teams/${teamId}/channels/${channelId}`,
|
|
257
|
+
undefined,
|
|
258
|
+
CSA_API_BASE
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async sendMessage(teamId: string, channelId: string, content: string): Promise<TeamsMessage> {
|
|
263
|
+
return this.request<TeamsMessage>(
|
|
264
|
+
'POST',
|
|
265
|
+
`/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/messages`,
|
|
266
|
+
{ content },
|
|
267
|
+
CSA_API_BASE
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async getMessages(
|
|
272
|
+
teamId: string,
|
|
273
|
+
channelId: string,
|
|
274
|
+
limit: number = 50
|
|
275
|
+
): Promise<TeamsMessage[]> {
|
|
276
|
+
return this.request<TeamsMessage[]>(
|
|
277
|
+
'GET',
|
|
278
|
+
`/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/messages?limit=${limit}`,
|
|
279
|
+
undefined,
|
|
280
|
+
CSA_API_BASE
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async getMessage(teamId: string, channelId: string, messageId: string): Promise<TeamsMessage> {
|
|
285
|
+
return this.request<TeamsMessage>(
|
|
286
|
+
'GET',
|
|
287
|
+
`/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/messages/${messageId}`,
|
|
288
|
+
undefined,
|
|
289
|
+
CSA_API_BASE
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async deleteMessage(teamId: string, channelId: string, messageId: string): Promise<void> {
|
|
294
|
+
return this.request<void>(
|
|
295
|
+
'DELETE',
|
|
296
|
+
`/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/messages/${messageId}`,
|
|
297
|
+
undefined,
|
|
298
|
+
CSA_API_BASE
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async addReaction(
|
|
303
|
+
teamId: string,
|
|
304
|
+
channelId: string,
|
|
305
|
+
messageId: string,
|
|
306
|
+
emoji: string
|
|
307
|
+
): Promise<void> {
|
|
308
|
+
return this.request<void>(
|
|
309
|
+
'POST',
|
|
310
|
+
`/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/messages/${messageId}/reactions`,
|
|
311
|
+
{ emoji },
|
|
312
|
+
CSA_API_BASE
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async removeReaction(
|
|
317
|
+
teamId: string,
|
|
318
|
+
channelId: string,
|
|
319
|
+
messageId: string,
|
|
320
|
+
emoji: string
|
|
321
|
+
): Promise<void> {
|
|
322
|
+
return this.request<void>(
|
|
323
|
+
'DELETE',
|
|
324
|
+
`/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/messages/${messageId}/reactions/${emoji}`,
|
|
325
|
+
undefined,
|
|
326
|
+
CSA_API_BASE
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async listUsers(teamId: string): Promise<TeamsUser[]> {
|
|
331
|
+
return this.request<TeamsUser[]>(
|
|
332
|
+
'GET',
|
|
333
|
+
`/csa/api/v1/teams/${teamId}/members`,
|
|
334
|
+
undefined,
|
|
335
|
+
CSA_API_BASE
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async getUser(userId: string): Promise<TeamsUser> {
|
|
340
|
+
return this.request<TeamsUser>('GET', `/csa/api/v1/users/${userId}`, undefined, CSA_API_BASE)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async uploadFile(teamId: string, channelId: string, filePath: string): Promise<TeamsFile> {
|
|
344
|
+
const fileBuffer = await readFile(filePath)
|
|
345
|
+
const filename = basename(filePath) || 'file'
|
|
346
|
+
|
|
347
|
+
const formData = new FormData()
|
|
348
|
+
formData.append('file', new Blob([fileBuffer]), filename)
|
|
349
|
+
|
|
350
|
+
return this.requestFormData<TeamsFile>(
|
|
351
|
+
`/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/files`,
|
|
352
|
+
formData,
|
|
353
|
+
CSA_API_BASE
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async listFiles(teamId: string, channelId: string): Promise<TeamsFile[]> {
|
|
358
|
+
return this.request<TeamsFile[]>(
|
|
359
|
+
'GET',
|
|
360
|
+
`/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/files`,
|
|
361
|
+
undefined,
|
|
362
|
+
CSA_API_BASE
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, spyOn, test } from 'bun:test'
|
|
2
|
+
import { TeamsClient } from '../client'
|
|
3
|
+
import { TeamsCredentialManager } from '../credential-manager'
|
|
4
|
+
import { TeamsTokenExtractor } from '../token-extractor'
|
|
5
|
+
|
|
6
|
+
let extractorExtractSpy: ReturnType<typeof spyOn>
|
|
7
|
+
let clientTestAuthSpy: ReturnType<typeof spyOn>
|
|
8
|
+
let clientListTeamsSpy: ReturnType<typeof spyOn>
|
|
9
|
+
let credManagerLoadConfigSpy: ReturnType<typeof spyOn>
|
|
10
|
+
let credManagerSaveConfigSpy: ReturnType<typeof spyOn>
|
|
11
|
+
let credManagerClearCredentialsSpy: ReturnType<typeof spyOn>
|
|
12
|
+
let credManagerIsTokenExpiredSpy: ReturnType<typeof spyOn>
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
extractorExtractSpy = spyOn(TeamsTokenExtractor.prototype, 'extract').mockResolvedValue({
|
|
16
|
+
token: 'test-skype-token-123',
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
clientTestAuthSpy = spyOn(TeamsClient.prototype, 'testAuth').mockResolvedValue({
|
|
20
|
+
id: 'user-123',
|
|
21
|
+
displayName: 'Test User',
|
|
22
|
+
email: 'test@example.com',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
clientListTeamsSpy = spyOn(TeamsClient.prototype, 'listTeams').mockResolvedValue([
|
|
26
|
+
{ id: 'team-1', name: 'Team One' },
|
|
27
|
+
{ id: 'team-2', name: 'Team Two' },
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
credManagerLoadConfigSpy = spyOn(
|
|
31
|
+
TeamsCredentialManager.prototype,
|
|
32
|
+
'loadConfig'
|
|
33
|
+
).mockResolvedValue(null)
|
|
34
|
+
|
|
35
|
+
credManagerSaveConfigSpy = spyOn(
|
|
36
|
+
TeamsCredentialManager.prototype,
|
|
37
|
+
'saveConfig'
|
|
38
|
+
).mockResolvedValue(undefined)
|
|
39
|
+
|
|
40
|
+
credManagerClearCredentialsSpy = spyOn(
|
|
41
|
+
TeamsCredentialManager.prototype,
|
|
42
|
+
'clearCredentials'
|
|
43
|
+
).mockResolvedValue(undefined)
|
|
44
|
+
|
|
45
|
+
credManagerIsTokenExpiredSpy = spyOn(
|
|
46
|
+
TeamsCredentialManager.prototype,
|
|
47
|
+
'isTokenExpired'
|
|
48
|
+
).mockResolvedValue(false)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
extractorExtractSpy?.mockRestore()
|
|
53
|
+
clientTestAuthSpy?.mockRestore()
|
|
54
|
+
clientListTeamsSpy?.mockRestore()
|
|
55
|
+
credManagerLoadConfigSpy?.mockRestore()
|
|
56
|
+
credManagerSaveConfigSpy?.mockRestore()
|
|
57
|
+
credManagerClearCredentialsSpy?.mockRestore()
|
|
58
|
+
credManagerIsTokenExpiredSpy?.mockRestore()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('extract: calls TeamsTokenExtractor', async () => {
|
|
62
|
+
const extractor = new TeamsTokenExtractor()
|
|
63
|
+
const result = await extractor.extract()
|
|
64
|
+
expect(result).toBeDefined()
|
|
65
|
+
expect(result?.token).toBe('test-skype-token-123')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('extract: validates token with TeamsClient', async () => {
|
|
69
|
+
const client = new TeamsClient('test-skype-token-123')
|
|
70
|
+
const authInfo = await client.testAuth()
|
|
71
|
+
expect(authInfo).toBeDefined()
|
|
72
|
+
expect(authInfo.id).toBe('user-123')
|
|
73
|
+
expect(authInfo.displayName).toBe('Test User')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('extract: discovers teams', async () => {
|
|
77
|
+
const client = new TeamsClient('test-skype-token-123')
|
|
78
|
+
const teams = await client.listTeams()
|
|
79
|
+
expect(teams).toHaveLength(2)
|
|
80
|
+
expect(teams[0].id).toBe('team-1')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('logout: clears credentials', async () => {
|
|
84
|
+
const credManager = new TeamsCredentialManager()
|
|
85
|
+
await credManager.clearCredentials()
|
|
86
|
+
expect(credManager.clearCredentials).toHaveBeenCalled()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('status: returns auth state when not authenticated', async () => {
|
|
90
|
+
const credManager = new TeamsCredentialManager()
|
|
91
|
+
const config = await credManager.loadConfig()
|
|
92
|
+
expect(config).toBeNull()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('status: checks token expiry', async () => {
|
|
96
|
+
const credManager = new TeamsCredentialManager()
|
|
97
|
+
const isExpired = await credManager.isTokenExpired()
|
|
98
|
+
expect(isExpired).toBe(false)
|
|
99
|
+
})
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { handleError } from '../../../shared/utils/error-handler'
|
|
3
|
+
import { formatOutput } from '../../../shared/utils/output'
|
|
4
|
+
import { TeamsClient } from '../client'
|
|
5
|
+
import { TeamsCredentialManager } from '../credential-manager'
|
|
6
|
+
import { TeamsTokenExtractor } from '../token-extractor'
|
|
7
|
+
|
|
8
|
+
export async function extractAction(options: {
|
|
9
|
+
pretty?: boolean
|
|
10
|
+
debug?: boolean
|
|
11
|
+
token?: string
|
|
12
|
+
}): Promise<void> {
|
|
13
|
+
try {
|
|
14
|
+
let token: string
|
|
15
|
+
|
|
16
|
+
if (options.token) {
|
|
17
|
+
token = options.token
|
|
18
|
+
if (options.debug) {
|
|
19
|
+
console.error(`[debug] Using provided token: ${token.substring(0, 20)}...`)
|
|
20
|
+
}
|
|
21
|
+
} else {
|
|
22
|
+
const extractor = new TeamsTokenExtractor()
|
|
23
|
+
|
|
24
|
+
if (process.platform === 'darwin') {
|
|
25
|
+
console.log('')
|
|
26
|
+
console.log(' Extracting your Microsoft Teams credentials...')
|
|
27
|
+
console.log('')
|
|
28
|
+
console.log(' Your Mac may ask for your password to access Keychain.')
|
|
29
|
+
console.log(' This is required because Teams encrypts your login token')
|
|
30
|
+
console.log(' using macOS Keychain for security.')
|
|
31
|
+
console.log('')
|
|
32
|
+
console.log(' What happens:')
|
|
33
|
+
console.log(" 1. We read the encrypted token from Teams' cookies")
|
|
34
|
+
console.log(' 2. macOS Keychain decrypts it (requires your password)')
|
|
35
|
+
console.log(' 3. The token is stored locally in ~/.config/agent-messenger/')
|
|
36
|
+
console.log('')
|
|
37
|
+
console.log(' Your password is never stored or transmitted anywhere.')
|
|
38
|
+
console.log('')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (options.debug) {
|
|
42
|
+
console.error(`[debug] Extracting Teams token...`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const extracted = await extractor.extract()
|
|
46
|
+
|
|
47
|
+
if (!extracted) {
|
|
48
|
+
console.log(
|
|
49
|
+
formatOutput(
|
|
50
|
+
{
|
|
51
|
+
error:
|
|
52
|
+
'No Teams token found. Make sure Microsoft Teams desktop app is installed and logged in.',
|
|
53
|
+
hint: 'Run with --token <token> to manually provide a token, or --debug for more info.',
|
|
54
|
+
},
|
|
55
|
+
options.pretty
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
token = extracted.token
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (options.debug) {
|
|
64
|
+
console.error(`[debug] Token extracted: ${token.substring(0, 20)}...`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const client = new TeamsClient(token)
|
|
69
|
+
|
|
70
|
+
if (options.debug) {
|
|
71
|
+
console.error(`[debug] Testing token validity...`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const authInfo = await client.testAuth()
|
|
75
|
+
|
|
76
|
+
if (options.debug) {
|
|
77
|
+
console.error(`[debug] ✓ Token valid for user: ${authInfo.displayName}`)
|
|
78
|
+
console.error(`[debug] Discovering teams...`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const teams = await client.listTeams()
|
|
82
|
+
|
|
83
|
+
if (options.debug) {
|
|
84
|
+
console.error(`[debug] ✓ Found ${teams.length} team(s)`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (teams.length === 0) {
|
|
88
|
+
console.log(
|
|
89
|
+
formatOutput(
|
|
90
|
+
{
|
|
91
|
+
error:
|
|
92
|
+
'No teams found. Make sure you are a member of at least one Microsoft Teams team.',
|
|
93
|
+
},
|
|
94
|
+
options.pretty
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
process.exit(1)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const credManager = new TeamsCredentialManager()
|
|
101
|
+
const teamMap: Record<string, { team_id: string; team_name: string }> = {}
|
|
102
|
+
|
|
103
|
+
for (const team of teams) {
|
|
104
|
+
teamMap[team.id] = {
|
|
105
|
+
team_id: team.id,
|
|
106
|
+
team_name: team.name,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const config = {
|
|
111
|
+
token: token,
|
|
112
|
+
current_team: teams[0].id,
|
|
113
|
+
teams: teamMap,
|
|
114
|
+
token_expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await credManager.saveConfig(config)
|
|
118
|
+
|
|
119
|
+
if (options.debug) {
|
|
120
|
+
console.error(`[debug] ✓ Credentials saved`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const output = {
|
|
124
|
+
teams: teams.map((t) => `${t.id}/${t.name}`),
|
|
125
|
+
current: teams[0].id,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log(formatOutput(output, options.pretty))
|
|
129
|
+
} catch (error) {
|
|
130
|
+
const errorMessage = (error as Error).message
|
|
131
|
+
const is401 = errorMessage.includes('401') || errorMessage.includes('Unauthorized')
|
|
132
|
+
console.log(
|
|
133
|
+
formatOutput(
|
|
134
|
+
{
|
|
135
|
+
error: `Token validation failed: ${errorMessage}`,
|
|
136
|
+
hint: is401
|
|
137
|
+
? 'Token expired. Open Microsoft Teams, send a message to refresh your session, then run "auth extract" again.'
|
|
138
|
+
: 'Make sure Microsoft Teams desktop app is running and you are logged in.',
|
|
139
|
+
},
|
|
140
|
+
options.pretty
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
process.exit(1)
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
handleError(error as Error)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function logoutAction(options: { pretty?: boolean }): Promise<void> {
|
|
151
|
+
try {
|
|
152
|
+
const credManager = new TeamsCredentialManager()
|
|
153
|
+
const config = await credManager.loadConfig()
|
|
154
|
+
|
|
155
|
+
if (!config?.token) {
|
|
156
|
+
console.log(
|
|
157
|
+
formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty)
|
|
158
|
+
)
|
|
159
|
+
process.exit(1)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await credManager.clearCredentials()
|
|
163
|
+
|
|
164
|
+
console.log(formatOutput({ removed: 'teams', success: true }, options.pretty))
|
|
165
|
+
} catch (error) {
|
|
166
|
+
handleError(error as Error)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function statusAction(options: { pretty?: boolean }): Promise<void> {
|
|
171
|
+
try {
|
|
172
|
+
const credManager = new TeamsCredentialManager()
|
|
173
|
+
const config = await credManager.loadConfig()
|
|
174
|
+
|
|
175
|
+
if (!config?.token) {
|
|
176
|
+
console.log(
|
|
177
|
+
formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty)
|
|
178
|
+
)
|
|
179
|
+
process.exit(1)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let authInfo: { id: string; displayName: string } | null = null
|
|
183
|
+
let valid = false
|
|
184
|
+
const isExpired = await credManager.isTokenExpired()
|
|
185
|
+
|
|
186
|
+
if (!isExpired) {
|
|
187
|
+
try {
|
|
188
|
+
const client = new TeamsClient(config.token, config.token_expires_at)
|
|
189
|
+
authInfo = await client.testAuth()
|
|
190
|
+
valid = true
|
|
191
|
+
} catch {
|
|
192
|
+
valid = false
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const output = {
|
|
197
|
+
authenticated: valid,
|
|
198
|
+
user: authInfo?.displayName,
|
|
199
|
+
current_team: config.current_team,
|
|
200
|
+
teams_count: Object.keys(config.teams).length,
|
|
201
|
+
token_expires_at: config.token_expires_at ?? null,
|
|
202
|
+
token_expired: isExpired,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log(formatOutput(output, options.pretty))
|
|
206
|
+
} catch (error) {
|
|
207
|
+
handleError(error as Error)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export const authCommand = new Command('auth')
|
|
212
|
+
.description('Authentication commands')
|
|
213
|
+
.addCommand(
|
|
214
|
+
new Command('extract')
|
|
215
|
+
.description('Extract token from Microsoft Teams desktop app')
|
|
216
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
217
|
+
.option('--debug', 'Show debug output for troubleshooting')
|
|
218
|
+
.option('--token <token>', 'Manually provide a token (bypasses auto-extraction)')
|
|
219
|
+
.action(extractAction)
|
|
220
|
+
)
|
|
221
|
+
.addCommand(
|
|
222
|
+
new Command('logout')
|
|
223
|
+
.description('Logout from Microsoft Teams')
|
|
224
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
225
|
+
.action(logoutAction)
|
|
226
|
+
)
|
|
227
|
+
.addCommand(
|
|
228
|
+
new Command('status')
|
|
229
|
+
.description('Show authentication status')
|
|
230
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
231
|
+
.action(statusAction)
|
|
232
|
+
)
|