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.
- package/dist/cli.js +70 -37
- package/dist/commands/ask-question.js +49 -16
- package/dist/discord-bot.js +4 -1
- package/dist/discord-utils.js +4 -1
- package/dist/escape-backticks.test.js +11 -3
- package/dist/session-handler.js +47 -3
- package/dist/system-message.js +4 -4
- package/dist/unnest-code-blocks.js +110 -0
- package/dist/unnest-code-blocks.test.js +213 -0
- package/dist/utils.js +1 -0
- package/package.json +1 -1
- package/src/cli.ts +91 -46
- package/src/commands/ask-question.ts +57 -22
- package/src/discord-bot.ts +4 -1
- package/src/discord-utils.ts +4 -1
- package/src/escape-backticks.test.ts +11 -3
- package/src/session-handler.ts +50 -3
- package/src/system-message.ts +4 -4
- package/src/unnest-code-blocks.test.ts +225 -0
- package/src/unnest-code-blocks.ts +127 -0
- package/src/utils.ts +1 -0
package/src/system-message.ts
CHANGED
|
@@ -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