agent-messenger 2.0.0 → 2.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.
Files changed (119) hide show
  1. package/.claude-plugin/marketplace.json +14 -1
  2. package/.claude-plugin/plugin.json +4 -2
  3. package/README.md +33 -29
  4. package/dist/package.json +10 -2
  5. package/dist/src/cli.d.ts.map +1 -1
  6. package/dist/src/cli.js +3 -0
  7. package/dist/src/cli.js.map +1 -1
  8. package/dist/src/platforms/webex/app-config.d.ts +7 -0
  9. package/dist/src/platforms/webex/app-config.d.ts.map +1 -0
  10. package/dist/src/platforms/webex/app-config.js +20 -0
  11. package/dist/src/platforms/webex/app-config.js.map +1 -0
  12. package/dist/src/platforms/webex/cli.d.ts +5 -0
  13. package/dist/src/platforms/webex/cli.d.ts.map +1 -0
  14. package/dist/src/platforms/webex/cli.js +32 -0
  15. package/dist/src/platforms/webex/cli.js.map +1 -0
  16. package/dist/src/platforms/webex/client.d.ts +45 -0
  17. package/dist/src/platforms/webex/client.d.ts.map +1 -0
  18. package/dist/src/platforms/webex/client.js +175 -0
  19. package/dist/src/platforms/webex/client.js.map +1 -0
  20. package/dist/src/platforms/webex/commands/auth.d.ts +15 -0
  21. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -0
  22. package/dist/src/platforms/webex/commands/auth.js +124 -0
  23. package/dist/src/platforms/webex/commands/auth.js.map +1 -0
  24. package/dist/src/platforms/webex/commands/index.d.ts +6 -0
  25. package/dist/src/platforms/webex/commands/index.d.ts.map +1 -0
  26. package/dist/src/platforms/webex/commands/index.js +6 -0
  27. package/dist/src/platforms/webex/commands/index.js.map +1 -0
  28. package/dist/src/platforms/webex/commands/member.d.ts +7 -0
  29. package/dist/src/platforms/webex/commands/member.d.ts.map +1 -0
  30. package/dist/src/platforms/webex/commands/member.js +34 -0
  31. package/dist/src/platforms/webex/commands/member.js.map +1 -0
  32. package/dist/src/platforms/webex/commands/message.d.ts +26 -0
  33. package/dist/src/platforms/webex/commands/message.d.ts.map +1 -0
  34. package/dist/src/platforms/webex/commands/message.js +153 -0
  35. package/dist/src/platforms/webex/commands/message.js.map +1 -0
  36. package/dist/src/platforms/webex/commands/snapshot.d.ts +9 -0
  37. package/dist/src/platforms/webex/commands/snapshot.d.ts.map +1 -0
  38. package/dist/src/platforms/webex/commands/snapshot.js +72 -0
  39. package/dist/src/platforms/webex/commands/snapshot.js.map +1 -0
  40. package/dist/src/platforms/webex/commands/space.d.ts +11 -0
  41. package/dist/src/platforms/webex/commands/space.d.ts.map +1 -0
  42. package/dist/src/platforms/webex/commands/space.js +59 -0
  43. package/dist/src/platforms/webex/commands/space.js.map +1 -0
  44. package/dist/src/platforms/webex/credential-manager.d.ts +23 -0
  45. package/dist/src/platforms/webex/credential-manager.d.ts.map +1 -0
  46. package/dist/src/platforms/webex/credential-manager.js +148 -0
  47. package/dist/src/platforms/webex/credential-manager.js.map +1 -0
  48. package/dist/src/platforms/webex/ensure-auth.d.ts +2 -0
  49. package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -0
  50. package/dist/src/platforms/webex/ensure-auth.js +20 -0
  51. package/dist/src/platforms/webex/ensure-auth.js.map +1 -0
  52. package/dist/src/platforms/webex/index.d.ts +6 -0
  53. package/dist/src/platforms/webex/index.d.ts.map +1 -0
  54. package/dist/src/platforms/webex/index.js +5 -0
  55. package/dist/src/platforms/webex/index.js.map +1 -0
  56. package/dist/src/platforms/webex/types.d.ts +124 -0
  57. package/dist/src/platforms/webex/types.d.ts.map +1 -0
  58. package/dist/src/platforms/webex/types.js +63 -0
  59. package/dist/src/platforms/webex/types.js.map +1 -0
  60. package/dist/src/tui/adapters/webex-adapter.d.ts +14 -0
  61. package/dist/src/tui/adapters/webex-adapter.d.ts.map +1 -0
  62. package/dist/src/tui/adapters/webex-adapter.js +79 -0
  63. package/dist/src/tui/adapters/webex-adapter.js.map +1 -0
  64. package/dist/src/tui/app.d.ts.map +1 -1
  65. package/dist/src/tui/app.js +2 -0
  66. package/dist/src/tui/app.js.map +1 -1
  67. package/docs/content/docs/cli/meta.json +1 -0
  68. package/docs/content/docs/cli/webex.mdx +291 -0
  69. package/docs/content/docs/sdk/meta.json +1 -1
  70. package/docs/content/docs/sdk/webex.mdx +260 -0
  71. package/docs/content/docs/tui.mdx +4 -3
  72. package/docs/src/app/page.tsx +2 -2
  73. package/package.json +10 -2
  74. package/skills/agent-channeltalk/SKILL.md +1 -1
  75. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  76. package/skills/agent-discord/SKILL.md +1 -1
  77. package/skills/agent-discordbot/SKILL.md +1 -1
  78. package/skills/agent-instagram/SKILL.md +1 -1
  79. package/skills/agent-kakaotalk/SKILL.md +1 -1
  80. package/skills/agent-line/SKILL.md +1 -1
  81. package/skills/agent-slack/SKILL.md +1 -1
  82. package/skills/agent-slackbot/SKILL.md +1 -1
  83. package/skills/agent-teams/SKILL.md +1 -1
  84. package/skills/agent-telegram/SKILL.md +1 -1
  85. package/skills/agent-webex/SKILL.md +386 -0
  86. package/skills/agent-webex/references/authentication.md +318 -0
  87. package/skills/agent-webex/references/common-patterns.md +723 -0
  88. package/skills/agent-webex/templates/monitor-space.sh +165 -0
  89. package/skills/agent-webex/templates/post-message.sh +170 -0
  90. package/skills/agent-whatsapp/SKILL.md +1 -1
  91. package/skills/agent-whatsappbot/SKILL.md +1 -1
  92. package/src/cli.ts +4 -0
  93. package/src/platforms/webex/app-config.test.ts +98 -0
  94. package/src/platforms/webex/app-config.ts +31 -0
  95. package/src/platforms/webex/cli.test.ts +58 -0
  96. package/src/platforms/webex/cli.ts +39 -0
  97. package/src/platforms/webex/client.test.ts +429 -0
  98. package/src/platforms/webex/client.ts +247 -0
  99. package/src/platforms/webex/commands/auth.test.ts +222 -0
  100. package/src/platforms/webex/commands/auth.ts +180 -0
  101. package/src/platforms/webex/commands/index.ts +5 -0
  102. package/src/platforms/webex/commands/member.test.ts +103 -0
  103. package/src/platforms/webex/commands/member.ts +45 -0
  104. package/src/platforms/webex/commands/message.test.ts +231 -0
  105. package/src/platforms/webex/commands/message.ts +204 -0
  106. package/src/platforms/webex/commands/snapshot.test.ts +96 -0
  107. package/src/platforms/webex/commands/snapshot.ts +91 -0
  108. package/src/platforms/webex/commands/space.test.ts +206 -0
  109. package/src/platforms/webex/commands/space.ts +74 -0
  110. package/src/platforms/webex/credential-manager.test.ts +314 -0
  111. package/src/platforms/webex/credential-manager.ts +197 -0
  112. package/src/platforms/webex/ensure-auth.test.ts +85 -0
  113. package/src/platforms/webex/ensure-auth.ts +19 -0
  114. package/src/platforms/webex/index.test.ts +25 -0
  115. package/src/platforms/webex/index.ts +17 -0
  116. package/src/platforms/webex/types.test.ts +307 -0
  117. package/src/platforms/webex/types.ts +127 -0
  118. package/src/tui/adapters/webex-adapter.ts +103 -0
  119. package/src/tui/app.ts +2 -0
@@ -0,0 +1,222 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from 'bun:test'
2
+ import * as childProcess from 'node:child_process'
3
+
4
+ import { WebexClient } from '../client'
5
+ import { WebexCredentialManager } from '../credential-manager'
6
+ import { loginAction, logoutAction, statusAction } from './auth'
7
+
8
+ describe('auth commands', () => {
9
+ let consoleSpy: ReturnType<typeof spyOn>
10
+ let _consoleErrorSpy: ReturnType<typeof spyOn>
11
+ const mockPerson = {
12
+ id: 'person-1',
13
+ displayName: 'Test User',
14
+ emails: ['test@example.com'],
15
+ orgId: 'org-1',
16
+ type: 'person' as const,
17
+ created: '2024-01-01T00:00:00.000Z',
18
+ }
19
+
20
+ beforeEach(() => {
21
+ consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
22
+ _consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
23
+ spyOn(childProcess, 'exec').mockImplementation((() => {}) as any)
24
+ })
25
+
26
+ afterEach(() => {
27
+ mock.restore()
28
+ })
29
+
30
+ describe('loginAction with --token', () => {
31
+ test('authenticates with provided token (bot token flow)', async () => {
32
+ spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
33
+ spyOn(WebexClient.prototype, 'testAuth').mockResolvedValue(mockPerson)
34
+ spyOn(WebexCredentialManager.prototype, 'saveConfig').mockResolvedValue(undefined)
35
+
36
+ await loginAction({ token: 'bot-token-123', pretty: false })
37
+
38
+ expect(consoleSpy).toHaveBeenCalled()
39
+ const lastCall = consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0] as string
40
+ const output = JSON.parse(lastCall)
41
+ expect(output.authenticated).toBe(true)
42
+ expect(output.user.displayName).toBe('Test User')
43
+ })
44
+
45
+ test('saves tokenType as manual with expiresAt 0', async () => {
46
+ spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
47
+ spyOn(WebexClient.prototype, 'testAuth').mockResolvedValue(mockPerson)
48
+ const saveSpy = spyOn(WebexCredentialManager.prototype, 'saveConfig').mockResolvedValue(undefined)
49
+
50
+ await loginAction({ token: 'bot-token-123', pretty: false })
51
+
52
+ const savedConfig = saveSpy.mock.calls[0][0] as { tokenType: string; expiresAt: number; refreshToken: string }
53
+ expect(savedConfig.tokenType).toBe('manual')
54
+ expect(savedConfig.expiresAt).toBe(0)
55
+ expect(savedConfig.refreshToken).toBe('')
56
+ })
57
+ })
58
+
59
+ describe('loginAction with --client-id and --client-secret', () => {
60
+ test('uses provided credentials for Device Grant flow', async () => {
61
+ spyOn(WebexCredentialManager.prototype, 'requestDeviceCode').mockResolvedValue({
62
+ deviceCode: 'd',
63
+ userCode: 'u',
64
+ verificationUri: 'https://v',
65
+ verificationUriComplete: 'https://vc',
66
+ expiresIn: 300,
67
+ interval: 0.01,
68
+ })
69
+ spyOn(WebexCredentialManager.prototype, 'pollDeviceToken').mockResolvedValue({
70
+ accessToken: 'at',
71
+ refreshToken: 'rt',
72
+ expiresAt: Date.now() + 3600000,
73
+ })
74
+ spyOn(WebexCredentialManager.prototype, 'saveConfig').mockResolvedValue(undefined)
75
+ spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
76
+ spyOn(WebexClient.prototype, 'testAuth').mockResolvedValue(mockPerson)
77
+
78
+ await loginAction({ clientId: 'my-id', clientSecret: 'my-secret', pretty: false })
79
+
80
+ expect(WebexCredentialManager.prototype.requestDeviceCode).toHaveBeenCalledWith('my-id')
81
+ expect(WebexCredentialManager.prototype.pollDeviceToken).toHaveBeenCalledWith('d', 0.01, 300, 'my-id', 'my-secret')
82
+ })
83
+
84
+ test('saves tokenType as oauth in config', async () => {
85
+ spyOn(WebexCredentialManager.prototype, 'requestDeviceCode').mockResolvedValue({
86
+ deviceCode: 'd',
87
+ userCode: 'u',
88
+ verificationUri: 'https://v',
89
+ verificationUriComplete: 'https://vc',
90
+ expiresIn: 300,
91
+ interval: 0.01,
92
+ })
93
+ spyOn(WebexCredentialManager.prototype, 'pollDeviceToken').mockResolvedValue({
94
+ accessToken: 'at',
95
+ refreshToken: 'rt',
96
+ expiresAt: Date.now() + 3600000,
97
+ })
98
+ const saveSpy = spyOn(WebexCredentialManager.prototype, 'saveConfig').mockResolvedValue(undefined)
99
+ spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
100
+ spyOn(WebexClient.prototype, 'testAuth').mockResolvedValue(mockPerson)
101
+
102
+ await loginAction({ clientId: 'my-id', clientSecret: 'my-secret', pretty: false })
103
+
104
+ const savedConfig = saveSpy.mock.calls[0][0] as { tokenType: string }
105
+ expect(savedConfig.tokenType).toBe('oauth')
106
+ })
107
+
108
+ test('saves clientId and clientSecret in config', async () => {
109
+ spyOn(WebexCredentialManager.prototype, 'requestDeviceCode').mockResolvedValue({
110
+ deviceCode: 'd',
111
+ userCode: 'u',
112
+ verificationUri: 'https://v',
113
+ verificationUriComplete: 'https://vc',
114
+ expiresIn: 300,
115
+ interval: 0.01,
116
+ })
117
+ spyOn(WebexCredentialManager.prototype, 'pollDeviceToken').mockResolvedValue({
118
+ accessToken: 'at',
119
+ refreshToken: 'rt',
120
+ expiresAt: Date.now() + 3600000,
121
+ })
122
+ spyOn(WebexCredentialManager.prototype, 'saveConfig').mockResolvedValue(undefined)
123
+ spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
124
+ spyOn(WebexClient.prototype, 'testAuth').mockResolvedValue(mockPerson)
125
+
126
+ await loginAction({ clientId: 'my-id', clientSecret: 'my-secret', pretty: false })
127
+
128
+ const savedConfig = (WebexCredentialManager.prototype.saveConfig as ReturnType<typeof spyOn>).mock.calls[0][0] as { clientId: string; clientSecret: string }
129
+ expect(savedConfig.clientId).toBe('my-id')
130
+ expect(savedConfig.clientSecret).toBe('my-secret')
131
+ })
132
+ })
133
+
134
+ describe('statusAction', () => {
135
+ test('shows authenticated status when token is valid', async () => {
136
+ spyOn(WebexCredentialManager.prototype, 'loadConfig').mockResolvedValue(null)
137
+ spyOn(WebexCredentialManager.prototype, 'getToken').mockResolvedValue('valid-token')
138
+ spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
139
+ spyOn(WebexClient.prototype, 'testAuth').mockResolvedValue(mockPerson)
140
+
141
+ await statusAction({ pretty: false })
142
+
143
+ const lastCall = consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0] as string
144
+ const output = JSON.parse(lastCall)
145
+ expect(output.authenticated).toBe(true)
146
+ expect(output.user.displayName).toBe('Test User')
147
+ })
148
+
149
+ test('shows not authenticated when no token', async () => {
150
+ spyOn(WebexCredentialManager.prototype, 'loadConfig').mockResolvedValue(null)
151
+ spyOn(WebexCredentialManager.prototype, 'getToken').mockResolvedValue(null)
152
+ const exitSpy = spyOn(process, 'exit').mockImplementation(() => undefined as never)
153
+
154
+ await statusAction({ pretty: false })
155
+
156
+ const lastCall = consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0] as string
157
+ const output = JSON.parse(lastCall)
158
+ expect(output.error).toContain('Not authenticated')
159
+ expect(exitSpy).toHaveBeenCalledWith(1)
160
+ })
161
+
162
+ test('shows not authenticated when token validation fails', async () => {
163
+ spyOn(WebexCredentialManager.prototype, 'loadConfig').mockResolvedValue(null)
164
+ spyOn(WebexCredentialManager.prototype, 'getToken').mockResolvedValue('invalid-token')
165
+ spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
166
+ spyOn(WebexClient.prototype, 'testAuth').mockRejectedValue(new Error('401 Unauthorized'))
167
+
168
+ await statusAction({ pretty: false })
169
+
170
+ const lastCall = consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0] as string
171
+ const output = JSON.parse(lastCall)
172
+ expect(output.authenticated).toBe(false)
173
+ })
174
+
175
+ test('loads config for stored client credentials', async () => {
176
+ spyOn(WebexCredentialManager.prototype, 'loadConfig').mockResolvedValue({
177
+ accessToken: 'at',
178
+ refreshToken: 'rt',
179
+ expiresAt: Date.now() + 3600000,
180
+ clientId: 'stored-id',
181
+ clientSecret: 'stored-secret',
182
+ })
183
+ spyOn(WebexCredentialManager.prototype, 'getToken').mockResolvedValue('valid-token')
184
+ spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
185
+ spyOn(WebexClient.prototype, 'testAuth').mockResolvedValue(mockPerson)
186
+
187
+ await statusAction({ pretty: false })
188
+
189
+ expect(WebexCredentialManager.prototype.getToken).toHaveBeenCalledWith('stored-id', 'stored-secret')
190
+ })
191
+ })
192
+
193
+ describe('logoutAction', () => {
194
+ test('clears credentials when authenticated', async () => {
195
+ spyOn(WebexCredentialManager.prototype, 'loadConfig').mockResolvedValue({
196
+ accessToken: 'token',
197
+ refreshToken: 'refresh',
198
+ expiresAt: Date.now() + 3600000,
199
+ })
200
+ const clearSpy = spyOn(WebexCredentialManager.prototype, 'clearCredentials').mockResolvedValue(undefined)
201
+
202
+ await logoutAction({ pretty: false })
203
+
204
+ expect(clearSpy).toHaveBeenCalled()
205
+ const lastCall = consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0] as string
206
+ const output = JSON.parse(lastCall)
207
+ expect(output.success).toBe(true)
208
+ })
209
+
210
+ test('shows error when not authenticated', async () => {
211
+ spyOn(WebexCredentialManager.prototype, 'loadConfig').mockResolvedValue(null)
212
+ const exitSpy = spyOn(process, 'exit').mockImplementation(() => undefined as never)
213
+
214
+ await logoutAction({ pretty: false })
215
+
216
+ const lastCall = consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0] as string
217
+ const output = JSON.parse(lastCall)
218
+ expect(output.error).toContain('Not authenticated')
219
+ expect(exitSpy).toHaveBeenCalledWith(1)
220
+ })
221
+ })
222
+ })
@@ -0,0 +1,180 @@
1
+ import { Command } from 'commander'
2
+
3
+ import { handleError } from '@/shared/utils/error-handler'
4
+ import { formatOutput } from '@/shared/utils/output'
5
+
6
+ import { getWebexAppCredentials } from '../app-config'
7
+ import { WebexClient } from '../client'
8
+ import { WebexCredentialManager } from '../credential-manager'
9
+
10
+ interface ResolvedCredentials {
11
+ clientId: string
12
+ clientSecret: string
13
+ }
14
+
15
+ async function openBrowser(url: string): Promise<void> {
16
+ const { exec } = await import('node:child_process')
17
+ const command =
18
+ process.platform === 'darwin'
19
+ ? `open "${url}"`
20
+ : process.platform === 'win32'
21
+ ? `start "" "${url}"`
22
+ : `xdg-open "${url}"`
23
+ exec(command)
24
+ }
25
+
26
+ async function resolveClientCredentials(options: {
27
+ clientId?: string
28
+ clientSecret?: string
29
+ }): Promise<ResolvedCredentials> {
30
+ // 1. CLI flags
31
+ if (options.clientId || options.clientSecret) {
32
+ if (!options.clientId || !options.clientSecret) {
33
+ throw new Error('Both --client-id and --client-secret must be provided together.')
34
+ }
35
+ return { clientId: options.clientId, clientSecret: options.clientSecret }
36
+ }
37
+
38
+ // 2. Env vars → 3. Built-in defaults (always resolves)
39
+ return getWebexAppCredentials()
40
+ }
41
+
42
+ export async function loginAction(options: { token?: string; clientId?: string; clientSecret?: string; pretty?: boolean }): Promise<void> {
43
+ try {
44
+ const credManager = new WebexCredentialManager()
45
+
46
+ if (options.token) {
47
+ const client = await new WebexClient().login({ token: options.token })
48
+ const person = await client.testAuth()
49
+ await credManager.saveConfig({
50
+ accessToken: options.token,
51
+ refreshToken: '',
52
+ expiresAt: 0,
53
+ tokenType: 'manual',
54
+ })
55
+ console.log(
56
+ formatOutput(
57
+ {
58
+ user: { id: person.id, displayName: person.displayName, emails: person.emails },
59
+ authenticated: true,
60
+ },
61
+ options.pretty,
62
+ ),
63
+ )
64
+ return
65
+ }
66
+
67
+ const { clientId, clientSecret } = await resolveClientCredentials(options)
68
+
69
+ const device = await credManager.requestDeviceCode(clientId)
70
+
71
+ console.error(`Open this URL and enter the code: ${device.verificationUri}`)
72
+ console.error(`Code: ${device.userCode}`)
73
+ console.error('')
74
+ await openBrowser(device.verificationUriComplete)
75
+ console.error('Waiting for authorization...')
76
+
77
+ const config = await credManager.pollDeviceToken(
78
+ device.deviceCode,
79
+ device.interval,
80
+ device.expiresIn,
81
+ clientId,
82
+ clientSecret,
83
+ )
84
+
85
+ await credManager.saveConfig({ ...config, clientId, clientSecret, tokenType: 'oauth' })
86
+
87
+ const client = await new WebexClient().login({ token: config.accessToken })
88
+ const person = await client.testAuth()
89
+
90
+ console.log(
91
+ formatOutput(
92
+ {
93
+ user: { id: person.id, displayName: person.displayName, emails: person.emails },
94
+ authenticated: true,
95
+ },
96
+ options.pretty,
97
+ ),
98
+ )
99
+ } catch (error) {
100
+ handleError(error as Error)
101
+ }
102
+ }
103
+
104
+ export async function statusAction(options: { pretty?: boolean }): Promise<void> {
105
+ try {
106
+ const credManager = new WebexCredentialManager()
107
+ const config = await credManager.loadConfig()
108
+ const token = await credManager.getToken(config?.clientId, config?.clientSecret)
109
+
110
+ if (!token) {
111
+ console.log(
112
+ formatOutput({ error: 'Not authenticated. Run "auth login" first.' }, options.pretty),
113
+ )
114
+ process.exit(1)
115
+ return
116
+ }
117
+
118
+ try {
119
+ const client = await new WebexClient().login({ token })
120
+ const person = await client.testAuth()
121
+ console.log(
122
+ formatOutput(
123
+ {
124
+ authenticated: true,
125
+ user: { id: person.id, displayName: person.displayName, emails: person.emails },
126
+ },
127
+ options.pretty,
128
+ ),
129
+ )
130
+ } catch {
131
+ console.log(formatOutput({ authenticated: false, user: null }, options.pretty))
132
+ }
133
+ } catch (error) {
134
+ handleError(error as Error)
135
+ }
136
+ }
137
+
138
+ export async function logoutAction(options: { pretty?: boolean }): Promise<void> {
139
+ try {
140
+ const credManager = new WebexCredentialManager()
141
+ const config = await credManager.loadConfig()
142
+
143
+ if (!config) {
144
+ console.log(
145
+ formatOutput({ error: 'Not authenticated. Run "auth login" first.' }, options.pretty),
146
+ )
147
+ process.exit(1)
148
+ return
149
+ }
150
+
151
+ await credManager.clearCredentials()
152
+ console.log(formatOutput({ removed: 'webex', success: true }, options.pretty))
153
+ } catch (error) {
154
+ handleError(error as Error)
155
+ }
156
+ }
157
+
158
+ export const authCommand = new Command('auth')
159
+ .description('Authentication commands')
160
+ .addCommand(
161
+ new Command('login')
162
+ .description('Login to Webex')
163
+ .option('--token <token>', 'Use a bot token or personal access token directly')
164
+ .option('--client-id <id>', 'Webex Integration client ID')
165
+ .option('--client-secret <secret>', 'Webex Integration client secret')
166
+ .option('--pretty', 'Pretty print JSON output')
167
+ .action(loginAction),
168
+ )
169
+ .addCommand(
170
+ new Command('status')
171
+ .description('Show authentication status')
172
+ .option('--pretty', 'Pretty print JSON output')
173
+ .action(statusAction),
174
+ )
175
+ .addCommand(
176
+ new Command('logout')
177
+ .description('Logout from Webex')
178
+ .option('--pretty', 'Pretty print JSON output')
179
+ .action(logoutAction),
180
+ )
@@ -0,0 +1,5 @@
1
+ export { authCommand } from './auth'
2
+ export { memberCommand } from './member'
3
+ export { messageCommand } from './message'
4
+ export { snapshotAction, snapshotCommand } from './snapshot'
5
+ export { spaceCommand } from './space'
@@ -0,0 +1,103 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from 'bun:test'
2
+
3
+ import { WebexClient } from '../client'
4
+ import { WebexError } from '../types'
5
+ import { listAction } from './member'
6
+
7
+ describe('member commands', () => {
8
+ let consoleSpy: ReturnType<typeof spyOn>
9
+ let consoleErrorSpy: ReturnType<typeof spyOn>
10
+ let processExitSpy: ReturnType<typeof spyOn>
11
+ const mockMembers = [
12
+ {
13
+ id: 'mem-1',
14
+ roomId: 'room-1',
15
+ personId: 'person-1',
16
+ personEmail: 'alice@example.com',
17
+ personDisplayName: 'Alice',
18
+ isModerator: true,
19
+ created: '2024-01-01T00:00:00.000Z',
20
+ },
21
+ {
22
+ id: 'mem-2',
23
+ roomId: 'room-1',
24
+ personId: 'person-2',
25
+ personEmail: 'bob@example.com',
26
+ personDisplayName: 'Bob',
27
+ isModerator: false,
28
+ created: '2024-01-02T00:00:00.000Z',
29
+ },
30
+ ]
31
+
32
+ beforeEach(() => {
33
+ consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
34
+ consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
35
+ processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => {
36
+ throw new Error(`process.exit(${_code})`)
37
+ })
38
+ spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient() as any)
39
+ spyOn(WebexClient.prototype, 'listMemberships').mockResolvedValue(mockMembers)
40
+ })
41
+
42
+ afterEach(() => {
43
+ mock.restore()
44
+ })
45
+
46
+ test('listAction calls listMemberships with spaceId and outputs mapped members', async () => {
47
+ const listMembershipsSpy = spyOn(WebexClient.prototype, 'listMemberships').mockResolvedValue(
48
+ mockMembers,
49
+ )
50
+
51
+ await listAction('room-1', {})
52
+
53
+ expect(listMembershipsSpy).toHaveBeenCalledWith('room-1', { max: undefined })
54
+ expect(consoleSpy).toHaveBeenCalledWith(
55
+ JSON.stringify([
56
+ {
57
+ id: 'mem-1',
58
+ personId: 'person-1',
59
+ personEmail: 'alice@example.com',
60
+ personDisplayName: 'Alice',
61
+ isModerator: true,
62
+ created: '2024-01-01T00:00:00.000Z',
63
+ },
64
+ {
65
+ id: 'mem-2',
66
+ personId: 'person-2',
67
+ personEmail: 'bob@example.com',
68
+ personDisplayName: 'Bob',
69
+ isModerator: false,
70
+ created: '2024-01-02T00:00:00.000Z',
71
+ },
72
+ ]),
73
+ )
74
+ })
75
+
76
+ test('listAction passes limit option to listMemberships', async () => {
77
+ const listMembershipsSpy = spyOn(WebexClient.prototype, 'listMemberships').mockResolvedValue(
78
+ mockMembers,
79
+ )
80
+
81
+ await listAction('room-1', { limit: 25 })
82
+
83
+ expect(listMembershipsSpy).toHaveBeenCalledWith('room-1', { max: 25 })
84
+ })
85
+
86
+ test('listAction handles not-authenticated case', async () => {
87
+ spyOn(WebexClient.prototype, 'login').mockRejectedValue(
88
+ new WebexError('No Webex credentials found.', 'no_credentials'),
89
+ )
90
+
91
+ await expect(listAction('room-1', {})).rejects.toThrow('process.exit(1)')
92
+
93
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('No Webex credentials found'))
94
+ })
95
+
96
+ test('listAction handles API error', async () => {
97
+ spyOn(WebexClient.prototype, 'listMemberships').mockRejectedValue(new Error('API failure'))
98
+
99
+ await expect(listAction('room-1', {})).rejects.toThrow('process.exit(1)')
100
+
101
+ expect(processExitSpy).toHaveBeenCalledWith(1)
102
+ })
103
+ })
@@ -0,0 +1,45 @@
1
+ import { Command } from 'commander'
2
+
3
+ import { handleError } from '@/shared/utils/error-handler'
4
+ import { formatOutput } from '@/shared/utils/output'
5
+
6
+ import { WebexClient } from '../client'
7
+
8
+ export async function listAction(
9
+ spaceId: string,
10
+ options: { limit?: number; pretty?: boolean },
11
+ ): Promise<void> {
12
+ try {
13
+ const client = await new WebexClient().login()
14
+ const members = await client.listMemberships(spaceId, { max: options.limit })
15
+
16
+ const output = members.map((m) => ({
17
+ id: m.id,
18
+ personId: m.personId,
19
+ personEmail: m.personEmail,
20
+ personDisplayName: m.personDisplayName,
21
+ isModerator: m.isModerator,
22
+ created: m.created,
23
+ }))
24
+
25
+ console.log(formatOutput(output, options.pretty))
26
+ } catch (error) {
27
+ handleError(error as Error)
28
+ }
29
+ }
30
+
31
+ export const memberCommand = new Command('member')
32
+ .description('Member commands')
33
+ .addCommand(
34
+ new Command('list')
35
+ .description('List members of a space')
36
+ .argument('<space-id>', 'Space ID')
37
+ .option('--limit <n>', 'Number of members to retrieve', '100')
38
+ .option('--pretty', 'Pretty print JSON output')
39
+ .action((spaceId, options) =>
40
+ listAction(spaceId, {
41
+ limit: parseInt(options.limit, 10),
42
+ pretty: options.pretty,
43
+ }),
44
+ ),
45
+ )