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.
Files changed (111) hide show
  1. package/.claude-plugin/README.md +38 -14
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.github/workflows/ci.yml +3 -0
  4. package/CONTRIBUTING.md +24 -1
  5. package/README.md +12 -8
  6. package/dist/package.json +1 -1
  7. package/dist/src/platforms/discord/cli.d.ts.map +1 -1
  8. package/dist/src/platforms/discord/cli.js +8 -1
  9. package/dist/src/platforms/discord/cli.js.map +1 -1
  10. package/dist/src/platforms/discord/commands/file.d.ts.map +1 -1
  11. package/dist/src/platforms/discord/commands/file.js +13 -7
  12. package/dist/src/platforms/discord/commands/file.js.map +1 -1
  13. package/dist/src/platforms/discord/commands/friend.d.ts.map +1 -1
  14. package/dist/src/platforms/discord/commands/friend.js +30 -30
  15. package/dist/src/platforms/discord/commands/friend.js.map +1 -1
  16. package/dist/src/platforms/discord/commands/index.d.ts +7 -0
  17. package/dist/src/platforms/discord/commands/index.d.ts.map +1 -1
  18. package/dist/src/platforms/discord/commands/index.js +7 -0
  19. package/dist/src/platforms/discord/commands/index.js.map +1 -1
  20. package/dist/src/platforms/discord/commands/snapshot.d.ts.map +1 -1
  21. package/dist/src/platforms/discord/commands/snapshot.js +1 -2
  22. package/dist/src/platforms/discord/commands/snapshot.js.map +1 -1
  23. package/dist/src/platforms/slack/commands/file.d.ts.map +1 -1
  24. package/dist/src/platforms/slack/commands/file.js +10 -4
  25. package/dist/src/platforms/slack/commands/file.js.map +1 -1
  26. package/dist/src/platforms/slack/commands/sections.d.ts.map +1 -1
  27. package/dist/src/platforms/slack/commands/sections.js +5 -6
  28. package/dist/src/platforms/slack/commands/sections.js.map +1 -1
  29. package/dist/src/platforms/slack/commands/snapshot.d.ts.map +1 -1
  30. package/dist/src/platforms/slack/commands/snapshot.js +1 -2
  31. package/dist/src/platforms/slack/commands/snapshot.js.map +1 -1
  32. package/dist/src/platforms/slack/commands/user.js +8 -8
  33. package/dist/src/platforms/slackbot/commands/reaction.js +2 -2
  34. package/dist/src/platforms/slackbot/commands/reaction.js.map +1 -1
  35. package/dist/src/platforms/teams/cli.d.ts.map +1 -1
  36. package/dist/src/platforms/teams/cli.js +7 -1
  37. package/dist/src/platforms/teams/cli.js.map +1 -1
  38. package/dist/src/platforms/teams/commands/auth.d.ts +3 -0
  39. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
  40. package/dist/src/platforms/teams/commands/auth.js +208 -107
  41. package/dist/src/platforms/teams/commands/auth.js.map +1 -1
  42. package/dist/src/platforms/teams/commands/channel.js +9 -9
  43. package/dist/src/platforms/teams/commands/channel.js.map +1 -1
  44. package/dist/src/platforms/teams/commands/file.js +21 -21
  45. package/dist/src/platforms/teams/commands/file.js.map +1 -1
  46. package/dist/src/platforms/teams/commands/message.js +12 -12
  47. package/dist/src/platforms/teams/commands/message.js.map +1 -1
  48. package/dist/src/platforms/teams/commands/reaction.js +6 -6
  49. package/dist/src/platforms/teams/commands/reaction.js.map +1 -1
  50. package/dist/src/platforms/teams/commands/snapshot.d.ts.map +1 -1
  51. package/dist/src/platforms/teams/commands/snapshot.js +6 -6
  52. package/dist/src/platforms/teams/commands/snapshot.js.map +1 -1
  53. package/dist/src/platforms/teams/commands/team.d.ts.map +1 -1
  54. package/dist/src/platforms/teams/commands/team.js +21 -25
  55. package/dist/src/platforms/teams/commands/team.js.map +1 -1
  56. package/dist/src/platforms/teams/commands/user.js +9 -9
  57. package/dist/src/platforms/teams/commands/user.js.map +1 -1
  58. package/dist/src/platforms/teams/credential-manager.d.ts +15 -2
  59. package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
  60. package/dist/src/platforms/teams/credential-manager.js +110 -28
  61. package/dist/src/platforms/teams/credential-manager.js.map +1 -1
  62. package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
  63. package/dist/src/platforms/teams/ensure-auth.js +46 -16
  64. package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
  65. package/dist/src/platforms/teams/token-extractor.d.ts +8 -2
  66. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  67. package/dist/src/platforms/teams/token-extractor.js +36 -24
  68. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  69. package/dist/src/platforms/teams/types.d.ts +121 -0
  70. package/dist/src/platforms/teams/types.d.ts.map +1 -1
  71. package/dist/src/platforms/teams/types.js +16 -0
  72. package/dist/src/platforms/teams/types.js.map +1 -1
  73. package/e2e/README.md +1 -1
  74. package/package.json +1 -1
  75. package/skills/agent-discord/SKILL.md +5 -0
  76. package/skills/agent-slack/SKILL.md +11 -17
  77. package/skills/agent-teams/SKILL.md +21 -24
  78. package/skills/agent-teams/references/common-patterns.md +63 -49
  79. package/src/platforms/discord/cli.ts +14 -0
  80. package/src/platforms/discord/commands/file.ts +13 -7
  81. package/src/platforms/discord/commands/friend.ts +34 -34
  82. package/src/platforms/discord/commands/index.ts +7 -0
  83. package/src/platforms/discord/commands/snapshot.ts +1 -2
  84. package/src/platforms/slack/commands/file.ts +12 -4
  85. package/src/platforms/slack/commands/sections.ts +8 -9
  86. package/src/platforms/slack/commands/snapshot.ts +1 -2
  87. package/src/platforms/slack/commands/user.ts +8 -8
  88. package/src/platforms/slackbot/commands/reaction.ts +2 -2
  89. package/src/platforms/teams/cli.ts +7 -0
  90. package/src/platforms/teams/commands/auth.test.ts +6 -5
  91. package/src/platforms/teams/commands/auth.ts +283 -120
  92. package/src/platforms/teams/commands/channel.test.ts +10 -4
  93. package/src/platforms/teams/commands/channel.ts +9 -9
  94. package/src/platforms/teams/commands/file.test.ts +9 -3
  95. package/src/platforms/teams/commands/file.ts +21 -21
  96. package/src/platforms/teams/commands/message.test.ts +9 -3
  97. package/src/platforms/teams/commands/message.ts +12 -12
  98. package/src/platforms/teams/commands/reaction.test.ts +9 -3
  99. package/src/platforms/teams/commands/reaction.ts +6 -6
  100. package/src/platforms/teams/commands/snapshot.ts +6 -6
  101. package/src/platforms/teams/commands/team.test.ts +19 -12
  102. package/src/platforms/teams/commands/team.ts +22 -28
  103. package/src/platforms/teams/commands/user.ts +9 -9
  104. package/src/platforms/teams/credential-manager.test.ts +30 -24
  105. package/src/platforms/teams/credential-manager.ts +106 -31
  106. package/src/platforms/teams/ensure-auth.test.ts +125 -26
  107. package/src/platforms/teams/ensure-auth.ts +47 -15
  108. package/src/platforms/teams/token-extractor.test.ts +116 -85
  109. package/src/platforms/teams/token-extractor.ts +65 -97
  110. package/src/platforms/teams/types.test.ts +31 -13
  111. 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
- return JSON.parse(content) as TeamsConfig
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
- return config?.token ?? null
76
+ if (!config) return null
77
+ return this.resolveCurrentAccount(config)?.token ?? null
37
78
  }
38
79
 
39
- async setToken(token: string, expiresAt?: string): Promise<void> {
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
- config.token = token
49
- if (expiresAt !== undefined) {
50
- config.token_expires_at = expiresAt
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?.current_team) {
58
- return null
59
- }
60
- return config.teams[config.current_team] ?? null
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
- let config = await this.loadConfig()
65
- if (!config) {
66
- config = {
67
- token: '',
68
- current_team: null,
69
- teams: {},
70
- }
71
- }
72
- config.current_team = teamId
73
- config.teams[teamId] = { team_id: teamId, team_name: teamName }
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?.token_expires_at) {
86
- return true
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
- const expiresAt = new Date(config.token_expires_at)
90
- return expiresAt.getTime() <= Date.now()
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
- isTokenExpiredSpy = spyOn(TeamsCredentialManager.prototype, 'isTokenExpired').mockResolvedValue(true)
18
-
19
- extractSpy = spyOn(TeamsTokenExtractor.prototype, 'extract').mockResolvedValue({
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
- token: 'existing-token',
50
- current_team: 'team-1',
51
- teams: { 'team-1': { team_id: 'team-1', team_name: 'Team One' } },
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
- token: 'test-teams-token',
75
- current_team: 'team-1',
76
- teams: {
77
- 'team-1': { team_id: 'team-1', team_name: 'Team One' },
78
- 'team-2': { team_id: 'team-2', team_name: 'Team Two' },
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
- token: 'expired-token',
88
- current_team: 'team-1',
89
- teams: { 'team-1': { team_id: 'team-1', team_name: 'Team One' } },
90
- token_expires_at: new Date(Date.now() - 3600000).toISOString(),
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(expect.objectContaining({ token: 'test-teams-token' }))
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(expect.objectContaining({ current_team: 'team-1' }))
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(null)
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?.token && !(await credManager.isTokenExpired())) return
11
+ if (config && hasValidToken(config)) return
11
12
 
12
13
  const extractor = new TeamsTokenExtractor()
13
14
  const extracted = await extractor.extract()
14
- if (!extracted) return
15
+ if (extracted.length === 0) return
15
16
 
16
- const client = new TeamsClient(extracted.token)
17
- await client.testAuth()
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
- const teams = await client.listTeams()
20
- if (teams.length === 0) return
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
- const teamMap: Record<string, { team_id: string; team_name: string }> = {}
23
- for (const team of teams) {
24
- teamMap[team.id] = { team_id: team.id, team_name: team.name }
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
- await credManager.saveConfig({
28
- token: extracted.token,
29
- current_team: teams[0].id,
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
+ }