agent-messenger 2.6.2 → 2.7.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/plugin.json +1 -1
- package/dist/package.json +1 -1
- package/dist/src/platforms/webex/client.d.ts +3 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.js +22 -8
- package/dist/src/platforms/webex/client.js.map +1 -1
- package/dist/src/platforms/webex/commands/snapshot.d.ts +0 -3
- package/dist/src/platforms/webex/commands/snapshot.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/snapshot.js +9 -47
- package/dist/src/platforms/webex/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/webex/markdown-to-html.d.ts +3 -0
- package/dist/src/platforms/webex/markdown-to-html.d.ts.map +1 -0
- package/dist/src/platforms/webex/markdown-to-html.js +161 -0
- package/dist/src/platforms/webex/markdown-to-html.js.map +1 -0
- package/docs/content/docs/cli/webex.mdx +3 -11
- package/package.json +1 -1
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +6 -14
- package/skills/agent-webex/references/common-patterns.md +3 -32
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/webex/client.test.ts +35 -0
- package/src/platforms/webex/client.ts +26 -9
- package/src/platforms/webex/commands/snapshot.test.ts +13 -41
- package/src/platforms/webex/commands/snapshot.ts +10 -61
- package/src/platforms/webex/markdown-to-html.test.ts +153 -0
- package/src/platforms/webex/markdown-to-html.ts +194 -0
- package/src/platforms/wechatbot/cli.ts +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-webex
|
|
3
3
|
description: Interact with Cisco Webex - send messages, read spaces, manage memberships
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.7.0
|
|
5
5
|
allowed-tools: Bash(agent-webex:*)
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
|
@@ -116,7 +116,7 @@ At the **start of every task**, read `~/.config/agent-messenger/MEMORY.md` using
|
|
|
116
116
|
After discovering useful information, update `~/.config/agent-messenger/MEMORY.md` using the `Write` tool. Write triggers include:
|
|
117
117
|
|
|
118
118
|
- After discovering space IDs and titles (from `space list`, `snapshot`, etc.)
|
|
119
|
-
- After discovering member IDs and names (from `member list`,
|
|
119
|
+
- After discovering member IDs and names (from `member list`, etc.)
|
|
120
120
|
- After the user gives you an alias or preference ("call this the standup space", "my main space is X")
|
|
121
121
|
- After discovering space structure (group vs direct spaces)
|
|
122
122
|
|
|
@@ -247,25 +247,17 @@ agent-webex member list <space-id> --limit 100
|
|
|
247
247
|
|
|
248
248
|
### Snapshot Command
|
|
249
249
|
|
|
250
|
-
Get
|
|
250
|
+
Get workspace spaces overview for AI agents:
|
|
251
251
|
|
|
252
252
|
```bash
|
|
253
|
-
# Full snapshot
|
|
254
253
|
agent-webex snapshot
|
|
255
|
-
|
|
256
|
-
# Filtered snapshots
|
|
257
|
-
agent-webex snapshot --spaces-only
|
|
258
|
-
agent-webex snapshot --members-only
|
|
259
|
-
|
|
260
|
-
# Limit messages per space
|
|
261
|
-
agent-webex snapshot --limit 10
|
|
262
254
|
```
|
|
263
255
|
|
|
264
256
|
Returns JSON with:
|
|
265
257
|
|
|
266
|
-
- Spaces (id, title, type,
|
|
267
|
-
|
|
268
|
-
|
|
258
|
+
- Spaces (id, title, type, lastActivity) — only spaces you're a member of
|
|
259
|
+
|
|
260
|
+
For messages or members, use `message list <space-id>` or `member list <space-id>`.
|
|
269
261
|
|
|
270
262
|
## Output Format
|
|
271
263
|
|
|
@@ -323,9 +323,9 @@ echo "Found: $(echo "$MATCH" | jq -r '.personDisplayName') ($(echo "$MATCH" | jq
|
|
|
323
323
|
|
|
324
324
|
## Snapshot Patterns
|
|
325
325
|
|
|
326
|
-
### Pattern 19:
|
|
326
|
+
### Pattern 19: Workspace Snapshot
|
|
327
327
|
|
|
328
|
-
**Use case**: Get
|
|
328
|
+
**Use case**: Get spaces overview for AI context
|
|
329
329
|
|
|
330
330
|
```bash
|
|
331
331
|
#!/bin/bash
|
|
@@ -339,36 +339,7 @@ echo "Total spaces: $SPACE_COUNT"
|
|
|
339
339
|
echo "$SNAPSHOT" | jq -r '.spaces[] | " \(.title) (\(.type))"'
|
|
340
340
|
```
|
|
341
341
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
**Use case**: Quick overview without messages or members
|
|
345
|
-
|
|
346
|
-
```bash
|
|
347
|
-
#!/bin/bash
|
|
348
|
-
|
|
349
|
-
agent-webex snapshot --spaces-only
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
### Pattern 21: Members-Only Snapshot
|
|
353
|
-
|
|
354
|
-
**Use case**: Get member lists across all spaces
|
|
355
|
-
|
|
356
|
-
```bash
|
|
357
|
-
#!/bin/bash
|
|
358
|
-
|
|
359
|
-
agent-webex snapshot --members-only
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
### Pattern 22: Snapshot with Message Limit
|
|
363
|
-
|
|
364
|
-
**Use case**: Control how many messages per space
|
|
365
|
-
|
|
366
|
-
```bash
|
|
367
|
-
#!/bin/bash
|
|
368
|
-
|
|
369
|
-
# Get snapshot with last 5 messages per space
|
|
370
|
-
agent-webex snapshot --limit 5
|
|
371
|
-
```
|
|
342
|
+
**When to use**: Quick workspace overview to discover space IDs and titles. Use `message list <space-id>` or `member list <space-id>` for details.
|
|
372
343
|
|
|
373
344
|
## Pipeline Patterns
|
|
374
345
|
|
|
@@ -483,6 +483,29 @@ describe('WebexClient', () => {
|
|
|
483
483
|
expect(message.personEmail).toBe('test@example.com')
|
|
484
484
|
expect(message.created).toBe('2026-01-01T00:00:00.000Z')
|
|
485
485
|
})
|
|
486
|
+
|
|
487
|
+
test('markdown option converts content to HTML and strips displayName', async () => {
|
|
488
|
+
mockResponse(mockActivity('bold text'))
|
|
489
|
+
|
|
490
|
+
const client = await createExtractedClient()
|
|
491
|
+
await client.sendMessage(TEST_ROOM_ID, '**bold text**', { markdown: true })
|
|
492
|
+
|
|
493
|
+
const body = JSON.parse(fetchCalls[0].options?.body as string)
|
|
494
|
+
expect(body.object.displayName).toBe('bold text')
|
|
495
|
+
expect(body.object.content).toBe('<strong>bold text</strong>')
|
|
496
|
+
expect(body.object.markdown).toBeUndefined()
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
test('markdown option does not affect plain text messages', async () => {
|
|
500
|
+
mockResponse(mockActivity('Hello world'))
|
|
501
|
+
|
|
502
|
+
const client = await createExtractedClient()
|
|
503
|
+
await client.sendMessage(TEST_ROOM_ID, 'Hello world')
|
|
504
|
+
|
|
505
|
+
const body = JSON.parse(fetchCalls[0].options?.body as string)
|
|
506
|
+
expect(body.object.displayName).toBe('Hello world')
|
|
507
|
+
expect(body.object.content).toBe('Hello world')
|
|
508
|
+
})
|
|
486
509
|
})
|
|
487
510
|
|
|
488
511
|
describe('listMessages', () => {
|
|
@@ -637,6 +660,18 @@ describe('WebexClient', () => {
|
|
|
637
660
|
const body = JSON.parse(fetchCalls[0].options?.body as string)
|
|
638
661
|
expect(body.target.id).toBe(TEST_CONV_UUID)
|
|
639
662
|
})
|
|
663
|
+
|
|
664
|
+
test('markdown option converts content to HTML and strips displayName', async () => {
|
|
665
|
+
mockResponse(mockActivity('italic text'))
|
|
666
|
+
|
|
667
|
+
const client = await createExtractedClient()
|
|
668
|
+
await client.editMessage('activity-123', TEST_ROOM_ID, '_italic text_', { markdown: true })
|
|
669
|
+
|
|
670
|
+
const body = JSON.parse(fetchCalls[0].options?.body as string)
|
|
671
|
+
expect(body.object.displayName).toBe('italic text')
|
|
672
|
+
expect(body.object.content).toBe('<em>italic text</em>')
|
|
673
|
+
expect(body.object.markdown).toBeUndefined()
|
|
674
|
+
})
|
|
640
675
|
})
|
|
641
676
|
|
|
642
677
|
describe('sendDirectMessage', () => {
|
|
@@ -2,6 +2,7 @@ import type { WebexMembership, WebexMessage, WebexPerson, WebexSpace } from './t
|
|
|
2
2
|
import { WebexError } from './types'
|
|
3
3
|
import { WebexCredentialManager } from './credential-manager'
|
|
4
4
|
import { WebexEncryptionService } from './encryption'
|
|
5
|
+
import { markdownToHtml, stripMarkdown } from './markdown-to-html'
|
|
5
6
|
|
|
6
7
|
const BASE_URL = 'https://webexapis.com/v1'
|
|
7
8
|
const MAX_RETRIES = 3
|
|
@@ -237,7 +238,7 @@ export class WebexClient {
|
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
private async activityToMessage(a: InternalActivity, roomId: string): Promise<WebexMessage> {
|
|
240
|
-
let text = a.object?.
|
|
241
|
+
let text = a.object?.displayName ?? a.object?.content
|
|
241
242
|
|
|
242
243
|
if (this.encryption && text?.startsWith('eyJ')) {
|
|
243
244
|
const keyUrl = a.encryptionKeyUrl ?? a.object?.encryptionKeyUrl
|
|
@@ -265,10 +266,8 @@ export class WebexClient {
|
|
|
265
266
|
text: string,
|
|
266
267
|
options?: { markdown?: boolean },
|
|
267
268
|
): Promise<{ object: Record<string, string>; encryptionKeyUrl?: string }> {
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
? { objectType: 'comment', displayName: content, content, markdown: content }
|
|
271
|
-
: { objectType: 'comment', displayName: content, content }
|
|
269
|
+
const displayName = options?.markdown ? stripMarkdown(text) : text
|
|
270
|
+
const content = options?.markdown ? markdownToHtml(text) : text
|
|
272
271
|
|
|
273
272
|
if (this.encryption) {
|
|
274
273
|
const conv = await this.internalRequest<InternalConversation>(
|
|
@@ -276,14 +275,22 @@ export class WebexClient {
|
|
|
276
275
|
)
|
|
277
276
|
const keyUri = conv.defaultActivityEncryptionKeyUrl
|
|
278
277
|
if (keyUri) {
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
278
|
+
const encryptedDisplayName = await this.encryption.encryptText(keyUri, displayName)
|
|
279
|
+
const encryptedContent = await this.encryption.encryptText(keyUri, content)
|
|
280
|
+
if (encryptedDisplayName && encryptedContent) {
|
|
281
|
+
return {
|
|
282
|
+
object: {
|
|
283
|
+
objectType: 'comment',
|
|
284
|
+
displayName: encryptedDisplayName,
|
|
285
|
+
content: encryptedContent,
|
|
286
|
+
},
|
|
287
|
+
encryptionKeyUrl: keyUri,
|
|
288
|
+
}
|
|
282
289
|
}
|
|
283
290
|
}
|
|
284
291
|
}
|
|
285
292
|
|
|
286
|
-
return { object:
|
|
293
|
+
return { object: { objectType: 'comment', displayName, content } }
|
|
287
294
|
}
|
|
288
295
|
|
|
289
296
|
private async sendMessageInternal(
|
|
@@ -438,6 +445,16 @@ export class WebexClient {
|
|
|
438
445
|
return data.items
|
|
439
446
|
}
|
|
440
447
|
|
|
448
|
+
async listMyMemberships(options?: { max?: number }): Promise<WebexMembership[]> {
|
|
449
|
+
const params = new URLSearchParams()
|
|
450
|
+
params.set('max', String(options?.max ?? 100))
|
|
451
|
+
const data = await this.request<{ items: WebexMembership[] }>(
|
|
452
|
+
'GET',
|
|
453
|
+
`/memberships?${params}`,
|
|
454
|
+
)
|
|
455
|
+
return data.items
|
|
456
|
+
}
|
|
457
|
+
|
|
441
458
|
async listMemberships(
|
|
442
459
|
roomId: string,
|
|
443
460
|
options?: { max?: number },
|
|
@@ -11,24 +11,19 @@ mock.module('@/shared/utils/error-handler', () => ({
|
|
|
11
11
|
|
|
12
12
|
const mockSpaces = [
|
|
13
13
|
{ 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' },
|
|
14
|
+
{ id: 'space-2', title: 'Random', type: 'group', isLocked: false, lastActivity: '2024-01-14T00:00:00.000Z', created: '2024-01-01T00:00:00.000Z', creatorId: 'person-1' },
|
|
14
15
|
]
|
|
15
16
|
|
|
16
|
-
const
|
|
17
|
-
{ id: 'msg-1', roomId: 'space-1', roomType: 'group', text: 'Hello', personId: 'person-1', personEmail: 'alice@example.com', created: '2024-01-15T00:00:00.000Z' },
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
const mockMembers = [
|
|
17
|
+
const mockMyMemberships = [
|
|
21
18
|
{ id: 'mem-1', roomId: 'space-1', personId: 'person-1', personEmail: 'alice@example.com', personDisplayName: 'Alice', isModerator: true, created: '2024-01-01T00:00:00.000Z' },
|
|
22
19
|
]
|
|
23
20
|
|
|
24
21
|
const mockListSpaces = mock(() => Promise.resolve(mockSpaces as any))
|
|
25
|
-
const
|
|
26
|
-
const mockListMemberships = mock(() => Promise.resolve(mockMembers as any))
|
|
22
|
+
const mockListMyMemberships = mock(() => Promise.resolve(mockMyMemberships as any))
|
|
27
23
|
|
|
28
24
|
const mockClient = {
|
|
29
25
|
listSpaces: mockListSpaces,
|
|
30
|
-
|
|
31
|
-
listMemberships: mockListMemberships,
|
|
26
|
+
listMyMemberships: mockListMyMemberships,
|
|
32
27
|
}
|
|
33
28
|
|
|
34
29
|
const mockLogin = mock(() => Promise.resolve(mockClient))
|
|
@@ -46,8 +41,7 @@ describe('snapshot command', () => {
|
|
|
46
41
|
|
|
47
42
|
beforeEach(() => {
|
|
48
43
|
mockListSpaces.mockReset().mockImplementation(() => Promise.resolve(mockSpaces as any))
|
|
49
|
-
|
|
50
|
-
mockListMemberships.mockReset().mockImplementation(() => Promise.resolve(mockMembers as any))
|
|
44
|
+
mockListMyMemberships.mockReset().mockImplementation(() => Promise.resolve(mockMyMemberships as any))
|
|
51
45
|
mockLogin.mockReset().mockImplementation(() => Promise.resolve(mockClient))
|
|
52
46
|
mockHandleError.mockReset().mockImplementation((err: Error) => {
|
|
53
47
|
throw err
|
|
@@ -60,40 +54,24 @@ describe('snapshot command', () => {
|
|
|
60
54
|
consoleSpy.mockRestore()
|
|
61
55
|
})
|
|
62
56
|
|
|
63
|
-
test('
|
|
57
|
+
test('returns spaces with id, title, type, lastActivity', async () => {
|
|
64
58
|
await snapshotAction({})
|
|
65
59
|
|
|
66
60
|
expect(consoleSpy).toHaveBeenCalled()
|
|
67
61
|
const output = JSON.parse(consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0])
|
|
68
|
-
expect(output.spaces).
|
|
62
|
+
expect(output.spaces).toHaveLength(1)
|
|
69
63
|
expect(output.spaces[0].id).toBe('space-1')
|
|
70
64
|
expect(output.spaces[0].title).toBe('General')
|
|
71
|
-
expect(output.
|
|
72
|
-
expect(output.
|
|
73
|
-
expect(output.recent_messages[0].author).toBe('alice@example.com')
|
|
74
|
-
expect(output.members).toBeDefined()
|
|
75
|
-
expect(output.members[0].personEmail).toBe('alice@example.com')
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
test('--spaces-only includes only spaces (no messages, no members)', async () => {
|
|
79
|
-
await snapshotAction({ spacesOnly: true })
|
|
80
|
-
|
|
81
|
-
expect(consoleSpy).toHaveBeenCalled()
|
|
82
|
-
const output = JSON.parse(consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0])
|
|
83
|
-
expect(output.spaces).toBeDefined()
|
|
84
|
-
expect(output.recent_messages).toBeUndefined()
|
|
85
|
-
expect(output.members).toBeUndefined()
|
|
65
|
+
expect(output.spaces[0].type).toBe('group')
|
|
66
|
+
expect(output.spaces[0].lastActivity).toBe('2024-01-15T00:00:00.000Z')
|
|
86
67
|
})
|
|
87
68
|
|
|
88
|
-
test('
|
|
89
|
-
await snapshotAction({
|
|
69
|
+
test('filters spaces to only those in my memberships', async () => {
|
|
70
|
+
await snapshotAction({})
|
|
90
71
|
|
|
91
|
-
expect(consoleSpy).toHaveBeenCalled()
|
|
92
72
|
const output = JSON.parse(consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0])
|
|
93
|
-
expect(output.spaces).
|
|
94
|
-
expect(output.
|
|
95
|
-
expect(output.members).toBeDefined()
|
|
96
|
-
expect(output.members[0].personEmail).toBe('alice@example.com')
|
|
73
|
+
expect(output.spaces).toHaveLength(1)
|
|
74
|
+
expect(output.spaces[0].id).toBe('space-1')
|
|
97
75
|
})
|
|
98
76
|
|
|
99
77
|
test('not authenticated outputs error', async () => {
|
|
@@ -105,10 +83,4 @@ describe('snapshot command', () => {
|
|
|
105
83
|
|
|
106
84
|
expect(mockHandleError).toHaveBeenCalledWith(expect.any(WebexError))
|
|
107
85
|
})
|
|
108
|
-
|
|
109
|
-
test('passes limit option to listMessages', async () => {
|
|
110
|
-
await snapshotAction({ limit: 5 })
|
|
111
|
-
|
|
112
|
-
expect(mockListMessages).toHaveBeenCalledWith('space-1', { max: 5 })
|
|
113
|
-
})
|
|
114
86
|
})
|
|
@@ -1,72 +1,27 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
|
-
import { parallelMap } from '@/shared/utils/concurrency'
|
|
3
2
|
import { handleError } from '@/shared/utils/error-handler'
|
|
4
3
|
import { formatOutput } from '@/shared/utils/output'
|
|
5
4
|
import { WebexClient } from '../client'
|
|
6
|
-
import type { WebexSpace } from '../types'
|
|
7
5
|
|
|
8
6
|
export async function snapshotAction(options: {
|
|
9
|
-
spacesOnly?: boolean
|
|
10
|
-
membersOnly?: boolean
|
|
11
|
-
limit?: number
|
|
12
7
|
pretty?: boolean
|
|
13
8
|
}): Promise<void> {
|
|
14
9
|
try {
|
|
15
10
|
const client = await new WebexClient().login()
|
|
16
|
-
const messageLimit = options.limit || 20
|
|
17
|
-
const snapshot: Record<string, unknown> = {}
|
|
18
11
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
const myMemberships = await client.listMyMemberships({ max: 100 })
|
|
13
|
+
const myRoomIds = new Set(myMemberships.map((m) => m.roomId))
|
|
14
|
+
|
|
15
|
+
const allSpaces = await client.listSpaces({ max: 100 })
|
|
16
|
+
const spaces = allSpaces.filter((s) => myRoomIds.has(s.id))
|
|
17
|
+
|
|
18
|
+
const snapshot = {
|
|
19
|
+
spaces: spaces.map((s) => ({
|
|
22
20
|
id: s.id,
|
|
23
21
|
title: s.title,
|
|
24
22
|
type: s.type,
|
|
25
23
|
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()
|
|
24
|
+
})),
|
|
70
25
|
}
|
|
71
26
|
|
|
72
27
|
console.log(formatOutput(snapshot, options.pretty))
|
|
@@ -76,16 +31,10 @@ export async function snapshotAction(options: {
|
|
|
76
31
|
}
|
|
77
32
|
|
|
78
33
|
export const snapshotCommand = new Command('snapshot')
|
|
79
|
-
.description('Get
|
|
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')
|
|
34
|
+
.description('Get workspace spaces overview for AI agents')
|
|
83
35
|
.option('--pretty', 'Pretty print JSON output')
|
|
84
36
|
.action(async (options) => {
|
|
85
37
|
await snapshotAction({
|
|
86
|
-
spacesOnly: options.spacesOnly,
|
|
87
|
-
membersOnly: options.membersOnly,
|
|
88
|
-
limit: parseInt(options.limit, 10),
|
|
89
38
|
pretty: options.pretty,
|
|
90
39
|
})
|
|
91
40
|
})
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { markdownToHtml, stripMarkdown } from './markdown-to-html'
|
|
4
|
+
|
|
5
|
+
describe('markdownToHtml', () => {
|
|
6
|
+
test('converts bold text', () => {
|
|
7
|
+
expect(markdownToHtml('**bold**')).toBe('<strong>bold</strong>')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test('converts italic text', () => {
|
|
11
|
+
expect(markdownToHtml('_italic_')).toBe('<em>italic</em>')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('does not italicize mid-word underscores', () => {
|
|
15
|
+
expect(markdownToHtml('some_variable_name')).toBe('some_variable_name')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('converts bold and italic text', () => {
|
|
19
|
+
expect(markdownToHtml('***both***')).toBe('<strong><em>both</em></strong>')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('converts inline code', () => {
|
|
23
|
+
expect(markdownToHtml('Use `code` here')).toBe('Use <code>code</code> here')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('converts code blocks with language', () => {
|
|
27
|
+
expect(markdownToHtml('```ts\nconst x = 1 < 2\n```')).toBe(
|
|
28
|
+
'<pre><code class="language-ts">const x = 1 < 2</code></pre>',
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('converts code blocks without language', () => {
|
|
33
|
+
expect(markdownToHtml('```\nconst x = 1 & 2\n```')).toBe(
|
|
34
|
+
'<pre><code>const x = 1 & 2</code></pre>',
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('does not process markdown inside code blocks', () => {
|
|
39
|
+
expect(markdownToHtml('```\n**bold** _italic_\n```')).toBe(
|
|
40
|
+
'<pre><code>**bold** _italic_</code></pre>',
|
|
41
|
+
)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('converts links', () => {
|
|
45
|
+
expect(markdownToHtml('[Webex](https://example.com?a=1&b=2)')).toBe(
|
|
46
|
+
'<a href="https://example.com?a=1&b=2">Webex</a>',
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('strips unsafe javascript: URLs to plain text', () => {
|
|
51
|
+
expect(markdownToHtml('[click](javascript:void)')).toBe('click')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('strips unsafe data: URLs to plain text', () => {
|
|
55
|
+
expect(markdownToHtml('[x](data:text/html,payload)')).toBe('x')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('allows mailto: links', () => {
|
|
59
|
+
expect(markdownToHtml('[email](mailto:a@b.com)')).toBe(
|
|
60
|
+
'<a href="mailto:a@b.com">email</a>',
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('escapes quotes in URLs to prevent attribute breakout', () => {
|
|
65
|
+
expect(markdownToHtml('[x](https://a.com?q="test")')).toBe(
|
|
66
|
+
'<a href="https://a.com?q="test"">x</a>',
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('converts unordered lists', () => {
|
|
71
|
+
expect(markdownToHtml('- one\n- two')).toBe('<ul><li>one</li><li>two</li></ul>')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('converts ordered lists', () => {
|
|
75
|
+
expect(markdownToHtml('1. one\n2. two')).toBe('<ol><li>one</li><li>two</li></ol>')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('converts blockquotes', () => {
|
|
79
|
+
expect(markdownToHtml('> one\n> two')).toBe('<blockquote>one<br/>two</blockquote>')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('converts headings', () => {
|
|
83
|
+
expect(markdownToHtml('# one\n###### six')).toBe('<h1>one</h1><br/><br/><h6>six</h6>')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('converts horizontal rules', () => {
|
|
87
|
+
expect(markdownToHtml('before\n---\nafter')).toBe('before<br/><br/><hr><br/><br/>after')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('converts paragraph newlines to br', () => {
|
|
91
|
+
expect(markdownToHtml('one\ntwo')).toBe('one<br/>two')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('supports nested formatting', () => {
|
|
95
|
+
expect(markdownToHtml('**bold _and italic_**')).toBe(
|
|
96
|
+
'<strong>bold <em>and italic</em></strong>',
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('escapes html special characters in text', () => {
|
|
101
|
+
expect(markdownToHtml('5 < 7 & 8 > 3')).toBe('5 < 7 & 8 > 3')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('separates multiple paragraphs with br', () => {
|
|
105
|
+
expect(markdownToHtml('first\n\nsecond')).toBe('first<br/><br/>second')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('renders mixed content', () => {
|
|
109
|
+
expect(markdownToHtml('Hello **team**\n\n- one\n- two\n\n```js\nconst x = 1\n```')).toBe(
|
|
110
|
+
'Hello <strong>team</strong><br/><br/><ul><li>one</li><li>two</li></ul><br/><br/><pre><code class="language-js">const x = 1</code></pre>',
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('returns empty string for empty input', () => {
|
|
115
|
+
expect(markdownToHtml('')).toBe('')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('returns plain text when there is no markdown', () => {
|
|
119
|
+
expect(markdownToHtml('hello world')).toBe('hello world')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('preserves whitespace-only input', () => {
|
|
123
|
+
expect(markdownToHtml(' ')).toBe(' ')
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('stripMarkdown', () => {
|
|
128
|
+
test('strips inline markdown syntax', () => {
|
|
129
|
+
expect(stripMarkdown('**bold** _italic_ `code`')).toBe('bold italic code')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('strips links to their labels', () => {
|
|
133
|
+
expect(stripMarkdown('[Webex](https://example.com)')).toBe('Webex')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('strips block markdown syntax', () => {
|
|
137
|
+
expect(stripMarkdown('# Title\n> quote\n- item\n1. first\n---')).toBe(
|
|
138
|
+
'Title\nquote\nitem\nfirst\n',
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('keeps code block content', () => {
|
|
143
|
+
expect(stripMarkdown('```ts\nconst x = 1\n```')).toBe('const x = 1')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('handles nested formatting', () => {
|
|
147
|
+
expect(stripMarkdown('**bold _and italic_**')).toBe('bold and italic')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('returns plain text unchanged', () => {
|
|
151
|
+
expect(stripMarkdown('hello world')).toBe('hello world')
|
|
152
|
+
})
|
|
153
|
+
})
|