openclaw-ringcentral 2026.1.29-beta1

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.
@@ -0,0 +1,168 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { toRingCentralMarkdown, needsMarkdownConversion, hasCodeBlocks, markdownToAdaptiveCard } from "./markdown.js";
3
+
4
+ describe("toRingCentralMarkdown", () => {
5
+ it("converts single underscore italic to asterisk", () => {
6
+ expect(toRingCentralMarkdown("_italic_")).toBe("*italic*");
7
+ expect(toRingCentralMarkdown("This is _italic_ text")).toBe("This is *italic* text");
8
+ });
9
+
10
+ it("preserves double asterisk bold", () => {
11
+ expect(toRingCentralMarkdown("**bold**")).toBe("**bold**");
12
+ expect(toRingCentralMarkdown("This is **bold** text")).toBe("This is **bold** text");
13
+ });
14
+
15
+ it("converts double underscore to bold", () => {
16
+ expect(toRingCentralMarkdown("__bold__")).toBe("**bold**");
17
+ });
18
+
19
+ it("removes strikethrough", () => {
20
+ expect(toRingCentralMarkdown("~~strikethrough~~")).toBe("strikethrough");
21
+ expect(toRingCentralMarkdown("This is ~~deleted~~ text")).toBe("This is deleted text");
22
+ });
23
+
24
+ it("removes code blocks", () => {
25
+ expect(toRingCentralMarkdown("```\ncode\n```")).toBe("code");
26
+ expect(toRingCentralMarkdown("```javascript\nconst x = 1;\n```")).toBe("const x = 1;");
27
+ });
28
+
29
+ it("removes inline code", () => {
30
+ expect(toRingCentralMarkdown("`code`")).toBe("code");
31
+ expect(toRingCentralMarkdown("Use `npm install` to install")).toBe("Use npm install to install");
32
+ });
33
+
34
+ it("converts headings to bold", () => {
35
+ expect(toRingCentralMarkdown("# Heading 1")).toBe("**Heading 1**");
36
+ expect(toRingCentralMarkdown("## Heading 2")).toBe("**Heading 2**");
37
+ expect(toRingCentralMarkdown("### Heading 3")).toBe("**Heading 3**");
38
+ });
39
+
40
+ it("normalizes bullet lists", () => {
41
+ expect(toRingCentralMarkdown("- item")).toBe("* item");
42
+ expect(toRingCentralMarkdown("+ item")).toBe("* item");
43
+ expect(toRingCentralMarkdown("* item")).toBe("* item");
44
+ });
45
+
46
+ it("preserves links", () => {
47
+ expect(toRingCentralMarkdown("[link](https://example.com)")).toBe("[link](https://example.com)");
48
+ });
49
+
50
+ it("preserves blockquotes", () => {
51
+ expect(toRingCentralMarkdown("> quote")).toBe("> quote");
52
+ });
53
+
54
+ it("preserves numbered lists", () => {
55
+ expect(toRingCentralMarkdown("1. first\n2. second")).toBe("1. first\n2. second");
56
+ });
57
+
58
+ it("handles complex markdown", () => {
59
+ const input = `# Title
60
+
61
+ This is _italic_ and **bold** text.
62
+
63
+ - Item 1
64
+ - Item 2
65
+
66
+ \`\`\`
67
+ code block
68
+ \`\`\`
69
+
70
+ Use \`inline code\` here.`;
71
+
72
+ const expected = `**Title**
73
+
74
+ This is *italic* and **bold** text.
75
+
76
+ * Item 1
77
+ * Item 2
78
+
79
+ code block
80
+
81
+ Use inline code here.`;
82
+
83
+ expect(toRingCentralMarkdown(input)).toBe(expected);
84
+ });
85
+
86
+ it("does not convert underscore in URLs or paths", () => {
87
+ // URLs with underscores should be preserved in links
88
+ expect(toRingCentralMarkdown("[link](https://example.com/path_with_underscore)"))
89
+ .toBe("[link](https://example.com/path_with_underscore)");
90
+ });
91
+ });
92
+
93
+ describe("needsMarkdownConversion", () => {
94
+ it("returns true for text with underscore italic", () => {
95
+ expect(needsMarkdownConversion("_italic_")).toBe(true);
96
+ });
97
+
98
+ it("returns true for text with strikethrough", () => {
99
+ expect(needsMarkdownConversion("~~strike~~")).toBe(true);
100
+ });
101
+
102
+ it("returns true for text with code blocks", () => {
103
+ expect(needsMarkdownConversion("```code```")).toBe(true);
104
+ });
105
+
106
+ it("returns true for text with inline code", () => {
107
+ expect(needsMarkdownConversion("`code`")).toBe(true);
108
+ });
109
+
110
+ it("returns true for text with headings", () => {
111
+ expect(needsMarkdownConversion("# Heading")).toBe(true);
112
+ });
113
+
114
+ it("returns false for plain text", () => {
115
+ expect(needsMarkdownConversion("plain text")).toBe(false);
116
+ });
117
+
118
+ it("returns false for already compatible markdown", () => {
119
+ expect(needsMarkdownConversion("*italic* and **bold**")).toBe(false);
120
+ });
121
+ });
122
+
123
+ describe("hasCodeBlocks", () => {
124
+ it("returns true for text with code blocks", () => {
125
+ expect(hasCodeBlocks("```\ncode\n```")).toBe(true);
126
+ expect(hasCodeBlocks("```js\nconst x = 1;\n```")).toBe(true);
127
+ });
128
+
129
+ it("returns false for text without code blocks", () => {
130
+ expect(hasCodeBlocks("plain text")).toBe(false);
131
+ expect(hasCodeBlocks("`inline code`")).toBe(false);
132
+ });
133
+ });
134
+
135
+ describe("markdownToAdaptiveCard", () => {
136
+ it("converts simple text to adaptive card", () => {
137
+ const result = markdownToAdaptiveCard("Hello world");
138
+ expect(result.type).toBe("AdaptiveCard");
139
+ expect(result.version).toBe("1.3");
140
+ expect(result.body).toHaveLength(1);
141
+ expect(result.body[0].type).toBe("TextBlock");
142
+ expect(result.body[0].text).toBe("Hello world");
143
+ });
144
+
145
+ it("converts code blocks with monospace font", () => {
146
+ const result = markdownToAdaptiveCard("```js\nconst x = 1;\n```");
147
+ expect(result.body).toHaveLength(1);
148
+ expect(result.body[0].fontType).toBe("Monospace");
149
+ expect(result.body[0].text).toBe("const x = 1;");
150
+ });
151
+
152
+ it("handles mixed text and code blocks", () => {
153
+ const result = markdownToAdaptiveCard("Some text\n\n```\ncode\n```\n\nMore text");
154
+ expect(result.body).toHaveLength(3);
155
+ expect(result.body[0].text).toBe("Some text");
156
+ expect(result.body[1].fontType).toBe("Monospace");
157
+ expect(result.body[1].text).toBe("code");
158
+ expect(result.body[2].text).toBe("More text");
159
+ });
160
+
161
+ it("converts headings with bold styling", () => {
162
+ const result = markdownToAdaptiveCard("# Title");
163
+ expect(result.body).toHaveLength(1);
164
+ expect(result.body[0].weight).toBe("Bolder");
165
+ expect(result.body[0].size).toBe("Medium");
166
+ expect(result.body[0].text).toBe("Title");
167
+ });
168
+ });
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Convert standard Markdown to RingCentral Mini-Markdown format.
3
+ *
4
+ * RingCentral Mini-Markdown differences:
5
+ * - `_text_` = underline (not italic)
6
+ * - `*text*` = italic
7
+ * - `**text**` = bold
8
+ * - `[text](url)` = link
9
+ * - `> quote` = blockquote
10
+ * - `* item` = bullet list
11
+ *
12
+ * Not supported: strikethrough (~~), code blocks (```), headings (#), etc.
13
+ */
14
+
15
+ /**
16
+ * Convert standard markdown to RingCentral mini-markdown
17
+ */
18
+ export function toRingCentralMarkdown(text: string): string {
19
+ let result = text;
20
+
21
+ // 1. Preserve links - temporarily replace them to avoid modifying URLs
22
+ const linkPlaceholders: string[] = [];
23
+ result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match) => {
24
+ const idx = linkPlaceholders.length;
25
+ linkPlaceholders.push(match);
26
+ return `\x00LINK_${idx}\x00`;
27
+ });
28
+
29
+ // 2. Convert __text__ (some markdown bold) to **text**
30
+ result = result.replace(/__([^_]+)__/g, "**$1**");
31
+
32
+ // 3. Convert single _text_ to *text* for italic
33
+ // Match _text_ but not inside words (e.g., snake_case_name)
34
+ result = result.replace(/(?<=^|[\s\p{P}])_([^_\n]+)_(?=$|[\s\p{P}])/gu, "*$1*");
35
+
36
+ // 4. Remove strikethrough ~~text~~ (not supported)
37
+ result = result.replace(/~~([^~]+)~~/g, "$1");
38
+
39
+ // 5. Convert code blocks to plain text (not well supported)
40
+ // Remove triple backtick code blocks
41
+ result = result.replace(/```[\s\S]*?```/g, (match) => {
42
+ // Extract content without the backticks and language identifier
43
+ const content = match.replace(/```\w*\n?/, "").replace(/\n?```$/, "");
44
+ return content;
45
+ });
46
+
47
+ // Convert inline code `text` to plain text
48
+ result = result.replace(/`([^`]+)`/g, "$1");
49
+
50
+ // 6. Convert headings to bold (# Heading -> **Heading**)
51
+ result = result.replace(/^#{1,6}\s+(.+)$/gm, "**$1**");
52
+
53
+ // 7. Convert horizontal rules (---, ***, ___) to simple separator
54
+ result = result.replace(/^[-*_]{3,}$/gm, "---");
55
+
56
+ // 8. Normalize bullet lists (-, +, *) to * for consistency
57
+ result = result.replace(/^(\s*)[-+]\s+/gm, "$1* ");
58
+
59
+ // 9. Restore links
60
+ result = result.replace(/\x00LINK_(\d+)\x00/g, (_match, idx) => {
61
+ return linkPlaceholders[Number(idx)];
62
+ });
63
+
64
+ // 10. Keep numbered lists as-is (1. item)
65
+ // 11. Keep blockquotes as-is (> quote)
66
+
67
+ return result;
68
+ }
69
+
70
+ /**
71
+ * Check if text contains markdown that needs conversion
72
+ */
73
+ export function needsMarkdownConversion(text: string): boolean {
74
+ // Check for patterns that need conversion
75
+ return (
76
+ // Single underscore italic
77
+ /(?<![\\*_])_[^_\n]+_(?![_])/.test(text) ||
78
+ // Strikethrough
79
+ /~~[^~]+~~/.test(text) ||
80
+ // Code blocks
81
+ /```[\s\S]*?```/.test(text) ||
82
+ // Inline code
83
+ /`[^`]+`/.test(text) ||
84
+ // Headings
85
+ /^#{1,6}\s+.+$/m.test(text)
86
+ );
87
+ }
88
+
89
+ /**
90
+ * Check if text contains code blocks that would benefit from Adaptive Card formatting
91
+ */
92
+ export function hasCodeBlocks(text: string): boolean {
93
+ return /```[\s\S]*?```/.test(text);
94
+ }
95
+
96
+ /**
97
+ * Convert markdown text to an Adaptive Card structure.
98
+ * This preserves code blocks with proper formatting.
99
+ */
100
+ export function markdownToAdaptiveCard(text: string): {
101
+ type: "AdaptiveCard";
102
+ $schema: string;
103
+ version: string;
104
+ body: Array<{ type: string; text?: string; wrap?: boolean; fontType?: string; size?: string; weight?: string }>;
105
+ } {
106
+ const body: Array<{ type: string; text?: string; wrap?: boolean; fontType?: string; size?: string; weight?: string }> = [];
107
+
108
+ // Split by code blocks
109
+ const parts = text.split(/(```[\s\S]*?```)/g);
110
+
111
+ for (const part of parts) {
112
+ if (!part.trim()) continue;
113
+
114
+ if (part.startsWith("```")) {
115
+ // Extract language and code
116
+ const match = part.match(/```(\w*)\n?([\s\S]*?)\n?```/);
117
+ if (match) {
118
+ const [, , code] = match;
119
+ // Add code block with monospace font
120
+ body.push({
121
+ type: "TextBlock",
122
+ text: code.trim(),
123
+ wrap: true,
124
+ fontType: "Monospace",
125
+ });
126
+ }
127
+ } else {
128
+ // Regular text - convert markdown
129
+ const converted = toRingCentralMarkdown(part.trim());
130
+ if (converted) {
131
+ // Check if it's a heading (starts with **)
132
+ const headingMatch = converted.match(/^\*\*(.+)\*\*$/);
133
+ if (headingMatch && !converted.includes("\n")) {
134
+ body.push({
135
+ type: "TextBlock",
136
+ text: headingMatch[1],
137
+ wrap: true,
138
+ size: "Medium",
139
+ weight: "Bolder",
140
+ });
141
+ } else {
142
+ body.push({
143
+ type: "TextBlock",
144
+ text: converted,
145
+ wrap: true,
146
+ });
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ return {
153
+ type: "AdaptiveCard",
154
+ $schema: "http://adaptivecards.io/schemas/adaptive-card.json",
155
+ version: "1.3",
156
+ body,
157
+ };
158
+ }
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isSenderAllowed } from "./monitor.js";
3
+
4
+ describe("isSenderAllowed", () => {
5
+ it("returns true when allowFrom contains wildcard", () => {
6
+ expect(isSenderAllowed("12345", ["*"])).toBe(true);
7
+ expect(isSenderAllowed("any-user", ["*"])).toBe(true);
8
+ });
9
+
10
+ it("returns true when sender ID matches exactly", () => {
11
+ expect(isSenderAllowed("12345", ["12345"])).toBe(true);
12
+ expect(isSenderAllowed("12345", ["other", "12345"])).toBe(true);
13
+ });
14
+
15
+ it("returns true when sender ID matches with ringcentral: prefix", () => {
16
+ expect(isSenderAllowed("12345", ["ringcentral:12345"])).toBe(true);
17
+ expect(isSenderAllowed("12345", ["RINGCENTRAL:12345"])).toBe(true);
18
+ });
19
+
20
+ it("returns true when sender ID matches with rc: prefix", () => {
21
+ expect(isSenderAllowed("12345", ["rc:12345"])).toBe(true);
22
+ expect(isSenderAllowed("12345", ["RC:12345"])).toBe(true);
23
+ });
24
+
25
+ it("returns true when sender ID matches with user: prefix", () => {
26
+ expect(isSenderAllowed("12345", ["user:12345"])).toBe(true);
27
+ expect(isSenderAllowed("12345", ["USER:12345"])).toBe(true);
28
+ });
29
+
30
+ it("returns false when sender ID not in allowFrom", () => {
31
+ expect(isSenderAllowed("12345", ["67890"])).toBe(false);
32
+ expect(isSenderAllowed("12345", [])).toBe(false);
33
+ });
34
+
35
+ it("handles case-insensitive matching", () => {
36
+ expect(isSenderAllowed("ABC123", ["abc123"])).toBe(true);
37
+ expect(isSenderAllowed("abc123", ["ABC123"])).toBe(true);
38
+ });
39
+
40
+ it("handles whitespace in allowFrom entries", () => {
41
+ expect(isSenderAllowed("12345", [" 12345 "])).toBe(true);
42
+ });
43
+
44
+ it("ignores empty entries in allowFrom", () => {
45
+ expect(isSenderAllowed("12345", ["", "12345", " "])).toBe(true);
46
+ });
47
+ });