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,231 @@
1
+ import { afterEach, beforeEach, expect, mock, spyOn, test } from 'bun:test'
2
+
3
+ import { WebexClient } from '../client'
4
+ import { WebexError } from '../types'
5
+ import { deleteAction, dmAction, editAction, getAction, listAction, sendAction } from './message'
6
+
7
+ let clientSendMessageSpy: ReturnType<typeof spyOn>
8
+ let clientSendDirectMessageSpy: ReturnType<typeof spyOn>
9
+ let clientListMessagesSpy: ReturnType<typeof spyOn>
10
+ let clientGetMessageSpy: ReturnType<typeof spyOn>
11
+ let clientDeleteMessageSpy: ReturnType<typeof spyOn>
12
+ let clientEditMessageSpy: ReturnType<typeof spyOn>
13
+ let clientLoginSpy: ReturnType<typeof spyOn>
14
+ const originalConsoleLog = console.log
15
+ const originalConsoleError = console.error
16
+
17
+ const mockMessage = {
18
+ id: 'msg_123',
19
+ roomId: 'space_456',
20
+ roomType: 'group' as const,
21
+ text: 'Hello world',
22
+ personId: 'person_789',
23
+ personEmail: 'user@example.com',
24
+ created: '2025-01-29T10:00:00.000Z',
25
+ }
26
+
27
+ const mockMessage2 = {
28
+ id: 'msg_124',
29
+ roomId: 'space_456',
30
+ roomType: 'group' as const,
31
+ text: 'Second message',
32
+ personId: 'person_789',
33
+ personEmail: 'user@example.com',
34
+ created: '2025-01-29T10:01:00.000Z',
35
+ }
36
+
37
+ beforeEach(() => {
38
+ clientLoginSpy = spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient() as any)
39
+
40
+ clientSendMessageSpy = spyOn(WebexClient.prototype, 'sendMessage').mockResolvedValue(mockMessage)
41
+
42
+ clientSendDirectMessageSpy = spyOn(WebexClient.prototype, 'sendDirectMessage').mockResolvedValue(mockMessage)
43
+
44
+ clientListMessagesSpy = spyOn(WebexClient.prototype, 'listMessages').mockResolvedValue([
45
+ mockMessage,
46
+ mockMessage2,
47
+ ])
48
+
49
+ clientGetMessageSpy = spyOn(WebexClient.prototype, 'getMessage').mockResolvedValue(mockMessage)
50
+
51
+ clientDeleteMessageSpy = spyOn(WebexClient.prototype, 'deleteMessage').mockResolvedValue(
52
+ undefined,
53
+ )
54
+
55
+ clientEditMessageSpy = spyOn(WebexClient.prototype, 'editMessage').mockResolvedValue({
56
+ ...mockMessage,
57
+ text: 'Updated message',
58
+ })
59
+ })
60
+
61
+ afterEach(() => {
62
+ clientLoginSpy?.mockRestore()
63
+ clientSendMessageSpy?.mockRestore()
64
+ clientSendDirectMessageSpy?.mockRestore()
65
+ clientListMessagesSpy?.mockRestore()
66
+ clientGetMessageSpy?.mockRestore()
67
+ clientDeleteMessageSpy?.mockRestore()
68
+ clientEditMessageSpy?.mockRestore()
69
+ console.log = originalConsoleLog
70
+ console.error = originalConsoleError
71
+ })
72
+
73
+ test('send: calls sendMessage with correct args and outputs result', async () => {
74
+ const consoleSpy = mock((_msg: string) => {})
75
+ console.log = consoleSpy
76
+
77
+ await sendAction('space_456', 'Hello world', { pretty: false })
78
+
79
+ expect(clientSendMessageSpy).toHaveBeenCalledWith('space_456', 'Hello world', {
80
+ markdown: undefined,
81
+ })
82
+ expect(consoleSpy).toHaveBeenCalled()
83
+ const output = consoleSpy.mock.calls[0][0]
84
+ expect(output).toContain('msg_123')
85
+ expect(output).toContain('space_456')
86
+ expect(output).toContain('user@example.com')
87
+ })
88
+
89
+ test('send: with --markdown passes markdown option', async () => {
90
+ const consoleSpy = mock((_msg: string) => {})
91
+ console.log = consoleSpy
92
+
93
+ await sendAction('space_456', '**bold**', { markdown: true, pretty: false })
94
+
95
+ expect(clientSendMessageSpy).toHaveBeenCalledWith('space_456', '**bold**', { markdown: true })
96
+ })
97
+
98
+ test('send: not authenticated shows error', async () => {
99
+ clientLoginSpy.mockRejectedValue(new WebexError('No Webex credentials found.', 'no_credentials'))
100
+ const errorSpy = mock((_msg: string) => {})
101
+ console.error = errorSpy
102
+
103
+ const originalExit = process.exit
104
+ process.exit = mock((_code?: number) => {
105
+ throw new Error('process.exit called')
106
+ }) as never
107
+
108
+ try {
109
+ await sendAction('space_456', 'Hello', { pretty: false })
110
+ } catch {
111
+ } finally {
112
+ process.exit = originalExit
113
+ }
114
+
115
+ expect(errorSpy).toHaveBeenCalled()
116
+ const output = errorSpy.mock.calls[0][0]
117
+ expect(output).toContain('No Webex credentials found')
118
+ })
119
+
120
+ test('dm: calls sendDirectMessage with email and text', async () => {
121
+ const consoleSpy = mock((_msg: string) => {})
122
+ console.log = consoleSpy
123
+
124
+ await dmAction('alice@example.com', 'Hello!', { pretty: false })
125
+
126
+ expect(clientSendDirectMessageSpy).toHaveBeenCalledWith('alice@example.com', 'Hello!', {
127
+ markdown: undefined,
128
+ })
129
+ expect(consoleSpy).toHaveBeenCalled()
130
+ const output = consoleSpy.mock.calls[0][0]
131
+ expect(output).toContain('msg_123')
132
+ })
133
+
134
+ test('dm: with --markdown passes markdown option', async () => {
135
+ const consoleSpy = mock((_msg: string) => {})
136
+ console.log = consoleSpy
137
+
138
+ await dmAction('alice@example.com', '**bold**', { markdown: true, pretty: false })
139
+
140
+ expect(clientSendDirectMessageSpy).toHaveBeenCalledWith('alice@example.com', '**bold**', {
141
+ markdown: true,
142
+ })
143
+ })
144
+
145
+ test('list: calls listMessages with limit and outputs array', async () => {
146
+ const consoleSpy = mock((_msg: string) => {})
147
+ console.log = consoleSpy
148
+
149
+ await listAction('space_456', { limit: 50, pretty: false })
150
+
151
+ expect(clientListMessagesSpy).toHaveBeenCalledWith('space_456', { max: 50 })
152
+ expect(consoleSpy).toHaveBeenCalled()
153
+ const output = consoleSpy.mock.calls[0][0]
154
+ expect(output).toContain('msg_123')
155
+ expect(output).toContain('msg_124')
156
+ })
157
+
158
+ test('get: calls getMessage with correct id and outputs result', async () => {
159
+ const consoleSpy = mock((_msg: string) => {})
160
+ console.log = consoleSpy
161
+
162
+ await getAction('msg_123', { pretty: false })
163
+
164
+ expect(clientGetMessageSpy).toHaveBeenCalledWith('msg_123')
165
+ expect(consoleSpy).toHaveBeenCalled()
166
+ const output = consoleSpy.mock.calls[0][0]
167
+ expect(output).toContain('msg_123')
168
+ expect(output).toContain('user@example.com')
169
+ })
170
+
171
+ test('delete: with --force calls deleteMessage and outputs deleted id', async () => {
172
+ const consoleSpy = mock((_msg: string) => {})
173
+ console.log = consoleSpy
174
+
175
+ await deleteAction('msg_123', { force: true, pretty: false })
176
+
177
+ expect(clientDeleteMessageSpy).toHaveBeenCalledWith('msg_123')
178
+ expect(consoleSpy).toHaveBeenCalled()
179
+ const output = consoleSpy.mock.calls[0][0]
180
+ expect(output).toContain('deleted')
181
+ expect(output).toContain('msg_123')
182
+ })
183
+
184
+ test('delete: without --force shows warning and does not delete', async () => {
185
+ const consoleSpy = mock((_msg: string) => {})
186
+ console.log = consoleSpy
187
+
188
+ const originalExit = process.exit
189
+ process.exit = mock((_code?: number) => {
190
+ throw new Error('process.exit called')
191
+ }) as never
192
+
193
+ try {
194
+ await deleteAction('msg_123', { force: false, pretty: false })
195
+ } catch {
196
+ } finally {
197
+ process.exit = originalExit
198
+ }
199
+
200
+ expect(clientDeleteMessageSpy).not.toHaveBeenCalled()
201
+ expect(consoleSpy).toHaveBeenCalled()
202
+ const output = consoleSpy.mock.calls[0][0]
203
+ expect(output).toContain('warning')
204
+ expect(output).toContain('--force')
205
+ })
206
+
207
+ test('edit: calls editMessage with roomId in args and outputs result', async () => {
208
+ const consoleSpy = mock((_msg: string) => {})
209
+ console.log = consoleSpy
210
+
211
+ await editAction('msg_123', 'space_456', 'Updated message', { pretty: false })
212
+
213
+ expect(clientEditMessageSpy).toHaveBeenCalledWith('msg_123', 'space_456', 'Updated message', {
214
+ markdown: undefined,
215
+ })
216
+ expect(consoleSpy).toHaveBeenCalled()
217
+ const output = consoleSpy.mock.calls[0][0]
218
+ expect(output).toContain('msg_123')
219
+ expect(output).toContain('Updated message')
220
+ })
221
+
222
+ test('edit: with --markdown passes markdown option', async () => {
223
+ const consoleSpy = mock((_msg: string) => {})
224
+ console.log = consoleSpy
225
+
226
+ await editAction('msg_123', 'space_456', '**updated**', { markdown: true, pretty: false })
227
+
228
+ expect(clientEditMessageSpy).toHaveBeenCalledWith('msg_123', 'space_456', '**updated**', {
229
+ markdown: true,
230
+ })
231
+ })
@@ -0,0 +1,204 @@
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
+ import type { WebexMessage } from '../types'
8
+
9
+ export async function sendAction(
10
+ spaceId: string,
11
+ text: string,
12
+ options: { markdown?: boolean; pretty?: boolean },
13
+ ): Promise<void> {
14
+ try {
15
+ const client = await new WebexClient().login()
16
+ const message = await client.sendMessage(spaceId, text, { markdown: options.markdown })
17
+
18
+ const output = {
19
+ id: message.id,
20
+ roomId: message.roomId,
21
+ text: message.text,
22
+ personEmail: message.personEmail,
23
+ created: message.created,
24
+ }
25
+
26
+ console.log(formatOutput(output, options.pretty))
27
+ } catch (error) {
28
+ handleError(error as Error)
29
+ }
30
+ }
31
+
32
+ export async function listAction(
33
+ spaceId: string,
34
+ options: { limit?: number; pretty?: boolean },
35
+ ): Promise<void> {
36
+ try {
37
+ const client = await new WebexClient().login()
38
+ const limit = options.limit ?? 50
39
+ const messages = await client.listMessages(spaceId, { max: limit })
40
+
41
+ const output = messages.map((msg: WebexMessage) => ({
42
+ id: msg.id,
43
+ roomId: msg.roomId,
44
+ text: msg.text,
45
+ personEmail: msg.personEmail,
46
+ created: msg.created,
47
+ }))
48
+
49
+ console.log(formatOutput(output, options.pretty))
50
+ } catch (error) {
51
+ handleError(error as Error)
52
+ }
53
+ }
54
+
55
+ export async function getAction(
56
+ messageId: string,
57
+ options: { pretty?: boolean },
58
+ ): Promise<void> {
59
+ try {
60
+ const client = await new WebexClient().login()
61
+ const message = await client.getMessage(messageId)
62
+
63
+ const output = {
64
+ id: message.id,
65
+ roomId: message.roomId,
66
+ text: message.text,
67
+ personEmail: message.personEmail,
68
+ created: message.created,
69
+ }
70
+
71
+ console.log(formatOutput(output, options.pretty))
72
+ } catch (error) {
73
+ handleError(error as Error)
74
+ }
75
+ }
76
+
77
+ export async function deleteAction(
78
+ messageId: string,
79
+ options: { force?: boolean; pretty?: boolean },
80
+ ): Promise<void> {
81
+ try {
82
+ if (!options.force) {
83
+ console.log(
84
+ formatOutput({ warning: 'Use --force to confirm deletion', messageId }, options.pretty),
85
+ )
86
+ process.exit(0)
87
+ }
88
+
89
+ const client = await new WebexClient().login()
90
+ await client.deleteMessage(messageId)
91
+
92
+ console.log(formatOutput({ deleted: messageId }, options.pretty))
93
+ } catch (error) {
94
+ handleError(error as Error)
95
+ }
96
+ }
97
+
98
+ export async function editAction(
99
+ messageId: string,
100
+ spaceId: string,
101
+ text: string,
102
+ options: { markdown?: boolean; pretty?: boolean },
103
+ ): Promise<void> {
104
+ try {
105
+ const client = await new WebexClient().login()
106
+ const message = await client.editMessage(messageId, spaceId, text, {
107
+ markdown: options.markdown,
108
+ })
109
+
110
+ const output = {
111
+ id: message.id,
112
+ roomId: message.roomId,
113
+ text: message.text,
114
+ personEmail: message.personEmail,
115
+ created: message.created,
116
+ }
117
+
118
+ console.log(formatOutput(output, options.pretty))
119
+ } catch (error) {
120
+ handleError(error as Error)
121
+ }
122
+ }
123
+
124
+ export async function dmAction(
125
+ email: string,
126
+ text: string,
127
+ options: { markdown?: boolean; pretty?: boolean },
128
+ ): Promise<void> {
129
+ try {
130
+ const client = await new WebexClient().login()
131
+ const message = await client.sendDirectMessage(email, text, { markdown: options.markdown })
132
+
133
+ const output = {
134
+ id: message.id,
135
+ roomId: message.roomId,
136
+ text: message.text,
137
+ personEmail: message.personEmail,
138
+ created: message.created,
139
+ }
140
+
141
+ console.log(formatOutput(output, options.pretty))
142
+ } catch (error) {
143
+ handleError(error as Error)
144
+ }
145
+ }
146
+
147
+ export const messageCommand = new Command('message')
148
+ .description('Message commands')
149
+ .addCommand(
150
+ new Command('send')
151
+ .description('Send message to a space')
152
+ .argument('<space-id>', 'Space/Room ID')
153
+ .argument('<text>', 'Message text')
154
+ .option('--markdown', 'Send as markdown')
155
+ .option('--pretty', 'Pretty print JSON output')
156
+ .action(sendAction),
157
+ )
158
+ .addCommand(
159
+ new Command('dm')
160
+ .description('Send a direct message by email')
161
+ .argument('<email>', 'Recipient email address')
162
+ .argument('<text>', 'Message text')
163
+ .option('--markdown', 'Send as markdown')
164
+ .option('--pretty', 'Pretty print JSON output')
165
+ .action(dmAction),
166
+ )
167
+ .addCommand(
168
+ new Command('list')
169
+ .description('List messages from a space')
170
+ .argument('<space-id>', 'Space/Room ID')
171
+ .option('--limit <n>', 'Number of messages to retrieve', '50')
172
+ .option('--pretty', 'Pretty print JSON output')
173
+ .action((spaceId: string, options: { limit: string; pretty?: boolean }) => {
174
+ return listAction(spaceId, {
175
+ limit: parseInt(options.limit, 10),
176
+ pretty: options.pretty,
177
+ })
178
+ }),
179
+ )
180
+ .addCommand(
181
+ new Command('get')
182
+ .description('Get a single message by ID')
183
+ .argument('<message-id>', 'Message ID')
184
+ .option('--pretty', 'Pretty print JSON output')
185
+ .action(getAction),
186
+ )
187
+ .addCommand(
188
+ new Command('delete')
189
+ .description('Delete a message')
190
+ .argument('<message-id>', 'Message ID')
191
+ .option('--force', 'Skip confirmation')
192
+ .option('--pretty', 'Pretty print JSON output')
193
+ .action(deleteAction),
194
+ )
195
+ .addCommand(
196
+ new Command('edit')
197
+ .description('Edit a message')
198
+ .argument('<message-id>', 'Message ID')
199
+ .argument('<space-id>', 'Space/Room ID (required by Webex API)')
200
+ .argument('<text>', 'New message text')
201
+ .option('--markdown', 'Send as markdown')
202
+ .option('--pretty', 'Pretty print JSON output')
203
+ .action(editAction),
204
+ )
@@ -0,0 +1,96 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from 'bun:test'
2
+ import { WebexClient } from '../client'
3
+ import { WebexError } from '../types'
4
+ import { snapshotAction } from './snapshot'
5
+
6
+ describe('snapshot command', () => {
7
+ let consoleSpy: ReturnType<typeof spyOn>
8
+ let consoleErrorSpy: ReturnType<typeof spyOn>
9
+
10
+ const mockSpaces = [
11
+ { id: 'space-1', title: 'General', type: 'group', isLocked: false, lastActivity: '2024-01-15T00:00:00.000Z', created: '2024-01-01T00:00:00.000Z', creatorId: 'person-1' },
12
+ ]
13
+
14
+ const mockMessages = [
15
+ { id: 'msg-1', roomId: 'space-1', roomType: 'group', text: 'Hello', personId: 'person-1', personEmail: 'alice@example.com', created: '2024-01-15T00:00:00.000Z' },
16
+ ]
17
+
18
+ const mockMembers = [
19
+ { id: 'mem-1', roomId: 'space-1', personId: 'person-1', personEmail: 'alice@example.com', personDisplayName: 'Alice', isModerator: true, created: '2024-01-01T00:00:00.000Z' },
20
+ ]
21
+
22
+ beforeEach(() => {
23
+ consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
24
+ consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
25
+ spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient() as any)
26
+ spyOn(WebexClient.prototype, 'listSpaces').mockResolvedValue(mockSpaces as any)
27
+ spyOn(WebexClient.prototype, 'listMessages').mockResolvedValue(mockMessages as any)
28
+ spyOn(WebexClient.prototype, 'listMemberships').mockResolvedValue(mockMembers as any)
29
+ })
30
+
31
+ afterEach(() => { mock.restore() })
32
+
33
+ test('full snapshot includes spaces, recent_messages, members', async () => {
34
+ await snapshotAction({})
35
+
36
+ expect(consoleSpy).toHaveBeenCalled()
37
+ const output = JSON.parse(consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0])
38
+ expect(output.spaces).toBeDefined()
39
+ expect(output.spaces[0].id).toBe('space-1')
40
+ expect(output.spaces[0].title).toBe('General')
41
+ expect(output.recent_messages).toBeDefined()
42
+ expect(output.recent_messages[0].id).toBe('msg-1')
43
+ expect(output.recent_messages[0].author).toBe('alice@example.com')
44
+ expect(output.members).toBeDefined()
45
+ expect(output.members[0].personEmail).toBe('alice@example.com')
46
+ })
47
+
48
+ test('--spaces-only includes only spaces (no messages, no members)', async () => {
49
+ await snapshotAction({ spacesOnly: true })
50
+
51
+ expect(consoleSpy).toHaveBeenCalled()
52
+ const output = JSON.parse(consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0])
53
+ expect(output.spaces).toBeDefined()
54
+ expect(output.recent_messages).toBeUndefined()
55
+ expect(output.members).toBeUndefined()
56
+ })
57
+
58
+ test('--members-only includes only members (no spaces, no messages)', async () => {
59
+ await snapshotAction({ membersOnly: true })
60
+
61
+ expect(consoleSpy).toHaveBeenCalled()
62
+ const output = JSON.parse(consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0])
63
+ expect(output.spaces).toBeUndefined()
64
+ expect(output.recent_messages).toBeUndefined()
65
+ expect(output.members).toBeDefined()
66
+ expect(output.members[0].personEmail).toBe('alice@example.com')
67
+ })
68
+
69
+ test('not authenticated outputs error', async () => {
70
+ spyOn(WebexClient.prototype, 'login').mockRejectedValue(
71
+ new WebexError('No Webex credentials found.', 'no_credentials'),
72
+ )
73
+
74
+ const originalExit = process.exit
75
+ process.exit = mock((_code?: number) => { throw new Error('process.exit called') }) as never
76
+
77
+ try {
78
+ await snapshotAction({})
79
+ } catch {
80
+ } finally {
81
+ process.exit = originalExit
82
+ }
83
+
84
+ expect(consoleErrorSpy).toHaveBeenCalled()
85
+ const output = consoleErrorSpy.mock.calls[0][0]
86
+ expect(output).toContain('No Webex credentials found')
87
+ })
88
+
89
+ test('passes limit option to listMessages', async () => {
90
+ const listMessagesSpy = spyOn(WebexClient.prototype, 'listMessages').mockResolvedValue(mockMessages as any)
91
+
92
+ await snapshotAction({ limit: 5 })
93
+
94
+ expect(listMessagesSpy).toHaveBeenCalledWith('space-1', { max: 5 })
95
+ })
96
+ })
@@ -0,0 +1,91 @@
1
+ import { Command } from 'commander'
2
+ import { parallelMap } from '@/shared/utils/concurrency'
3
+ import { handleError } from '@/shared/utils/error-handler'
4
+ import { formatOutput } from '@/shared/utils/output'
5
+ import { WebexClient } from '../client'
6
+ import type { WebexSpace } from '../types'
7
+
8
+ export async function snapshotAction(options: {
9
+ spacesOnly?: boolean
10
+ membersOnly?: boolean
11
+ limit?: number
12
+ pretty?: boolean
13
+ }): Promise<void> {
14
+ try {
15
+ const client = await new WebexClient().login()
16
+ const messageLimit = options.limit || 20
17
+ const snapshot: Record<string, unknown> = {}
18
+
19
+ if (!options.membersOnly) {
20
+ const spaces = await client.listSpaces({ max: 50 })
21
+ snapshot.spaces = spaces.map((s) => ({
22
+ id: s.id,
23
+ title: s.title,
24
+ type: s.type,
25
+ lastActivity: s.lastActivity,
26
+ }))
27
+
28
+ if (!options.spacesOnly) {
29
+ const spaceMessages = await parallelMap(
30
+ spaces,
31
+ async (space: WebexSpace) => {
32
+ const messages = await client.listMessages(space.id, { max: messageLimit })
33
+ return messages.map((msg) => ({
34
+ ...msg,
35
+ space_title: space.title,
36
+ }))
37
+ },
38
+ 5,
39
+ )
40
+
41
+ snapshot.recent_messages = spaceMessages.flat().map((msg) => ({
42
+ space_id: msg.roomId,
43
+ space_title: msg.space_title,
44
+ id: msg.id,
45
+ author: msg.personEmail,
46
+ text: msg.text || msg.markdown || '',
47
+ created: msg.created,
48
+ }))
49
+ }
50
+ }
51
+
52
+ if (!options.spacesOnly) {
53
+ // Get members for the first few spaces (avoid massive API calls)
54
+ const spaces = await client.listSpaces({ max: 10 })
55
+ const spaceMembers = await parallelMap(
56
+ spaces,
57
+ async (space: WebexSpace) => {
58
+ const members = await client.listMemberships(space.id, { max: 100 })
59
+ return members.map((m) => ({
60
+ space_id: space.id,
61
+ space_title: space.title,
62
+ personEmail: m.personEmail,
63
+ personDisplayName: m.personDisplayName,
64
+ isModerator: m.isModerator,
65
+ }))
66
+ },
67
+ 5,
68
+ )
69
+ snapshot.members = spaceMembers.flat()
70
+ }
71
+
72
+ console.log(formatOutput(snapshot, options.pretty))
73
+ } catch (error) {
74
+ handleError(error as Error)
75
+ }
76
+ }
77
+
78
+ export const snapshotCommand = new Command('snapshot')
79
+ .description('Get comprehensive workspace state for AI agents')
80
+ .option('--spaces-only', 'Include only spaces (exclude messages and members)')
81
+ .option('--members-only', 'Include only members (exclude spaces and messages)')
82
+ .option('--limit <n>', 'Number of recent messages per space (default: 20)', '20')
83
+ .option('--pretty', 'Pretty print JSON output')
84
+ .action(async (options) => {
85
+ await snapshotAction({
86
+ spacesOnly: options.spacesOnly,
87
+ membersOnly: options.membersOnly,
88
+ limit: parseInt(options.limit, 10),
89
+ pretty: options.pretty,
90
+ })
91
+ })