agent-messenger 1.4.0 → 1.6.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-plugin/README.md +38 -14
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/ci.yml +3 -0
- package/CONTRIBUTING.md +24 -1
- package/README.md +12 -8
- package/dist/package.json +1 -1
- package/dist/src/platforms/discord/cli.d.ts.map +1 -1
- package/dist/src/platforms/discord/cli.js +8 -1
- package/dist/src/platforms/discord/cli.js.map +1 -1
- package/dist/src/platforms/discord/commands/file.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/file.js +13 -7
- package/dist/src/platforms/discord/commands/file.js.map +1 -1
- package/dist/src/platforms/discord/commands/friend.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/friend.js +30 -30
- package/dist/src/platforms/discord/commands/friend.js.map +1 -1
- package/dist/src/platforms/discord/commands/index.d.ts +7 -0
- package/dist/src/platforms/discord/commands/index.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/index.js +7 -0
- package/dist/src/platforms/discord/commands/index.js.map +1 -1
- package/dist/src/platforms/discord/commands/snapshot.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/snapshot.js +1 -2
- package/dist/src/platforms/discord/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/slack/commands/file.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/file.js +10 -4
- package/dist/src/platforms/slack/commands/file.js.map +1 -1
- package/dist/src/platforms/slack/commands/sections.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/sections.js +5 -6
- package/dist/src/platforms/slack/commands/sections.js.map +1 -1
- package/dist/src/platforms/slack/commands/snapshot.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/snapshot.js +1 -2
- package/dist/src/platforms/slack/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/slack/commands/user.js +8 -8
- package/dist/src/platforms/slackbot/commands/reaction.js +2 -2
- package/dist/src/platforms/slackbot/commands/reaction.js.map +1 -1
- package/dist/src/platforms/teams/cli.d.ts.map +1 -1
- package/dist/src/platforms/teams/cli.js +7 -1
- package/dist/src/platforms/teams/cli.js.map +1 -1
- package/dist/src/platforms/teams/commands/auth.d.ts +3 -0
- package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/auth.js +208 -107
- package/dist/src/platforms/teams/commands/auth.js.map +1 -1
- package/dist/src/platforms/teams/commands/channel.js +9 -9
- package/dist/src/platforms/teams/commands/channel.js.map +1 -1
- package/dist/src/platforms/teams/commands/file.js +21 -21
- package/dist/src/platforms/teams/commands/file.js.map +1 -1
- package/dist/src/platforms/teams/commands/message.js +12 -12
- package/dist/src/platforms/teams/commands/message.js.map +1 -1
- package/dist/src/platforms/teams/commands/reaction.js +6 -6
- package/dist/src/platforms/teams/commands/reaction.js.map +1 -1
- package/dist/src/platforms/teams/commands/snapshot.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/snapshot.js +6 -6
- package/dist/src/platforms/teams/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/teams/commands/team.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/team.js +21 -25
- package/dist/src/platforms/teams/commands/team.js.map +1 -1
- package/dist/src/platforms/teams/commands/user.js +9 -9
- package/dist/src/platforms/teams/commands/user.js.map +1 -1
- package/dist/src/platforms/teams/credential-manager.d.ts +15 -2
- package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/teams/credential-manager.js +110 -28
- package/dist/src/platforms/teams/credential-manager.js.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.js +46 -16
- package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
- package/dist/src/platforms/teams/token-extractor.d.ts +8 -2
- package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/teams/token-extractor.js +36 -24
- package/dist/src/platforms/teams/token-extractor.js.map +1 -1
- package/dist/src/platforms/teams/types.d.ts +121 -0
- package/dist/src/platforms/teams/types.d.ts.map +1 -1
- package/dist/src/platforms/teams/types.js +16 -0
- package/dist/src/platforms/teams/types.js.map +1 -1
- package/e2e/README.md +1 -1
- package/package.json +1 -1
- package/skills/agent-discord/SKILL.md +5 -0
- package/skills/agent-slack/SKILL.md +11 -17
- package/skills/agent-teams/SKILL.md +21 -24
- package/skills/agent-teams/references/common-patterns.md +63 -49
- package/src/platforms/discord/cli.ts +14 -0
- package/src/platforms/discord/commands/file.ts +13 -7
- package/src/platforms/discord/commands/friend.ts +34 -34
- package/src/platforms/discord/commands/index.ts +7 -0
- package/src/platforms/discord/commands/snapshot.ts +1 -2
- package/src/platforms/slack/commands/file.ts +12 -4
- package/src/platforms/slack/commands/sections.ts +8 -9
- package/src/platforms/slack/commands/snapshot.ts +1 -2
- package/src/platforms/slack/commands/user.ts +8 -8
- package/src/platforms/slackbot/commands/reaction.ts +2 -2
- package/src/platforms/teams/cli.ts +7 -0
- package/src/platforms/teams/commands/auth.test.ts +6 -5
- package/src/platforms/teams/commands/auth.ts +283 -120
- package/src/platforms/teams/commands/channel.test.ts +10 -4
- package/src/platforms/teams/commands/channel.ts +9 -9
- package/src/platforms/teams/commands/file.test.ts +9 -3
- package/src/platforms/teams/commands/file.ts +21 -21
- package/src/platforms/teams/commands/message.test.ts +9 -3
- package/src/platforms/teams/commands/message.ts +12 -12
- package/src/platforms/teams/commands/reaction.test.ts +9 -3
- package/src/platforms/teams/commands/reaction.ts +6 -6
- package/src/platforms/teams/commands/snapshot.ts +6 -6
- package/src/platforms/teams/commands/team.test.ts +19 -12
- package/src/platforms/teams/commands/team.ts +22 -28
- package/src/platforms/teams/commands/user.ts +9 -9
- package/src/platforms/teams/credential-manager.test.ts +30 -24
- package/src/platforms/teams/credential-manager.ts +106 -31
- package/src/platforms/teams/ensure-auth.test.ts +125 -26
- package/src/platforms/teams/ensure-auth.ts +47 -15
- package/src/platforms/teams/token-extractor.test.ts +116 -85
- package/src/platforms/teams/token-extractor.ts +65 -97
- package/src/platforms/teams/types.test.ts +31 -13
- package/src/platforms/teams/types.ts +45 -0
|
@@ -2,9 +2,11 @@ import { existsSync } from 'node:fs'
|
|
|
2
2
|
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { homedir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
|
-
import type { TeamsConfig } from './types'
|
|
5
|
+
import type { TeamsAccount, TeamsAccountType, TeamsConfig, TeamsConfigLegacy } from './types'
|
|
6
6
|
|
|
7
7
|
export class TeamsCredentialManager {
|
|
8
|
+
static accountOverride?: TeamsAccountType
|
|
9
|
+
|
|
8
10
|
private configDir: string
|
|
9
11
|
private credentialsPath: string
|
|
10
12
|
|
|
@@ -20,7 +22,8 @@ export class TeamsCredentialManager {
|
|
|
20
22
|
|
|
21
23
|
try {
|
|
22
24
|
const content = await readFile(this.credentialsPath, 'utf-8')
|
|
23
|
-
|
|
25
|
+
const raw = JSON.parse(content)
|
|
26
|
+
return this.migrateIfNeeded(raw)
|
|
24
27
|
} catch {
|
|
25
28
|
return null
|
|
26
29
|
}
|
|
@@ -31,49 +34,115 @@ export class TeamsCredentialManager {
|
|
|
31
34
|
await writeFile(this.credentialsPath, JSON.stringify(config, null, 2), { mode: 0o600 })
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
private migrateIfNeeded(raw: TeamsConfig | TeamsConfigLegacy): TeamsConfig {
|
|
38
|
+
if ('accounts' in raw && raw.accounts) {
|
|
39
|
+
return raw as TeamsConfig
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const legacy = raw as TeamsConfigLegacy
|
|
43
|
+
const account: TeamsAccount = {
|
|
44
|
+
token: legacy.token,
|
|
45
|
+
token_expires_at: legacy.token_expires_at,
|
|
46
|
+
account_type: 'work',
|
|
47
|
+
current_team: legacy.current_team,
|
|
48
|
+
teams: legacy.teams,
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
current_account: 'work',
|
|
52
|
+
accounts: { work: account },
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private resolveAccountKey(config: TeamsConfig): string | null {
|
|
57
|
+
return TeamsCredentialManager.accountOverride ?? config.current_account
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getCurrentAccount(): Promise<TeamsAccount | null> {
|
|
61
|
+
const config = await this.loadConfig()
|
|
62
|
+
if (!config) return null
|
|
63
|
+
const key = this.resolveAccountKey(config)
|
|
64
|
+
if (!key) return null
|
|
65
|
+
return config.accounts[key] ?? null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private resolveCurrentAccount(config: TeamsConfig): TeamsAccount | null {
|
|
69
|
+
const key = this.resolveAccountKey(config)
|
|
70
|
+
if (!key) return null
|
|
71
|
+
return config.accounts[key] ?? null
|
|
72
|
+
}
|
|
73
|
+
|
|
34
74
|
async getToken(): Promise<string | null> {
|
|
35
75
|
const config = await this.loadConfig()
|
|
36
|
-
|
|
76
|
+
if (!config) return null
|
|
77
|
+
return this.resolveCurrentAccount(config)?.token ?? null
|
|
37
78
|
}
|
|
38
79
|
|
|
39
|
-
async
|
|
80
|
+
async getTokenWithExpiry(): Promise<{ token: string; tokenExpiresAt?: string } | null> {
|
|
81
|
+
const config = await this.loadConfig()
|
|
82
|
+
if (!config) return null
|
|
83
|
+
const account = this.resolveCurrentAccount(config)
|
|
84
|
+
if (!account?.token) return null
|
|
85
|
+
return { token: account.token, tokenExpiresAt: account.token_expires_at }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async setToken(token: string, accountType: TeamsAccountType, expiresAt?: string): Promise<void> {
|
|
40
89
|
let config = await this.loadConfig()
|
|
41
90
|
if (!config) {
|
|
42
|
-
config = {
|
|
43
|
-
token,
|
|
44
|
-
current_team: null,
|
|
45
|
-
teams: {},
|
|
46
|
-
}
|
|
91
|
+
config = { current_account: accountType, accounts: {} }
|
|
47
92
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
93
|
+
const existing = config.accounts[accountType]
|
|
94
|
+
config.accounts[accountType] = {
|
|
95
|
+
token,
|
|
96
|
+
token_expires_at: expiresAt,
|
|
97
|
+
account_type: accountType,
|
|
98
|
+
user_name: existing?.user_name,
|
|
99
|
+
current_team: existing?.current_team ?? null,
|
|
100
|
+
teams: existing?.teams ?? {},
|
|
101
|
+
}
|
|
102
|
+
if (!config.current_account) {
|
|
103
|
+
config.current_account = accountType
|
|
51
104
|
}
|
|
52
105
|
await this.saveConfig(config)
|
|
53
106
|
}
|
|
54
107
|
|
|
55
108
|
async getCurrentTeam(): Promise<{ team_id: string; team_name: string } | null> {
|
|
56
109
|
const config = await this.loadConfig()
|
|
57
|
-
if (!config
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return
|
|
110
|
+
if (!config) return null
|
|
111
|
+
const account = this.resolveCurrentAccount(config)
|
|
112
|
+
if (!account?.current_team) return null
|
|
113
|
+
return account.teams[account.current_team] ?? null
|
|
61
114
|
}
|
|
62
115
|
|
|
63
116
|
async setCurrentTeam(teamId: string, teamName: string): Promise<void> {
|
|
64
|
-
|
|
65
|
-
if (!config)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
117
|
+
const config = await this.loadConfig()
|
|
118
|
+
if (!config) return
|
|
119
|
+
const account = this.resolveCurrentAccount(config)
|
|
120
|
+
if (!account) return
|
|
121
|
+
account.current_team = teamId
|
|
122
|
+
account.teams[teamId] = { team_id: teamId, team_name: teamName }
|
|
123
|
+
await this.saveConfig(config)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async getCurrentAccountType(): Promise<TeamsAccountType | null> {
|
|
127
|
+
const config = await this.loadConfig()
|
|
128
|
+
if (!config) return null
|
|
129
|
+
const key = this.resolveAccountKey(config)
|
|
130
|
+
return (key as TeamsAccountType) ?? null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async setCurrentAccount(accountType: TeamsAccountType): Promise<void> {
|
|
134
|
+
const config = await this.loadConfig()
|
|
135
|
+
if (!config) return
|
|
136
|
+
if (!config.accounts[accountType]) return
|
|
137
|
+
config.current_account = accountType
|
|
74
138
|
await this.saveConfig(config)
|
|
75
139
|
}
|
|
76
140
|
|
|
141
|
+
async getAccounts(): Promise<Record<string, TeamsAccount>> {
|
|
142
|
+
const config = await this.loadConfig()
|
|
143
|
+
return config?.accounts ?? {}
|
|
144
|
+
}
|
|
145
|
+
|
|
77
146
|
async clearCredentials(): Promise<void> {
|
|
78
147
|
if (existsSync(this.credentialsPath)) {
|
|
79
148
|
await rm(this.credentialsPath)
|
|
@@ -82,11 +151,17 @@ export class TeamsCredentialManager {
|
|
|
82
151
|
|
|
83
152
|
async isTokenExpired(): Promise<boolean> {
|
|
84
153
|
const config = await this.loadConfig()
|
|
85
|
-
if (!config
|
|
86
|
-
|
|
87
|
-
|
|
154
|
+
if (!config) return true
|
|
155
|
+
const account = this.resolveCurrentAccount(config)
|
|
156
|
+
if (!account?.token_expires_at) return true
|
|
157
|
+
return new Date(account.token_expires_at).getTime() <= Date.now()
|
|
158
|
+
}
|
|
88
159
|
|
|
89
|
-
|
|
90
|
-
|
|
160
|
+
async isAccountTokenExpired(accountType: TeamsAccountType): Promise<boolean> {
|
|
161
|
+
const config = await this.loadConfig()
|
|
162
|
+
if (!config) return true
|
|
163
|
+
const account = config.accounts[accountType]
|
|
164
|
+
if (!account?.token_expires_at) return true
|
|
165
|
+
return new Date(account.token_expires_at).getTime() <= Date.now()
|
|
91
166
|
}
|
|
92
167
|
}
|
|
@@ -5,7 +5,6 @@ import { ensureTeamsAuth } from './ensure-auth'
|
|
|
5
5
|
import { TeamsTokenExtractor } from './token-extractor'
|
|
6
6
|
|
|
7
7
|
let loadConfigSpy: ReturnType<typeof spyOn>
|
|
8
|
-
let isTokenExpiredSpy: ReturnType<typeof spyOn>
|
|
9
8
|
let extractSpy: ReturnType<typeof spyOn>
|
|
10
9
|
let testAuthSpy: ReturnType<typeof spyOn>
|
|
11
10
|
let listTeamsSpy: ReturnType<typeof spyOn>
|
|
@@ -14,11 +13,9 @@ let saveConfigSpy: ReturnType<typeof spyOn>
|
|
|
14
13
|
beforeEach(() => {
|
|
15
14
|
loadConfigSpy = spyOn(TeamsCredentialManager.prototype, 'loadConfig').mockResolvedValue(null)
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
token: 'test-teams-token',
|
|
21
|
-
})
|
|
16
|
+
extractSpy = spyOn(TeamsTokenExtractor.prototype, 'extract').mockResolvedValue([
|
|
17
|
+
{ token: 'test-teams-token', accountType: 'work' },
|
|
18
|
+
])
|
|
22
19
|
|
|
23
20
|
testAuthSpy = spyOn(TeamsClient.prototype, 'testAuth').mockResolvedValue({
|
|
24
21
|
id: 'user-123',
|
|
@@ -35,7 +32,6 @@ beforeEach(() => {
|
|
|
35
32
|
|
|
36
33
|
afterEach(() => {
|
|
37
34
|
loadConfigSpy?.mockRestore()
|
|
38
|
-
isTokenExpiredSpy?.mockRestore()
|
|
39
35
|
extractSpy?.mockRestore()
|
|
40
36
|
testAuthSpy?.mockRestore()
|
|
41
37
|
listTeamsSpy?.mockRestore()
|
|
@@ -46,11 +42,17 @@ describe('ensureTeamsAuth', () => {
|
|
|
46
42
|
test('skips extraction when token exists and not expired', async () => {
|
|
47
43
|
// given
|
|
48
44
|
loadConfigSpy.mockResolvedValue({
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
current_account: 'work',
|
|
46
|
+
accounts: {
|
|
47
|
+
work: {
|
|
48
|
+
token: 'existing-token',
|
|
49
|
+
token_expires_at: new Date(Date.now() + 3600000).toISOString(),
|
|
50
|
+
account_type: 'work',
|
|
51
|
+
current_team: 'team-1',
|
|
52
|
+
teams: { 'team-1': { team_id: 'team-1', team_name: 'Team 1' } },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
52
55
|
})
|
|
53
|
-
isTokenExpiredSpy.mockResolvedValue(false)
|
|
54
56
|
|
|
55
57
|
// when
|
|
56
58
|
await ensureTeamsAuth()
|
|
@@ -71,12 +73,17 @@ describe('ensureTeamsAuth', () => {
|
|
|
71
73
|
expect(testAuthSpy).toHaveBeenCalled()
|
|
72
74
|
expect(saveConfigSpy).toHaveBeenCalledWith(
|
|
73
75
|
expect.objectContaining({
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
current_account: 'work',
|
|
77
|
+
accounts: expect.objectContaining({
|
|
78
|
+
work: expect.objectContaining({
|
|
79
|
+
token: 'test-teams-token',
|
|
80
|
+
current_team: 'team-1',
|
|
81
|
+
teams: {
|
|
82
|
+
'team-1': { team_id: 'team-1', team_name: 'Team One' },
|
|
83
|
+
'team-2': { team_id: 'team-2', team_name: 'Team Two' },
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
}),
|
|
80
87
|
}),
|
|
81
88
|
)
|
|
82
89
|
})
|
|
@@ -84,19 +91,31 @@ describe('ensureTeamsAuth', () => {
|
|
|
84
91
|
test('re-extracts when token is expired', async () => {
|
|
85
92
|
// given
|
|
86
93
|
loadConfigSpy.mockResolvedValue({
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
current_account: 'work',
|
|
95
|
+
accounts: {
|
|
96
|
+
work: {
|
|
97
|
+
token: 'expired-token',
|
|
98
|
+
token_expires_at: new Date(Date.now() - 3600000).toISOString(),
|
|
99
|
+
account_type: 'work',
|
|
100
|
+
current_team: 'team-1',
|
|
101
|
+
teams: { 'team-1': { team_id: 'team-1', team_name: 'Team One' } },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
91
104
|
})
|
|
92
|
-
isTokenExpiredSpy.mockResolvedValue(true)
|
|
93
105
|
|
|
94
106
|
// when
|
|
95
107
|
await ensureTeamsAuth()
|
|
96
108
|
|
|
97
109
|
// then
|
|
98
110
|
expect(extractSpy).toHaveBeenCalled()
|
|
99
|
-
expect(saveConfigSpy).toHaveBeenCalledWith(
|
|
111
|
+
expect(saveConfigSpy).toHaveBeenCalledWith(
|
|
112
|
+
expect.objectContaining({
|
|
113
|
+
current_account: 'work',
|
|
114
|
+
accounts: expect.objectContaining({
|
|
115
|
+
work: expect.objectContaining({ token: 'test-teams-token' }),
|
|
116
|
+
}),
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
100
119
|
})
|
|
101
120
|
|
|
102
121
|
test('sets first team as current', async () => {
|
|
@@ -104,7 +123,13 @@ describe('ensureTeamsAuth', () => {
|
|
|
104
123
|
await ensureTeamsAuth()
|
|
105
124
|
|
|
106
125
|
// then
|
|
107
|
-
expect(saveConfigSpy).toHaveBeenCalledWith(
|
|
126
|
+
expect(saveConfigSpy).toHaveBeenCalledWith(
|
|
127
|
+
expect.objectContaining({
|
|
128
|
+
accounts: expect.objectContaining({
|
|
129
|
+
work: expect.objectContaining({ current_team: 'team-1' }),
|
|
130
|
+
}),
|
|
131
|
+
}),
|
|
132
|
+
)
|
|
108
133
|
})
|
|
109
134
|
|
|
110
135
|
test('saves token_expires_at', async () => {
|
|
@@ -115,14 +140,14 @@ describe('ensureTeamsAuth', () => {
|
|
|
115
140
|
|
|
116
141
|
// then
|
|
117
142
|
const savedConfig = saveConfigSpy.mock.calls[0][0]
|
|
118
|
-
const expiresAt = new Date(savedConfig.token_expires_at).getTime()
|
|
143
|
+
const expiresAt = new Date(savedConfig.accounts.work.token_expires_at).getTime()
|
|
119
144
|
expect(expiresAt).toBeGreaterThanOrEqual(before + 60 * 60 * 1000 - 1)
|
|
120
145
|
expect(expiresAt).toBeLessThanOrEqual(after + 60 * 60 * 1000 + 1)
|
|
121
146
|
})
|
|
122
147
|
|
|
123
148
|
test('does not save when extraction returns null', async () => {
|
|
124
149
|
// given
|
|
125
|
-
extractSpy.mockResolvedValue(
|
|
150
|
+
extractSpy.mockResolvedValue([])
|
|
126
151
|
|
|
127
152
|
// when
|
|
128
153
|
await ensureTeamsAuth()
|
|
@@ -164,4 +189,78 @@ describe('ensureTeamsAuth', () => {
|
|
|
164
189
|
// then
|
|
165
190
|
expect(saveConfigSpy).not.toHaveBeenCalled()
|
|
166
191
|
})
|
|
192
|
+
|
|
193
|
+
test('extracts and saves multiple accounts', async () => {
|
|
194
|
+
// given
|
|
195
|
+
extractSpy.mockResolvedValue([
|
|
196
|
+
{ token: 'work-token', accountType: 'work' },
|
|
197
|
+
{ token: 'personal-token', accountType: 'personal' },
|
|
198
|
+
])
|
|
199
|
+
|
|
200
|
+
testAuthSpy
|
|
201
|
+
.mockResolvedValueOnce({ id: 'user-1', displayName: 'Work User' })
|
|
202
|
+
.mockResolvedValueOnce({ id: 'user-2', displayName: 'Personal User' })
|
|
203
|
+
|
|
204
|
+
listTeamsSpy
|
|
205
|
+
.mockResolvedValueOnce([{ id: 'team-w', name: 'Work Team' }])
|
|
206
|
+
.mockResolvedValueOnce([{ id: 'team-p', name: 'Personal Team' }])
|
|
207
|
+
|
|
208
|
+
// when
|
|
209
|
+
await ensureTeamsAuth()
|
|
210
|
+
|
|
211
|
+
// then
|
|
212
|
+
expect(saveConfigSpy).toHaveBeenCalledWith(
|
|
213
|
+
expect.objectContaining({
|
|
214
|
+
current_account: 'work',
|
|
215
|
+
accounts: expect.objectContaining({
|
|
216
|
+
work: expect.objectContaining({ token: 'work-token', current_team: 'team-w' }),
|
|
217
|
+
personal: expect.objectContaining({ token: 'personal-token', current_team: 'team-p' }),
|
|
218
|
+
}),
|
|
219
|
+
}),
|
|
220
|
+
)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test('skips failed account but saves successful ones', async () => {
|
|
224
|
+
// given
|
|
225
|
+
extractSpy.mockResolvedValue([
|
|
226
|
+
{ token: 'work-token', accountType: 'work' },
|
|
227
|
+
{ token: 'bad-token', accountType: 'personal' },
|
|
228
|
+
])
|
|
229
|
+
|
|
230
|
+
testAuthSpy
|
|
231
|
+
.mockResolvedValueOnce({ id: 'user-1', displayName: 'Work User' })
|
|
232
|
+
.mockRejectedValueOnce(new Error('401 Unauthorized'))
|
|
233
|
+
|
|
234
|
+
listTeamsSpy.mockResolvedValueOnce([{ id: 'team-w', name: 'Work Team' }])
|
|
235
|
+
|
|
236
|
+
// when
|
|
237
|
+
await ensureTeamsAuth()
|
|
238
|
+
|
|
239
|
+
// then
|
|
240
|
+
const savedConfig = saveConfigSpy.mock.calls[0][0]
|
|
241
|
+
expect(savedConfig.accounts.work).toBeDefined()
|
|
242
|
+
expect(savedConfig.accounts.personal).toBeUndefined()
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
test('re-extracts when token is empty string', async () => {
|
|
246
|
+
// given
|
|
247
|
+
loadConfigSpy.mockResolvedValue({
|
|
248
|
+
current_account: 'work',
|
|
249
|
+
accounts: {
|
|
250
|
+
work: {
|
|
251
|
+
token: '',
|
|
252
|
+
token_expires_at: new Date(Date.now() + 3600000).toISOString(),
|
|
253
|
+
account_type: 'work',
|
|
254
|
+
current_team: 'team-1',
|
|
255
|
+
teams: { 'team-1': { team_id: 'team-1', team_name: 'Team One' } },
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
// when
|
|
261
|
+
await ensureTeamsAuth()
|
|
262
|
+
|
|
263
|
+
// then
|
|
264
|
+
expect(extractSpy).toHaveBeenCalled()
|
|
265
|
+
})
|
|
167
266
|
})
|
|
@@ -1,34 +1,66 @@
|
|
|
1
1
|
import { TeamsClient } from './client'
|
|
2
2
|
import { TeamsCredentialManager } from './credential-manager'
|
|
3
3
|
import { TeamsTokenExtractor } from './token-extractor'
|
|
4
|
+
import type { TeamsAccount, TeamsConfig } from './types'
|
|
4
5
|
|
|
5
6
|
export async function ensureTeamsAuth(): Promise<void> {
|
|
6
7
|
try {
|
|
7
8
|
const credManager = new TeamsCredentialManager()
|
|
8
9
|
const config = await credManager.loadConfig()
|
|
9
10
|
|
|
10
|
-
if (config
|
|
11
|
+
if (config && hasValidToken(config)) return
|
|
11
12
|
|
|
12
13
|
const extractor = new TeamsTokenExtractor()
|
|
13
14
|
const extracted = await extractor.extract()
|
|
14
|
-
if (
|
|
15
|
+
if (extracted.length === 0) return
|
|
15
16
|
|
|
16
|
-
const
|
|
17
|
-
|
|
17
|
+
const newConfig: TeamsConfig = {
|
|
18
|
+
current_account: config?.current_account ?? null,
|
|
19
|
+
accounts: { ...config?.accounts },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const { token, accountType } of extracted) {
|
|
23
|
+
try {
|
|
24
|
+
const client = new TeamsClient(token)
|
|
25
|
+
await client.testAuth()
|
|
26
|
+
|
|
27
|
+
const teams = await client.listTeams()
|
|
28
|
+
if (teams.length === 0) continue
|
|
29
|
+
|
|
30
|
+
const teamMap: Record<string, { team_id: string; team_name: string }> = {}
|
|
31
|
+
for (const team of teams) {
|
|
32
|
+
teamMap[team.id] = { team_id: team.id, team_name: team.name }
|
|
33
|
+
}
|
|
18
34
|
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
const existing = newConfig.accounts[accountType]
|
|
36
|
+
const account: TeamsAccount = {
|
|
37
|
+
token,
|
|
38
|
+
token_expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
|
39
|
+
account_type: accountType,
|
|
40
|
+
user_name: existing?.user_name,
|
|
41
|
+
current_team: existing?.current_team ?? teams[0].id,
|
|
42
|
+
teams: teamMap,
|
|
43
|
+
}
|
|
21
44
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
45
|
+
newConfig.accounts[accountType] = account
|
|
46
|
+
if (!newConfig.current_account) {
|
|
47
|
+
newConfig.current_account = accountType
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error(`[agent-teams] Skipping ${accountType} account: ${(error as Error).message}`)
|
|
51
|
+
}
|
|
25
52
|
}
|
|
26
53
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
teams: teamMap,
|
|
31
|
-
token_expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
|
32
|
-
})
|
|
54
|
+
if (Object.keys(newConfig.accounts).length > 0) {
|
|
55
|
+
await credManager.saveConfig(newConfig)
|
|
56
|
+
}
|
|
33
57
|
} catch {}
|
|
34
58
|
}
|
|
59
|
+
|
|
60
|
+
function hasValidToken(config: TeamsConfig): boolean {
|
|
61
|
+
const key = TeamsCredentialManager.accountOverride ?? config.current_account
|
|
62
|
+
if (!key) return false
|
|
63
|
+
const account = config.accounts[key]
|
|
64
|
+
if (!account?.token || !account.token_expires_at) return false
|
|
65
|
+
return new Date(account.token_expires_at).getTime() > Date.now()
|
|
66
|
+
}
|