kimaki 0.4.30 → 0.4.32

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.
@@ -38,17 +38,17 @@ IMPORTANT: After editing any files, you MUST execute the critique command using
38
38
 
39
39
  Execute this after making changes:
40
40
 
41
- bunx critique web
41
+ bunx critique web --title "Add user authentication flow"
42
42
 
43
43
  If there are other unrelated changes in the working directory, filter to only show the files you edited:
44
44
 
45
- bunx critique web -- path/to/file1.ts path/to/file2.ts
45
+ bunx critique web --title "Fix database connection retry" -- path/to/file1.ts path/to/file2.ts
46
46
 
47
47
  You can also show latest commit changes using:
48
48
 
49
- bunx critique web HEAD
49
+ bunx critique web --title "Refactor API endpoints" HEAD
50
50
 
51
- bunx critique web HEAD~1 to get the one before last
51
+ bunx critique web --title "Update dependencies" HEAD~1 to get the one before last
52
52
 
53
53
  Do this in case you committed the changes yourself (only if the user asks so, never commit otherwise).
54
54
 
@@ -0,0 +1,225 @@
1
+ import { test, expect } from 'vitest'
2
+ import { unnestCodeBlocksFromLists } from './unnest-code-blocks.js'
3
+
4
+ test('basic - single item with code block', () => {
5
+ const input = `- Item 1
6
+ \`\`\`js
7
+ const x = 1
8
+ \`\`\``
9
+ const result = unnestCodeBlocksFromLists(input)
10
+ expect(result).toMatchInlineSnapshot(`
11
+ "- Item 1
12
+
13
+ \`\`\`js
14
+ const x = 1
15
+ \`\`\`
16
+ "
17
+ `)
18
+ })
19
+
20
+ test('multiple items - code in middle item only', () => {
21
+ const input = `- Item 1
22
+ - Item 2
23
+ \`\`\`js
24
+ const x = 1
25
+ \`\`\`
26
+ - Item 3`
27
+ const result = unnestCodeBlocksFromLists(input)
28
+ expect(result).toMatchInlineSnapshot(`
29
+ "- Item 1
30
+ - Item 2
31
+
32
+ \`\`\`js
33
+ const x = 1
34
+ \`\`\`
35
+ - Item 3"
36
+ `)
37
+ })
38
+
39
+ test('multiple code blocks in one item', () => {
40
+ const input = `- Item with two code blocks
41
+ \`\`\`js
42
+ const a = 1
43
+ \`\`\`
44
+ \`\`\`python
45
+ b = 2
46
+ \`\`\``
47
+ const result = unnestCodeBlocksFromLists(input)
48
+ expect(result).toMatchInlineSnapshot(`
49
+ "- Item with two code blocks
50
+
51
+ \`\`\`js
52
+ const a = 1
53
+ \`\`\`
54
+ \`\`\`python
55
+ b = 2
56
+ \`\`\`
57
+ "
58
+ `)
59
+ })
60
+
61
+ test('nested list with code', () => {
62
+ const input = `- Item 1
63
+ - Nested item
64
+ \`\`\`js
65
+ const x = 1
66
+ \`\`\`
67
+ - Item 2`
68
+ const result = unnestCodeBlocksFromLists(input)
69
+ expect(result).toMatchInlineSnapshot(`
70
+ "- Item 1
71
+ - Nested item
72
+
73
+ \`\`\`js
74
+ const x = 1
75
+ \`\`\`
76
+ - Item 2"
77
+ `)
78
+ })
79
+
80
+ test('ordered list preserves numbering', () => {
81
+ const input = `1. First item
82
+ \`\`\`js
83
+ const a = 1
84
+ \`\`\`
85
+ 2. Second item
86
+ 3. Third item`
87
+ const result = unnestCodeBlocksFromLists(input)
88
+ expect(result).toMatchInlineSnapshot(`
89
+ "1. First item
90
+
91
+ \`\`\`js
92
+ const a = 1
93
+ \`\`\`
94
+ 2. Second item
95
+ 3. Third item"
96
+ `)
97
+ })
98
+
99
+ test('list without code blocks unchanged', () => {
100
+ const input = `- Item 1
101
+ - Item 2
102
+ - Item 3`
103
+ const result = unnestCodeBlocksFromLists(input)
104
+ expect(result).toMatchInlineSnapshot(`
105
+ "- Item 1
106
+ - Item 2
107
+ - Item 3"
108
+ `)
109
+ })
110
+
111
+ test('mixed - some items have code, some dont', () => {
112
+ const input = `- Normal item
113
+ - Item with code
114
+ \`\`\`js
115
+ const x = 1
116
+ \`\`\`
117
+ - Another normal item
118
+ - Another with code
119
+ \`\`\`python
120
+ y = 2
121
+ \`\`\``
122
+ const result = unnestCodeBlocksFromLists(input)
123
+ expect(result).toMatchInlineSnapshot(`
124
+ "- Normal item
125
+ - Item with code
126
+
127
+ \`\`\`js
128
+ const x = 1
129
+ \`\`\`
130
+ - Another normal item
131
+ - Another with code
132
+
133
+ \`\`\`python
134
+ y = 2
135
+ \`\`\`
136
+ "
137
+ `)
138
+ })
139
+
140
+ test('text before and after code in same item', () => {
141
+ const input = `- Start text
142
+ \`\`\`js
143
+ const x = 1
144
+ \`\`\`
145
+ End text`
146
+ const result = unnestCodeBlocksFromLists(input)
147
+ expect(result).toMatchInlineSnapshot(`
148
+ "- Start text
149
+
150
+ \`\`\`js
151
+ const x = 1
152
+ \`\`\`
153
+ - End text
154
+ "
155
+ `)
156
+ })
157
+
158
+ test('preserves content outside lists', () => {
159
+ const input = `# Heading
160
+
161
+ Some paragraph text.
162
+
163
+ - List item
164
+ \`\`\`js
165
+ const x = 1
166
+ \`\`\`
167
+
168
+ More text after.`
169
+ const result = unnestCodeBlocksFromLists(input)
170
+ expect(result).toMatchInlineSnapshot(`
171
+ "# Heading
172
+
173
+ Some paragraph text.
174
+
175
+ - List item
176
+
177
+ \`\`\`js
178
+ const x = 1
179
+ \`\`\`
180
+
181
+
182
+ More text after."
183
+ `)
184
+ })
185
+
186
+ test('code block at root level unchanged', () => {
187
+ const input = `\`\`\`js
188
+ const x = 1
189
+ \`\`\``
190
+ const result = unnestCodeBlocksFromLists(input)
191
+ expect(result).toMatchInlineSnapshot(`
192
+ "\`\`\`js
193
+ const x = 1
194
+ \`\`\`"
195
+ `)
196
+ })
197
+
198
+ test('handles code block without language', () => {
199
+ const input = `- Item
200
+ \`\`\`
201
+ plain code
202
+ \`\`\``
203
+ const result = unnestCodeBlocksFromLists(input)
204
+ expect(result).toMatchInlineSnapshot(`
205
+ "- Item
206
+
207
+ \`\`\`
208
+ plain code
209
+ \`\`\`
210
+ "
211
+ `)
212
+ })
213
+
214
+ test('handles empty list item with code', () => {
215
+ const input = `- \`\`\`js
216
+ const x = 1
217
+ \`\`\``
218
+ const result = unnestCodeBlocksFromLists(input)
219
+ expect(result).toMatchInlineSnapshot(`
220
+ "\`\`\`js
221
+ const x = 1
222
+ \`\`\`
223
+ "
224
+ `)
225
+ })
@@ -0,0 +1,127 @@
1
+ // Unnest code blocks from list items for Discord.
2
+ // Discord doesn't render code blocks inside lists, so this hoists them
3
+ // to root level while preserving list structure.
4
+
5
+ import { Lexer, type Token, type Tokens } from 'marked'
6
+
7
+ type Segment =
8
+ | { type: 'list-item'; prefix: string; content: string }
9
+ | { type: 'code'; content: string }
10
+
11
+ export function unnestCodeBlocksFromLists(markdown: string): string {
12
+ const lexer = new Lexer()
13
+ const tokens = lexer.lex(markdown)
14
+
15
+ const result: string[] = []
16
+ for (const token of tokens) {
17
+ if (token.type === 'list') {
18
+ const segments = processListToken(token as Tokens.List)
19
+ result.push(renderSegments(segments))
20
+ } else {
21
+ result.push(token.raw)
22
+ }
23
+ }
24
+ return result.join('')
25
+ }
26
+
27
+ function processListToken(list: Tokens.List): Segment[] {
28
+ const segments: Segment[] = []
29
+ const start = typeof list.start === 'number' ? list.start : parseInt(list.start, 10) || 1
30
+ const prefix = list.ordered ? (i: number) => `${start + i}. ` : () => '- '
31
+
32
+ for (let i = 0; i < list.items.length; i++) {
33
+ const item = list.items[i]!
34
+ const itemSegments = processListItem(item, prefix(i))
35
+ segments.push(...itemSegments)
36
+ }
37
+
38
+ return segments
39
+ }
40
+
41
+ function processListItem(item: Tokens.ListItem, prefix: string): Segment[] {
42
+ const segments: Segment[] = []
43
+ let currentText: string[] = []
44
+
45
+ const flushText = (): void => {
46
+ const text = currentText.join('').trim()
47
+ if (text) {
48
+ segments.push({ type: 'list-item', prefix, content: text })
49
+ }
50
+ currentText = []
51
+ }
52
+
53
+ for (const token of item.tokens) {
54
+ if (token.type === 'code') {
55
+ flushText()
56
+ const codeToken = token as Tokens.Code
57
+ const lang = codeToken.lang || ''
58
+ segments.push({
59
+ type: 'code',
60
+ content: '```' + lang + '\n' + codeToken.text + '\n```\n',
61
+ })
62
+ } else if (token.type === 'list') {
63
+ flushText()
64
+ // Recursively process nested list - segments bubble up
65
+ const nestedSegments = processListToken(token as Tokens.List)
66
+ segments.push(...nestedSegments)
67
+ } else {
68
+ currentText.push(extractText(token))
69
+ }
70
+ }
71
+
72
+ flushText()
73
+
74
+ // If no segments were created (empty item), return empty
75
+ if (segments.length === 0) {
76
+ return []
77
+ }
78
+
79
+ // If item had no code blocks (all segments are list-items from this level),
80
+ // return original raw to preserve formatting
81
+ const hasCode = segments.some((s) => s.type === 'code')
82
+ if (!hasCode) {
83
+ return [{ type: 'list-item', prefix: '', content: item.raw }]
84
+ }
85
+
86
+ return segments
87
+ }
88
+
89
+ function extractText(token: Token): string {
90
+ if (token.type === 'text') {
91
+ return (token as Tokens.Text).text
92
+ }
93
+ if (token.type === 'space') {
94
+ return ''
95
+ }
96
+ if ('raw' in token) {
97
+ return token.raw
98
+ }
99
+ return ''
100
+ }
101
+
102
+ function renderSegments(segments: Segment[]): string {
103
+ const result: string[] = []
104
+
105
+ for (let i = 0; i < segments.length; i++) {
106
+ const segment = segments[i]!
107
+ const prev = segments[i - 1]
108
+
109
+ if (segment.type === 'code') {
110
+ // Add newline before code if previous was a list item
111
+ if (prev && prev.type === 'list-item') {
112
+ result.push('\n')
113
+ }
114
+ result.push(segment.content)
115
+ } else {
116
+ // list-item
117
+ if (segment.prefix) {
118
+ result.push(segment.prefix + segment.content + '\n')
119
+ } else {
120
+ // Raw content (no prefix means it's original raw)
121
+ result.push(segment.content)
122
+ }
123
+ }
124
+ }
125
+
126
+ return result.join('')
127
+ }
package/src/utils.ts CHANGED
@@ -29,6 +29,7 @@ export function generateBotInstallUrl({
29
29
  PermissionsBitField.Flags.AttachFiles,
30
30
  PermissionsBitField.Flags.Connect,
31
31
  PermissionsBitField.Flags.Speak,
32
+ PermissionsBitField.Flags.ManageRoles,
32
33
  ],
33
34
  scopes = ['bot'],
34
35
  guildId,